607 lines
20 KiB
Go
607 lines
20 KiB
Go
package storage
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/atlasos/calypso/internal/common/database"
|
|
"github.com/atlasos/calypso/internal/common/logger"
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
// SnapshotScheduleService handles snapshot schedule management
|
|
type SnapshotScheduleService struct {
|
|
db *database.DB
|
|
logger *logger.Logger
|
|
snapshotService *SnapshotService
|
|
}
|
|
|
|
// NewSnapshotScheduleService creates a new snapshot schedule service
|
|
func NewSnapshotScheduleService(db *database.DB, log *logger.Logger, snapshotService *SnapshotService) *SnapshotScheduleService {
|
|
return &SnapshotScheduleService{
|
|
db: db,
|
|
logger: log,
|
|
snapshotService: snapshotService,
|
|
}
|
|
}
|
|
|
|
// SnapshotSchedule represents a scheduled snapshot task
|
|
type SnapshotSchedule struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
Dataset string `json:"dataset"`
|
|
SnapshotNameTemplate string `json:"snapshot_name_template"`
|
|
ScheduleType string `json:"schedule_type"` // hourly, daily, weekly, monthly, cron
|
|
ScheduleConfig map[string]interface{} `json:"schedule_config"`
|
|
Recursive bool `json:"recursive"`
|
|
Enabled bool `json:"enabled"`
|
|
RetentionCount *int `json:"retention_count,omitempty"`
|
|
RetentionDays *int `json:"retention_days,omitempty"`
|
|
LastRunAt *time.Time `json:"last_run_at,omitempty"`
|
|
NextRunAt *time.Time `json:"next_run_at,omitempty"`
|
|
CreatedBy string `json:"created_by,omitempty"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
}
|
|
|
|
// CreateScheduleRequest represents a request to create a snapshot schedule
|
|
type CreateScheduleRequest struct {
|
|
Name string `json:"name" binding:"required"`
|
|
Dataset string `json:"dataset" binding:"required"`
|
|
SnapshotNameTemplate string `json:"snapshot_name_template" binding:"required"`
|
|
ScheduleType string `json:"schedule_type" binding:"required"`
|
|
ScheduleConfig map[string]interface{} `json:"schedule_config" binding:"required"`
|
|
Recursive bool `json:"recursive"`
|
|
RetentionCount *int `json:"retention_count"`
|
|
RetentionDays *int `json:"retention_days"`
|
|
}
|
|
|
|
// ListSchedules lists all snapshot schedules
|
|
func (s *SnapshotScheduleService) ListSchedules(ctx context.Context) ([]*SnapshotSchedule, error) {
|
|
query := `
|
|
SELECT id, name, dataset, snapshot_name_template, schedule_type, schedule_config,
|
|
recursive, enabled, retention_count, retention_days,
|
|
last_run_at, next_run_at, created_by, created_at, updated_at
|
|
FROM snapshot_schedules
|
|
ORDER BY created_at DESC
|
|
`
|
|
|
|
rows, err := s.db.QueryContext(ctx, query)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to query schedules: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
var schedules []*SnapshotSchedule
|
|
for rows.Next() {
|
|
schedule, err := s.scanSchedule(rows)
|
|
if err != nil {
|
|
s.logger.Error("Failed to scan schedule", "error", err)
|
|
continue
|
|
}
|
|
schedules = append(schedules, schedule)
|
|
}
|
|
|
|
return schedules, rows.Err()
|
|
}
|
|
|
|
// GetSchedule retrieves a schedule by ID
|
|
func (s *SnapshotScheduleService) GetSchedule(ctx context.Context, id string) (*SnapshotSchedule, error) {
|
|
query := `
|
|
SELECT id, name, dataset, snapshot_name_template, schedule_type, schedule_config,
|
|
recursive, enabled, retention_count, retention_days,
|
|
last_run_at, next_run_at, created_by, created_at, updated_at
|
|
FROM snapshot_schedules
|
|
WHERE id = $1
|
|
`
|
|
|
|
row := s.db.QueryRowContext(ctx, query, id)
|
|
schedule, err := s.scanScheduleRow(row)
|
|
if err != nil {
|
|
if err == sql.ErrNoRows {
|
|
return nil, fmt.Errorf("schedule not found")
|
|
}
|
|
return nil, fmt.Errorf("failed to get schedule: %w", err)
|
|
}
|
|
|
|
return schedule, nil
|
|
}
|
|
|
|
// CreateSchedule creates a new snapshot schedule
|
|
func (s *SnapshotScheduleService) CreateSchedule(ctx context.Context, req *CreateScheduleRequest, createdBy string) (*SnapshotSchedule, error) {
|
|
id := uuid.New().String()
|
|
|
|
// Calculate next run time
|
|
nextRunAt, err := s.calculateNextRun(req.ScheduleType, req.ScheduleConfig)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to calculate next run time: %w", err)
|
|
}
|
|
|
|
// Marshal schedule config to JSON
|
|
configJSON, err := json.Marshal(req.ScheduleConfig)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to marshal schedule config: %w", err)
|
|
}
|
|
|
|
query := `
|
|
INSERT INTO snapshot_schedules (
|
|
id, name, dataset, snapshot_name_template, schedule_type, schedule_config,
|
|
recursive, enabled, retention_count, retention_days,
|
|
next_run_at, created_by, created_at, updated_at
|
|
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, NOW(), NOW())
|
|
RETURNING id, name, dataset, snapshot_name_template, schedule_type, schedule_config,
|
|
recursive, enabled, retention_count, retention_days,
|
|
last_run_at, next_run_at, created_by, created_at, updated_at
|
|
`
|
|
|
|
row := s.db.QueryRowContext(ctx, query,
|
|
id, req.Name, req.Dataset, req.SnapshotNameTemplate, req.ScheduleType,
|
|
string(configJSON), req.Recursive, true, req.RetentionCount, req.RetentionDays,
|
|
nextRunAt, createdBy,
|
|
)
|
|
|
|
schedule, err := s.scanScheduleRow(row)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create schedule: %w", err)
|
|
}
|
|
|
|
s.logger.Info("Snapshot schedule created", "id", id, "name", req.Name, "dataset", req.Dataset)
|
|
return schedule, nil
|
|
}
|
|
|
|
// UpdateSchedule updates an existing schedule
|
|
func (s *SnapshotScheduleService) UpdateSchedule(ctx context.Context, id string, req *CreateScheduleRequest) (*SnapshotSchedule, error) {
|
|
// Calculate next run time
|
|
nextRunAt, err := s.calculateNextRun(req.ScheduleType, req.ScheduleConfig)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to calculate next run time: %w", err)
|
|
}
|
|
|
|
// Marshal schedule config to JSON
|
|
configJSON, err := json.Marshal(req.ScheduleConfig)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to marshal schedule config: %w", err)
|
|
}
|
|
|
|
query := `
|
|
UPDATE snapshot_schedules
|
|
SET name = $1, dataset = $2, snapshot_name_template = $3, schedule_type = $4,
|
|
schedule_config = $5, recursive = $6, retention_count = $7, retention_days = $8,
|
|
next_run_at = $9, updated_at = NOW()
|
|
WHERE id = $10
|
|
RETURNING id, name, dataset, snapshot_name_template, schedule_type, schedule_config,
|
|
recursive, enabled, retention_count, retention_days,
|
|
last_run_at, next_run_at, created_by, created_at, updated_at
|
|
`
|
|
|
|
row := s.db.QueryRowContext(ctx, query,
|
|
req.Name, req.Dataset, req.SnapshotNameTemplate, req.ScheduleType,
|
|
string(configJSON), req.Recursive, req.RetentionCount, req.RetentionDays,
|
|
nextRunAt, id,
|
|
)
|
|
|
|
schedule, err := s.scanScheduleRow(row)
|
|
if err != nil {
|
|
if err == sql.ErrNoRows {
|
|
return nil, fmt.Errorf("schedule not found")
|
|
}
|
|
return nil, fmt.Errorf("failed to update schedule: %w", err)
|
|
}
|
|
|
|
s.logger.Info("Snapshot schedule updated", "id", id)
|
|
return schedule, nil
|
|
}
|
|
|
|
// DeleteSchedule deletes a schedule
|
|
func (s *SnapshotScheduleService) DeleteSchedule(ctx context.Context, id string) error {
|
|
query := `DELETE FROM snapshot_schedules WHERE id = $1`
|
|
result, err := s.db.ExecContext(ctx, query, id)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to delete schedule: %w", err)
|
|
}
|
|
|
|
rowsAffected, err := result.RowsAffected()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get rows affected: %w", err)
|
|
}
|
|
|
|
if rowsAffected == 0 {
|
|
return fmt.Errorf("schedule not found")
|
|
}
|
|
|
|
s.logger.Info("Snapshot schedule deleted", "id", id)
|
|
return nil
|
|
}
|
|
|
|
// ToggleSchedule enables or disables a schedule
|
|
func (s *SnapshotScheduleService) ToggleSchedule(ctx context.Context, id string, enabled bool) error {
|
|
query := `UPDATE snapshot_schedules SET enabled = $1, updated_at = NOW() WHERE id = $2`
|
|
result, err := s.db.ExecContext(ctx, query, enabled, id)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to toggle schedule: %w", err)
|
|
}
|
|
|
|
rowsAffected, err := result.RowsAffected()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get rows affected: %w", err)
|
|
}
|
|
|
|
if rowsAffected == 0 {
|
|
return fmt.Errorf("schedule not found")
|
|
}
|
|
|
|
s.logger.Info("Snapshot schedule toggled", "id", id, "enabled", enabled)
|
|
return nil
|
|
}
|
|
|
|
// scanScheduleRow scans a single schedule row
|
|
func (s *SnapshotScheduleService) scanScheduleRow(row *sql.Row) (*SnapshotSchedule, error) {
|
|
var schedule SnapshotSchedule
|
|
var configJSON string
|
|
var lastRunAt, nextRunAt sql.NullTime
|
|
var retentionCount, retentionDays sql.NullInt64
|
|
var createdBy sql.NullString
|
|
|
|
err := row.Scan(
|
|
&schedule.ID, &schedule.Name, &schedule.Dataset, &schedule.SnapshotNameTemplate,
|
|
&schedule.ScheduleType, &configJSON, &schedule.Recursive, &schedule.Enabled,
|
|
&retentionCount, &retentionDays, &lastRunAt, &nextRunAt,
|
|
&createdBy, &schedule.CreatedAt, &schedule.UpdatedAt,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Parse schedule config JSON
|
|
if err := json.Unmarshal([]byte(configJSON), &schedule.ScheduleConfig); err != nil {
|
|
return nil, fmt.Errorf("failed to unmarshal schedule config: %w", err)
|
|
}
|
|
|
|
// Handle nullable fields
|
|
if lastRunAt.Valid {
|
|
schedule.LastRunAt = &lastRunAt.Time
|
|
}
|
|
if nextRunAt.Valid {
|
|
schedule.NextRunAt = &nextRunAt.Time
|
|
}
|
|
if retentionCount.Valid {
|
|
count := int(retentionCount.Int64)
|
|
schedule.RetentionCount = &count
|
|
}
|
|
if retentionDays.Valid {
|
|
days := int(retentionDays.Int64)
|
|
schedule.RetentionDays = &days
|
|
}
|
|
if createdBy.Valid {
|
|
schedule.CreatedBy = createdBy.String
|
|
}
|
|
|
|
return &schedule, nil
|
|
}
|
|
|
|
// scanSchedule scans a schedule from rows
|
|
func (s *SnapshotScheduleService) scanSchedule(rows *sql.Rows) (*SnapshotSchedule, error) {
|
|
var schedule SnapshotSchedule
|
|
var configJSON string
|
|
var lastRunAt, nextRunAt sql.NullTime
|
|
var retentionCount, retentionDays sql.NullInt64
|
|
var createdBy sql.NullString
|
|
|
|
err := rows.Scan(
|
|
&schedule.ID, &schedule.Name, &schedule.Dataset, &schedule.SnapshotNameTemplate,
|
|
&schedule.ScheduleType, &configJSON, &schedule.Recursive, &schedule.Enabled,
|
|
&retentionCount, &retentionDays, &lastRunAt, &nextRunAt,
|
|
&createdBy, &schedule.CreatedAt, &schedule.UpdatedAt,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Parse schedule config JSON
|
|
if err := json.Unmarshal([]byte(configJSON), &schedule.ScheduleConfig); err != nil {
|
|
return nil, fmt.Errorf("failed to unmarshal schedule config: %w", err)
|
|
}
|
|
|
|
// Handle nullable fields
|
|
if lastRunAt.Valid {
|
|
schedule.LastRunAt = &lastRunAt.Time
|
|
}
|
|
if nextRunAt.Valid {
|
|
schedule.NextRunAt = &nextRunAt.Time
|
|
}
|
|
if retentionCount.Valid {
|
|
count := int(retentionCount.Int64)
|
|
schedule.RetentionCount = &count
|
|
}
|
|
if retentionDays.Valid {
|
|
days := int(retentionDays.Int64)
|
|
schedule.RetentionDays = &days
|
|
}
|
|
if createdBy.Valid {
|
|
schedule.CreatedBy = createdBy.String
|
|
}
|
|
|
|
return &schedule, nil
|
|
}
|
|
|
|
// calculateNextRun calculates the next run time based on schedule type and config
|
|
func (s *SnapshotScheduleService) calculateNextRun(scheduleType string, config map[string]interface{}) (*time.Time, error) {
|
|
now := time.Now()
|
|
|
|
switch scheduleType {
|
|
case "hourly":
|
|
nextRun := now.Add(1 * time.Hour)
|
|
// Round to next hour
|
|
nextRun = nextRun.Truncate(time.Hour).Add(1 * time.Hour)
|
|
return &nextRun, nil
|
|
|
|
case "daily":
|
|
timeStr, ok := config["time"].(string)
|
|
if !ok {
|
|
timeStr = "00:00"
|
|
}
|
|
// Parse time (HH:MM format)
|
|
hour, min := 0, 0
|
|
fmt.Sscanf(timeStr, "%d:%d", &hour, &min)
|
|
nextRun := time.Date(now.Year(), now.Month(), now.Day(), hour, min, 0, 0, now.Location())
|
|
if nextRun.Before(now) {
|
|
nextRun = nextRun.Add(24 * time.Hour)
|
|
}
|
|
return &nextRun, nil
|
|
|
|
case "weekly":
|
|
dayOfWeek, ok := config["day"].(float64) // JSON numbers are float64
|
|
if !ok {
|
|
dayOfWeek = 0 // Sunday
|
|
}
|
|
timeStr, ok := config["time"].(string)
|
|
if !ok {
|
|
timeStr = "00:00"
|
|
}
|
|
hour, min := 0, 0
|
|
fmt.Sscanf(timeStr, "%d:%d", &hour, &min)
|
|
|
|
// Calculate next occurrence
|
|
currentDay := int(now.Weekday())
|
|
targetDay := int(dayOfWeek)
|
|
daysUntil := (targetDay - currentDay + 7) % 7
|
|
if daysUntil == 0 {
|
|
// If same day, check if time has passed
|
|
today := time.Date(now.Year(), now.Month(), now.Day(), hour, min, 0, 0, now.Location())
|
|
if today.Before(now) {
|
|
daysUntil = 7
|
|
}
|
|
}
|
|
nextRun := time.Date(now.Year(), now.Month(), now.Day(), hour, min, 0, 0, now.Location()).AddDate(0, 0, daysUntil)
|
|
return &nextRun, nil
|
|
|
|
case "monthly":
|
|
day, ok := config["day"].(float64)
|
|
if !ok {
|
|
day = 1
|
|
}
|
|
timeStr, ok := config["time"].(string)
|
|
if !ok {
|
|
timeStr = "00:00"
|
|
}
|
|
hour, min := 0, 0
|
|
fmt.Sscanf(timeStr, "%d:%d", &hour, &min)
|
|
|
|
// Calculate next occurrence
|
|
nextRun := time.Date(now.Year(), now.Month(), int(day), hour, min, 0, 0, now.Location())
|
|
if nextRun.Before(now) {
|
|
nextRun = nextRun.AddDate(0, 1, 0)
|
|
}
|
|
return &nextRun, nil
|
|
|
|
case "cron":
|
|
// For cron, we'll use a simple implementation
|
|
// In production, you'd want to use a proper cron parser library
|
|
_, ok := config["cron"].(string)
|
|
if !ok {
|
|
return nil, fmt.Errorf("cron expression not found in config")
|
|
}
|
|
// Simple implementation: assume next run is in 1 hour
|
|
// TODO: Implement proper cron parsing
|
|
nextRun := now.Add(1 * time.Hour)
|
|
return &nextRun, nil
|
|
|
|
default:
|
|
return nil, fmt.Errorf("unknown schedule type: %s", scheduleType)
|
|
}
|
|
}
|
|
|
|
// GetDueSchedules retrieves all enabled schedules that are due to run
|
|
func (s *SnapshotScheduleService) GetDueSchedules(ctx context.Context) ([]*SnapshotSchedule, error) {
|
|
now := time.Now()
|
|
query := `
|
|
SELECT id, name, dataset, snapshot_name_template, schedule_type, schedule_config,
|
|
recursive, enabled, retention_count, retention_days,
|
|
last_run_at, next_run_at, created_by, created_at, updated_at
|
|
FROM snapshot_schedules
|
|
WHERE enabled = true AND next_run_at <= $1
|
|
ORDER BY next_run_at ASC
|
|
`
|
|
|
|
rows, err := s.db.QueryContext(ctx, query, now)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to query due schedules: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
var schedules []*SnapshotSchedule
|
|
for rows.Next() {
|
|
schedule, err := s.scanSchedule(rows)
|
|
if err != nil {
|
|
s.logger.Error("Failed to scan schedule", "error", err)
|
|
continue
|
|
}
|
|
schedules = append(schedules, schedule)
|
|
}
|
|
|
|
return schedules, rows.Err()
|
|
}
|
|
|
|
// ExecuteSchedule executes a snapshot schedule
|
|
func (s *SnapshotScheduleService) ExecuteSchedule(ctx context.Context, schedule *SnapshotSchedule) error {
|
|
now := time.Now()
|
|
|
|
// Generate snapshot name from template
|
|
snapshotName := s.generateSnapshotName(schedule.SnapshotNameTemplate, now)
|
|
|
|
// Create snapshot
|
|
if err := s.snapshotService.CreateSnapshot(ctx, schedule.Dataset, snapshotName, schedule.Recursive); err != nil {
|
|
return fmt.Errorf("failed to create snapshot: %w", err)
|
|
}
|
|
|
|
s.logger.Info("Snapshot created from schedule", "schedule", schedule.Name, "snapshot", snapshotName, "dataset", schedule.Dataset)
|
|
|
|
// Calculate next run time
|
|
nextRunAt, err := s.calculateNextRun(schedule.ScheduleType, schedule.ScheduleConfig)
|
|
if err != nil {
|
|
s.logger.Error("Failed to calculate next run time", "error", err, "schedule", schedule.Name)
|
|
// Set a default next run time (1 hour from now) if calculation fails
|
|
defaultNextRun := now.Add(1 * time.Hour)
|
|
nextRunAt = &defaultNextRun
|
|
}
|
|
|
|
// Update schedule with last run time and next run time
|
|
query := `
|
|
UPDATE snapshot_schedules
|
|
SET last_run_at = $1, next_run_at = $2, updated_at = NOW()
|
|
WHERE id = $3
|
|
`
|
|
if _, err := s.db.ExecContext(ctx, query, now, nextRunAt, schedule.ID); err != nil {
|
|
s.logger.Error("Failed to update schedule after execution", "error", err, "schedule", schedule.Name)
|
|
// Don't return error here - snapshot was created successfully
|
|
}
|
|
|
|
// Handle retention
|
|
if err := s.handleRetention(ctx, schedule); err != nil {
|
|
s.logger.Error("Failed to handle retention", "error", err, "schedule", schedule.Name)
|
|
// Don't return error - retention is best effort
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// generateSnapshotName generates a snapshot name from a template
|
|
func (s *SnapshotScheduleService) generateSnapshotName(template string, t time.Time) string {
|
|
// Replace common time format placeholders
|
|
name := template
|
|
name = strings.ReplaceAll(name, "%Y", t.Format("2006"))
|
|
name = strings.ReplaceAll(name, "%m", t.Format("01"))
|
|
name = strings.ReplaceAll(name, "%d", t.Format("02"))
|
|
name = strings.ReplaceAll(name, "%H", t.Format("15"))
|
|
name = strings.ReplaceAll(name, "%M", t.Format("04"))
|
|
name = strings.ReplaceAll(name, "%S", t.Format("05"))
|
|
name = strings.ReplaceAll(name, "%Y-%m-%d", t.Format("2006-01-02"))
|
|
name = strings.ReplaceAll(name, "%Y-%m-%d-%H%M", t.Format("2006-01-02-1504"))
|
|
name = strings.ReplaceAll(name, "%Y-%m-%d-%H%M%S", t.Format("2006-01-02-150405"))
|
|
|
|
// If no placeholders were replaced, append timestamp
|
|
if name == template {
|
|
name = fmt.Sprintf("%s-%d", template, t.Unix())
|
|
}
|
|
|
|
return name
|
|
}
|
|
|
|
// handleRetention handles snapshot retention based on schedule settings
|
|
func (s *SnapshotScheduleService) handleRetention(ctx context.Context, schedule *SnapshotSchedule) error {
|
|
// Get all snapshots for this dataset
|
|
snapshots, err := s.snapshotService.ListSnapshots(ctx, schedule.Dataset)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to list snapshots: %w", err)
|
|
}
|
|
|
|
// Filter snapshots that match this schedule's naming pattern
|
|
matchingSnapshots := make([]*Snapshot, 0)
|
|
for _, snapshot := range snapshots {
|
|
// Check if snapshot name matches the template pattern
|
|
if s.snapshotMatchesTemplate(snapshot.SnapshotName, schedule.SnapshotNameTemplate) {
|
|
matchingSnapshots = append(matchingSnapshots, snapshot)
|
|
}
|
|
}
|
|
|
|
now := time.Now()
|
|
snapshotsToDelete := make([]*Snapshot, 0)
|
|
|
|
// Apply retention_count if set
|
|
if schedule.RetentionCount != nil && *schedule.RetentionCount > 0 {
|
|
if len(matchingSnapshots) > *schedule.RetentionCount {
|
|
// Sort by creation time (oldest first)
|
|
sort.Slice(matchingSnapshots, func(i, j int) bool {
|
|
return matchingSnapshots[i].Created.Before(matchingSnapshots[j].Created)
|
|
})
|
|
// Mark excess snapshots for deletion
|
|
excessCount := len(matchingSnapshots) - *schedule.RetentionCount
|
|
for i := 0; i < excessCount; i++ {
|
|
snapshotsToDelete = append(snapshotsToDelete, matchingSnapshots[i])
|
|
}
|
|
}
|
|
}
|
|
|
|
// Apply retention_days if set
|
|
if schedule.RetentionDays != nil && *schedule.RetentionDays > 0 {
|
|
cutoffTime := now.AddDate(0, 0, -*schedule.RetentionDays)
|
|
for _, snapshot := range matchingSnapshots {
|
|
if snapshot.Created.Before(cutoffTime) {
|
|
// Check if not already marked for deletion
|
|
alreadyMarked := false
|
|
for _, marked := range snapshotsToDelete {
|
|
if marked.ID == snapshot.ID {
|
|
alreadyMarked = true
|
|
break
|
|
}
|
|
}
|
|
if !alreadyMarked {
|
|
snapshotsToDelete = append(snapshotsToDelete, snapshot)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Delete snapshots
|
|
for _, snapshot := range snapshotsToDelete {
|
|
if err := s.snapshotService.DeleteSnapshot(ctx, snapshot.Name, schedule.Recursive); err != nil {
|
|
s.logger.Error("Failed to delete snapshot for retention", "error", err, "snapshot", snapshot.Name)
|
|
continue
|
|
}
|
|
s.logger.Info("Deleted snapshot for retention", "snapshot", snapshot.Name, "schedule", schedule.Name)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// snapshotMatchesTemplate checks if a snapshot name matches a template pattern
|
|
func (s *SnapshotScheduleService) snapshotMatchesTemplate(snapshotName, template string) bool {
|
|
if template == "" {
|
|
return false
|
|
}
|
|
|
|
// Extract the base name from template (everything before the first %)
|
|
// This handles templates like "auto-%Y-%m-%d" or "daily-backup-%Y%m%d"
|
|
baseName := template
|
|
if idx := strings.Index(template, "%"); idx != -1 {
|
|
baseName = template[:idx]
|
|
}
|
|
|
|
// If template starts with a placeholder, check if snapshot name matches the pattern
|
|
// by checking if it starts with any reasonable prefix
|
|
if baseName == "" {
|
|
// Template starts with placeholder - use a more lenient check
|
|
// Check if snapshot name matches common patterns
|
|
return true // Match all if template is just placeholders
|
|
}
|
|
|
|
// Check if snapshot name starts with the base name
|
|
// This handles cases like template "auto-%Y-%m-%d" matching snapshot "auto-2024-01-15"
|
|
return strings.HasPrefix(snapshotName, baseName)
|
|
}
|