github.com/percona/percona-xtradb-cluster-operator@v1.14.0/pkg/pxc/backup/pitr.go (about)

     1  package backup
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"fmt"
     7  	"strconv"
     8  	"strings"
     9  	"time"
    10  
    11  	"github.com/pkg/errors"
    12  	appsv1 "k8s.io/api/apps/v1"
    13  	k8serrors "k8s.io/apimachinery/pkg/api/errors"
    14  	"k8s.io/apimachinery/pkg/api/meta"
    15  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    16  	"k8s.io/apimachinery/pkg/types"
    17  
    18  	"sigs.k8s.io/controller-runtime/pkg/client"
    19  	logf "sigs.k8s.io/controller-runtime/pkg/log"
    20  
    21  	"github.com/percona/percona-xtradb-cluster-operator/clientcmd"
    22  	api "github.com/percona/percona-xtradb-cluster-operator/pkg/apis/pxc/v1"
    23  	"github.com/percona/percona-xtradb-cluster-operator/pkg/pxc/app/deployment"
    24  )
    25  
    26  func CheckPITRErrors(ctx context.Context, cl client.Client, clcmd *clientcmd.Client, cr *api.PerconaXtraDBCluster) error {
    27  	log := logf.FromContext(ctx)
    28  
    29  	if cr.Spec.Backup == nil || !cr.Spec.Backup.PITR.Enabled {
    30  		return nil
    31  	}
    32  
    33  	backup, err := getLatestSuccessfulBackup(ctx, cl, cr)
    34  	if err != nil {
    35  		if errors.Is(err, ErrNoBackups) {
    36  			return nil
    37  		}
    38  		return errors.Wrap(err, "get latest successful backup")
    39  	}
    40  
    41  	if cond := meta.FindStatusCondition(backup.Status.Conditions, api.BackupConditionPITRReady); cond != nil {
    42  		if cond.Status == metav1.ConditionFalse {
    43  			return nil
    44  		}
    45  	}
    46  
    47  	err = cl.Get(ctx, types.NamespacedName{Namespace: cr.Namespace, Name: deployment.GetBinlogCollectorDeploymentName(cr)}, new(appsv1.Deployment))
    48  	if err != nil {
    49  		if k8serrors.IsNotFound(err) {
    50  			return nil
    51  		}
    52  		return errors.Wrap(err, "get binlog collector deployment")
    53  	}
    54  
    55  	collectorPod, err := deployment.GetBinlogCollectorPod(ctx, cl, cr)
    56  	if err != nil {
    57  		return errors.Wrap(err, "get binlog collector pod")
    58  	}
    59  
    60  	stdoutBuf := &bytes.Buffer{}
    61  	stderrBuf := &bytes.Buffer{}
    62  	err = clcmd.Exec(collectorPod, "pitr", []string{"/bin/bash", "-c", "cat /tmp/gap-detected"}, nil, stdoutBuf, stderrBuf, false)
    63  	if err != nil {
    64  		if strings.Contains(stderrBuf.String(), "No such file or directory") {
    65  			return nil
    66  		}
    67  		return errors.Wrapf(err, "check binlog gaps in pod %s", collectorPod.Name)
    68  	}
    69  
    70  	if stdoutBuf.Len() == 0 {
    71  		log.Info("Gap detected but GTID set is empty", "collector", collectorPod.Name)
    72  		return nil
    73  	}
    74  
    75  	missingGTIDSet := stdoutBuf.String()
    76  	log.Info("Gap detected in binary logs", "collector", collectorPod.Name, "missingGTIDSet", missingGTIDSet)
    77  
    78  	condition := metav1.Condition{
    79  		Type:               api.BackupConditionPITRReady,
    80  		Status:             metav1.ConditionFalse,
    81  		Reason:             "BinlogGapDetected",
    82  		Message:            fmt.Sprintf("Binlog with GTID set %s not found", missingGTIDSet),
    83  		LastTransitionTime: metav1.Now(),
    84  	}
    85  	meta.SetStatusCondition(&backup.Status.Conditions, condition)
    86  
    87  	if err := cl.Status().Update(ctx, backup); err != nil {
    88  		return errors.Wrap(err, "update backup status")
    89  	}
    90  
    91  	if err := deployment.RemoveGapFile(ctx, clcmd, collectorPod); err != nil {
    92  		if !errors.Is(err, deployment.GapFileNotFound) {
    93  			return errors.Wrap(err, "remove gap file")
    94  		}
    95  	}
    96  
    97  	return nil
    98  }
    99  
   100  func UpdatePITRTimeline(ctx context.Context, cl client.Client, clcmd *clientcmd.Client, cr *api.PerconaXtraDBCluster) error {
   101  	log := logf.FromContext(ctx)
   102  
   103  	if cr.Spec.Backup == nil || !cr.Spec.Backup.PITR.Enabled {
   104  		return nil
   105  	}
   106  
   107  	backup, err := getLatestSuccessfulBackup(ctx, cl, cr)
   108  	if err != nil {
   109  		if errors.Is(err, ErrNoBackups) {
   110  			return nil
   111  		}
   112  		return errors.Wrap(err, "get latest successful backup")
   113  	}
   114  
   115  	err = cl.Get(ctx, types.NamespacedName{Namespace: cr.Namespace, Name: deployment.GetBinlogCollectorDeploymentName(cr)}, new(appsv1.Deployment))
   116  	if err != nil {
   117  		if k8serrors.IsNotFound(err) {
   118  			return nil
   119  		}
   120  		return errors.Wrap(err, "get binlog collector deployment")
   121  	}
   122  
   123  	collectorPod, err := deployment.GetBinlogCollectorPod(ctx, cl, cr)
   124  	if err != nil {
   125  		return errors.Wrap(err, "get binlog collector pod")
   126  	}
   127  
   128  	stdoutBuf := &bytes.Buffer{}
   129  	stderrBuf := &bytes.Buffer{}
   130  	err = clcmd.Exec(collectorPod, "pitr", []string{"/bin/bash", "-c", "cat /tmp/pitr-timeline"}, nil, stdoutBuf, stderrBuf, false)
   131  	if err != nil {
   132  		if strings.Contains(stderrBuf.String(), "No such file or directory") {
   133  			return nil
   134  		}
   135  		return errors.Wrapf(err, "check binlog gaps in pod %s", collectorPod.Name)
   136  	}
   137  
   138  	timelines := strings.Split(stdoutBuf.String(), "\n")
   139  
   140  	latest, err := strconv.ParseInt(timelines[1], 10, 64)
   141  	if err != nil {
   142  		return errors.Wrapf(err, "parse latest timeline %s", timelines[1])
   143  	}
   144  	latestTm := time.Unix(latest, 0)
   145  
   146  	if backup.Status.LatestRestorableTime != nil && backup.Status.LatestRestorableTime.Time.Equal(latestTm) {
   147  		return nil
   148  	}
   149  
   150  	backup.Status.LatestRestorableTime = &metav1.Time{Time: latestTm}
   151  
   152  	if err := cl.Status().Update(ctx, backup); err != nil {
   153  		return errors.Wrap(err, "update backup status")
   154  	}
   155  
   156  	log.Info("Updated PITR timelines", "latest", backup.Status.LatestRestorableTime, "lastBackup", backup.Name)
   157  
   158  	return nil
   159  }
   160  
   161  var ErrNoBackups = errors.New("No backups found")
   162  
   163  func getLatestSuccessfulBackup(ctx context.Context, cl client.Client, cr *api.PerconaXtraDBCluster) (*api.PerconaXtraDBClusterBackup, error) {
   164  	bcpList := api.PerconaXtraDBClusterBackupList{}
   165  	if err := cl.List(ctx, &bcpList, &client.ListOptions{Namespace: cr.Namespace}); err != nil {
   166  		return nil, errors.Wrap(err, "get backup objects")
   167  	}
   168  
   169  	if len(bcpList.Items) == 0 {
   170  		return nil, ErrNoBackups
   171  	}
   172  
   173  	latest := bcpList.Items[0]
   174  	for _, bcp := range bcpList.Items {
   175  		if bcp.Spec.PXCCluster != cr.Name || bcp.Status.State != api.BackupSucceeded {
   176  			continue
   177  		}
   178  
   179  		if latest.ObjectMeta.CreationTimestamp.Before(&bcp.ObjectMeta.CreationTimestamp) {
   180  			latest = bcp
   181  		}
   182  	}
   183  
   184  	// if there are no successful backups, don't blindly return the first item
   185  	if latest.Status.State != api.BackupSucceeded {
   186  		return nil, ErrNoBackups
   187  	}
   188  
   189  	return &latest, nil
   190  }