github.com/percona/percona-xtradb-cluster-operator@v1.14.0/pkg/controller/pxc/backup.go (about) 1 package pxc 2 3 import ( 4 "container/heap" 5 "context" 6 "crypto/sha1" 7 "encoding/hex" 8 "fmt" 9 "hash/crc32" 10 "strconv" 11 "strings" 12 "time" 13 14 "github.com/pkg/errors" 15 "github.com/robfig/cron/v3" 16 appsv1 "k8s.io/api/apps/v1" 17 k8serrors "k8s.io/apimachinery/pkg/api/errors" 18 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 19 "k8s.io/apimachinery/pkg/labels" 20 "k8s.io/apimachinery/pkg/types" 21 "sigs.k8s.io/controller-runtime/pkg/client" 22 logf "sigs.k8s.io/controller-runtime/pkg/log" 23 24 api "github.com/percona/percona-xtradb-cluster-operator/pkg/apis/pxc/v1" 25 "github.com/percona/percona-xtradb-cluster-operator/pkg/pxc/app/deployment" 26 ) 27 28 type BackupScheduleJob struct { 29 api.PXCScheduledBackupSchedule 30 JobID cron.EntryID 31 } 32 33 func (r *ReconcilePerconaXtraDBCluster) reconcileBackups(ctx context.Context, cr *api.PerconaXtraDBCluster) error { 34 log := logf.FromContext(ctx) 35 36 backups := make(map[string]api.PXCScheduledBackupSchedule) 37 backupNamePrefix := backupJobClusterPrefix(cr.Namespace + "-" + cr.Name) 38 39 if cr.Spec.Backup != nil { 40 restoreRunning, err := r.isRestoreRunning(cr.Name, cr.Namespace) 41 if err != nil { 42 return errors.Wrap(err, "failed to check if restore is running") 43 } 44 if cr.Status.Status == api.AppStateReady && cr.Spec.Backup.PITR.Enabled && !cr.Spec.Pause && !restoreRunning { 45 binlogCollector, err := deployment.GetBinlogCollectorDeployment(cr) 46 if err != nil { 47 return errors.Errorf("get binlog collector deployment for cluster '%s': %v", cr.Name, err) 48 } 49 50 currentCollector := appsv1.Deployment{} 51 err = r.client.Get(context.TODO(), types.NamespacedName{Name: binlogCollector.Name, Namespace: binlogCollector.Namespace}, ¤tCollector) 52 if err != nil && k8serrors.IsNotFound(err) { 53 if err := r.client.Create(context.TODO(), &binlogCollector); err != nil && !k8serrors.IsAlreadyExists(err) { 54 return errors.Wrapf(err, "create binlog collector deployment for cluster '%s'", cr.Name) 55 } 56 } else if err != nil { 57 return errors.Wrapf(err, "get binlog collector deployment '%s'", binlogCollector.Name) 58 } 59 60 currentCollector.Spec = binlogCollector.Spec 61 if err := r.client.Update(context.TODO(), ¤tCollector); err != nil { 62 return errors.Wrapf(err, "update binlog collector deployment '%s'", binlogCollector.Name) 63 } 64 } 65 66 if !cr.Spec.Backup.PITR.Enabled || cr.Spec.Pause || restoreRunning { 67 err := r.deletePITR(cr) 68 if err != nil { 69 return errors.Wrap(err, "delete pitr") 70 } 71 } 72 73 for i, bcp := range cr.Spec.Backup.Schedule { 74 bcp.Name = backupNamePrefix + "-" + bcp.Name 75 backups[bcp.Name] = bcp 76 strg, ok := cr.Spec.Backup.Storages[bcp.StorageName] 77 if !ok { 78 log.Info("invalid storage name for backup", "backup name", cr.Spec.Backup.Schedule[i].Name, "storage name", bcp.StorageName) 79 continue 80 } 81 82 sch := BackupScheduleJob{} 83 schRaw, ok := r.crons.backupJobs.Load(bcp.Name) 84 if ok { 85 sch = schRaw.(BackupScheduleJob) 86 } 87 88 if !ok || sch.PXCScheduledBackupSchedule.Schedule != bcp.Schedule || 89 sch.PXCScheduledBackupSchedule.StorageName != bcp.StorageName { 90 log.Info("Creating or updating backup job", "name", bcp.Name, "schedule", bcp.Schedule) 91 r.deleteBackupJob(bcp.Name) 92 jobID, err := r.crons.AddFuncWithSeconds(bcp.Schedule, r.createBackupJob(ctx, cr, bcp, strg.Type)) 93 if err != nil { 94 log.Error(err, "can't parse cronjob schedule", "backup name", cr.Spec.Backup.Schedule[i].Name, "schedule", bcp.Schedule) 95 continue 96 } 97 98 r.crons.backupJobs.Store(bcp.Name, BackupScheduleJob{ 99 PXCScheduledBackupSchedule: bcp, 100 JobID: jobID, 101 }) 102 } 103 } 104 } 105 106 r.crons.backupJobs.Range(func(k, v interface{}) bool { 107 item := v.(BackupScheduleJob) 108 if !strings.HasPrefix(item.Name, backupNamePrefix) { 109 return true 110 } 111 if spec, ok := backups[item.Name]; ok { 112 if spec.Keep > 0 { 113 oldjobs, err := r.oldScheduledBackups(cr, item.Name, spec.Keep) 114 if err != nil { 115 log.Error(err, "failed to list old backups", "name", item.Name) 116 return true 117 } 118 119 for _, todel := range oldjobs { 120 err = r.client.Delete(context.TODO(), &todel) 121 if err != nil { 122 log.Error(err, "failed to delete old backup", "name", todel.Name) 123 } 124 } 125 126 } 127 } else { 128 log.Info("deleting outdated backup job", "name", item.Name) 129 r.deleteBackupJob(item.Name) 130 } 131 132 return true 133 }) 134 135 return nil 136 } 137 138 func backupJobClusterPrefix(clusterName string) string { 139 h := sha1.New() 140 h.Write([]byte(clusterName)) 141 return hex.EncodeToString(h.Sum(nil))[:5] 142 } 143 144 // oldScheduledBackups returns list of the most old pxc-bakups that execeed `keep` limit 145 func (r *ReconcilePerconaXtraDBCluster) oldScheduledBackups(cr *api.PerconaXtraDBCluster, ancestor string, keep int) ([]api.PerconaXtraDBClusterBackup, error) { 146 bcpList := api.PerconaXtraDBClusterBackupList{} 147 err := r.client.List(context.TODO(), 148 &bcpList, 149 &client.ListOptions{ 150 Namespace: cr.Namespace, 151 LabelSelector: labels.SelectorFromSet(map[string]string{ 152 "cluster": cr.Name, 153 "ancestor": ancestor, 154 }), 155 }, 156 ) 157 if err != nil { 158 return []api.PerconaXtraDBClusterBackup{}, err 159 } 160 161 // fast path 162 if len(bcpList.Items) <= keep { 163 return []api.PerconaXtraDBClusterBackup{}, nil 164 } 165 166 // just build an ordered by creationTimestamp min-heap from items and return top "len(items) - keep" items 167 h := &minHeap{} 168 heap.Init(h) 169 for _, bcp := range bcpList.Items { 170 if bcp.Status.State == api.BackupSucceeded { 171 heap.Push(h, bcp) 172 } 173 } 174 175 if h.Len() <= keep { 176 return []api.PerconaXtraDBClusterBackup{}, nil 177 } 178 179 ret := make([]api.PerconaXtraDBClusterBackup, 0, h.Len()-keep) 180 for i := h.Len() - keep; i > 0; i-- { 181 o := heap.Pop(h).(api.PerconaXtraDBClusterBackup) 182 ret = append(ret, o) 183 } 184 185 return ret, nil 186 } 187 188 func (r *ReconcilePerconaXtraDBCluster) createBackupJob(ctx context.Context, cr *api.PerconaXtraDBCluster, backupJob api.PXCScheduledBackupSchedule, storageType api.BackupStorageType) func() { 189 log := logf.FromContext(ctx) 190 191 var fins []string 192 switch storageType { 193 case api.BackupStorageS3, api.BackupStorageAzure: 194 fins = append(fins, api.FinalizerDeleteS3Backup) 195 } 196 197 return func() { 198 localCr := &api.PerconaXtraDBCluster{} 199 err := r.client.Get(context.TODO(), types.NamespacedName{Name: cr.Name, Namespace: cr.Namespace}, localCr) 200 if k8serrors.IsNotFound(err) { 201 log.Info("cluster is not found, deleting the job", 202 "name", backupJob.Name, "cluster", cr.Name, "namespace", cr.Namespace) 203 r.deleteBackupJob(backupJob.Name) 204 return 205 } 206 207 bcp := &api.PerconaXtraDBClusterBackup{ 208 ObjectMeta: metav1.ObjectMeta{ 209 Finalizers: fins, 210 Namespace: cr.Namespace, 211 Name: generateBackupName(cr, backupJob.StorageName) + "-" + strconv.FormatUint(uint64(crc32.ChecksumIEEE([]byte(backupJob.Schedule))), 32)[:5], 212 Labels: map[string]string{ 213 "ancestor": backupJob.Name, 214 "cluster": cr.Name, 215 "type": "cron", 216 }, 217 }, 218 Spec: api.PXCBackupSpec{ 219 PXCCluster: cr.Name, 220 StorageName: backupJob.StorageName, 221 }, 222 } 223 err = r.client.Create(context.TODO(), bcp) 224 if err != nil { 225 log.Error(err, "failed to create backup") 226 } 227 } 228 } 229 230 func (r *ReconcilePerconaXtraDBCluster) deleteBackupJob(name string) { 231 job, ok := r.crons.backupJobs.LoadAndDelete(name) 232 if !ok { 233 return 234 } 235 r.crons.crons.Remove(job.(BackupScheduleJob).JobID) 236 } 237 238 func generateBackupName(cr *api.PerconaXtraDBCluster, storageName string) string { 239 result := "cron-" 240 if len(cr.Name) > 16 { 241 result += cr.Name[:16] 242 } else { 243 result += cr.Name 244 } 245 result += "-" + trimNameRight(storageName, 16) + "-" 246 tnow := time.Now() 247 result += fmt.Sprintf("%d%d%d%d%d%d", tnow.Year(), tnow.Month(), tnow.Day(), tnow.Hour(), tnow.Minute(), tnow.Second()) 248 return result 249 } 250 251 func trimNameRight(name string, ln int) string { 252 if len(name) <= ln { 253 ln = len(name) 254 } 255 256 for ; ln > 0; ln-- { 257 if name[ln-1] >= 'a' && name[ln-1] <= 'z' || 258 name[ln-1] >= '0' && name[ln-1] <= '9' { 259 break 260 } 261 } 262 263 return name[:ln] 264 } 265 266 // A minHeap is a min-heap of backup jobs. 267 type minHeap []api.PerconaXtraDBClusterBackup 268 269 func (h minHeap) Len() int { return len(h) } 270 func (h minHeap) Less(i, j int) bool { 271 return h[i].CreationTimestamp.Before(&h[j].CreationTimestamp) 272 } 273 func (h minHeap) Swap(i, j int) { h[i], h[j] = h[j], h[i] } 274 275 func (h *minHeap) Push(x interface{}) { 276 *h = append(*h, x.(api.PerconaXtraDBClusterBackup)) 277 } 278 279 func (h *minHeap) Pop() interface{} { 280 old := *h 281 n := len(old) 282 x := old[n-1] 283 *h = old[0 : n-1] 284 return x 285 } 286 287 func (r *ReconcilePerconaXtraDBCluster) deletePITR(cr *api.PerconaXtraDBCluster) error { 288 collectorDeployment := appsv1.Deployment{ 289 TypeMeta: metav1.TypeMeta{ 290 APIVersion: "apps/v1", 291 Kind: "Deployment", 292 }, 293 ObjectMeta: metav1.ObjectMeta{ 294 Name: deployment.GetBinlogCollectorDeploymentName(cr), 295 Namespace: cr.Namespace, 296 }, 297 } 298 err := r.client.Delete(context.TODO(), &collectorDeployment) 299 if err != nil && !k8serrors.IsNotFound(err) { 300 return errors.Wrap(err, "delete pitr deployment") 301 } 302 303 return nil 304 }