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}, &currentCollector)
    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(), &currentCollector); 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  }