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 }