github.com/percona/percona-xtradb-cluster-operator@v1.14.0/pkg/controller/pxcrestore/restorer.go (about) 1 package pxcrestore 2 3 import ( 4 "context" 5 "sort" 6 "strings" 7 "time" 8 9 "github.com/pkg/errors" 10 batchv1 "k8s.io/api/batch/v1" 11 corev1 "k8s.io/api/core/v1" 12 k8serrors "k8s.io/apimachinery/pkg/api/errors" 13 "k8s.io/apimachinery/pkg/runtime" 14 "k8s.io/apimachinery/pkg/types" 15 "sigs.k8s.io/controller-runtime/pkg/client" 16 17 api "github.com/percona/percona-xtradb-cluster-operator/pkg/apis/pxc/v1" 18 "github.com/percona/percona-xtradb-cluster-operator/pkg/k8s" 19 "github.com/percona/percona-xtradb-cluster-operator/pkg/pxc/backup" 20 "github.com/percona/percona-xtradb-cluster-operator/pkg/pxc/backup/storage" 21 ) 22 23 type Restorer interface { 24 Init(ctx context.Context) error 25 Job() (*batchv1.Job, error) 26 PITRJob() (*batchv1.Job, error) 27 Finalize(ctx context.Context) error 28 Validate(ctx context.Context) error 29 ValidateJob(ctx context.Context, job *batchv1.Job) error 30 } 31 32 type s3 struct{ *restorerOptions } 33 34 func (s *s3) Init(context.Context) error { return nil } 35 func (s *s3) Finalize(context.Context) error { return nil } 36 37 func (s *s3) Job() (*batchv1.Job, error) { 38 return backup.RestoreJob(s.cr, s.bcp, s.cluster, s.bcp.Status.Destination, false) 39 } 40 41 func (s *s3) PITRJob() (*batchv1.Job, error) { 42 return backup.RestoreJob(s.cr, s.bcp, s.cluster, s.bcp.Status.Destination, true) 43 } 44 45 func (s *s3) ValidateJob(ctx context.Context, job *batchv1.Job) error { 46 if s.bcp.Status.S3.CredentialsSecret == "" { 47 // Skip validation if the credentials secret isn't set. 48 // This allows authentication via IAM roles. 49 // More info: https://github.com/percona/k8spxc-docs/blob/87f98e6ddae8114474836c0610155d05d3531e03/docs/backups-storage.md?plain=1#L116-L126 50 return nil 51 } 52 53 return s.restorerOptions.ValidateJob(ctx, job) 54 } 55 56 func (s *s3) Validate(ctx context.Context) error { 57 opts, err := storage.GetOptionsFromBackup(ctx, s.k8sClient, s.cluster, s.bcp) 58 if err != nil { 59 return errors.Wrap(err, "failed to get storage options") 60 } 61 s3cli, err := s.newStorageClient(ctx, opts) 62 if err != nil { 63 return errors.Wrap(err, "failed to create s3 client") 64 } 65 66 backupName := s.bcp.Status.Destination.BackupName() + "/" 67 objs, err := s3cli.ListObjects(ctx, backupName) 68 if err != nil { 69 return errors.Wrap(err, "failed to list objects") 70 } 71 if len(objs) == 0 { 72 return errors.New("backup not found") 73 } 74 75 return nil 76 } 77 78 type pvc struct{ *restorerOptions } 79 80 func (s *pvc) Validate(ctx context.Context) error { 81 destination := s.bcp.Status.Destination 82 83 pod, err := backup.PVCRestorePod(s.cr, s.bcp.Status.StorageName, destination.BackupName(), s.cluster) 84 if err != nil { 85 return errors.Wrap(err, "restore pod") 86 } 87 if err := k8s.SetControllerReference(s.cr, pod, s.scheme); err != nil { 88 return err 89 } 90 pod.Name += "-verify" 91 pod.Spec.Containers[0].Command = []string{"bash", "-c", `[[ $(stat -c%s /backup/xtrabackup.stream) -gt 5000000 ]]`} 92 pod.Spec.RestartPolicy = corev1.RestartPolicyNever 93 94 if err := s.k8sClient.Delete(ctx, pod); client.IgnoreNotFound(err) != nil { 95 return errors.Wrap(err, "failed to delete") 96 } 97 98 if err := s.k8sClient.Create(ctx, pod); err != nil { 99 return errors.Wrap(err, "failed to create pod") 100 } 101 for { 102 time.Sleep(time.Second * 1) 103 104 err := s.k8sClient.Get(ctx, types.NamespacedName{Name: pod.Name, Namespace: pod.Namespace}, pod) 105 if err != nil { 106 return errors.Wrap(err, "get pod status") 107 } 108 if pod.Status.Phase == corev1.PodFailed { 109 return errors.Errorf("backup files not found on %s", destination) 110 } 111 if pod.Status.Phase == corev1.PodSucceeded { 112 break 113 } 114 } 115 116 return nil 117 } 118 119 func (s *pvc) Job() (*batchv1.Job, error) { 120 return backup.RestoreJob(s.cr, s.bcp, s.cluster, "", false) 121 } 122 123 func (s *pvc) PITRJob() (*batchv1.Job, error) { 124 return nil, errors.New("pitr restore is not supported for pvc") 125 } 126 127 func (s *pvc) Init(ctx context.Context) error { 128 destination := s.bcp.Status.Destination 129 130 svc := backup.PVCRestoreService(s.cr) 131 if err := k8s.SetControllerReference(s.cr, svc, s.scheme); err != nil { 132 return err 133 } 134 pod, err := backup.PVCRestorePod(s.cr, s.bcp.Status.StorageName, destination.BackupName(), s.cluster) 135 if err != nil { 136 return errors.Wrap(err, "restore pod") 137 } 138 if err := k8s.SetControllerReference(s.cr, pod, s.scheme); err != nil { 139 return err 140 } 141 if err := s.k8sClient.Delete(ctx, svc); client.IgnoreNotFound(err) != nil { 142 return err 143 } 144 if err := s.k8sClient.Delete(ctx, pod); client.IgnoreNotFound(err) != nil { 145 return err 146 } 147 148 err = s.k8sClient.Create(ctx, svc) 149 if err != nil { 150 return errors.Wrap(err, "create service") 151 } 152 err = s.k8sClient.Create(ctx, pod) 153 if err != nil { 154 return errors.Wrap(err, "create pod") 155 } 156 for { 157 time.Sleep(time.Second * 1) 158 159 err := s.k8sClient.Get(ctx, types.NamespacedName{Name: pod.Name, Namespace: pod.Namespace}, pod) 160 if err != nil { 161 return errors.Wrap(err, "get pod status") 162 } 163 if pod.Status.Phase == corev1.PodRunning { 164 break 165 } 166 } 167 return nil 168 } 169 170 func (s *pvc) Finalize(ctx context.Context) error { 171 svc := backup.PVCRestoreService(s.cr) 172 if err := s.k8sClient.Delete(ctx, svc); err != nil { 173 return errors.Wrap(err, "failed to delete pvc service") 174 } 175 pod, err := backup.PVCRestorePod(s.cr, s.bcp.Status.StorageName, s.bcp.Status.Destination.BackupName(), s.cluster) 176 if err != nil { 177 return err 178 } 179 if err := s.k8sClient.Delete(ctx, pod); err != nil { 180 return errors.Wrap(err, "failed to delete pvc pod") 181 } 182 return nil 183 } 184 185 type azure struct{ *restorerOptions } 186 187 func (s *azure) Init(context.Context) error { return nil } 188 func (s *azure) Finalize(context.Context) error { return nil } 189 190 func (s *azure) Job() (*batchv1.Job, error) { 191 return backup.RestoreJob(s.cr, s.bcp, s.cluster, s.bcp.Status.Destination, false) 192 } 193 194 func (s *azure) PITRJob() (*batchv1.Job, error) { 195 return backup.RestoreJob(s.cr, s.bcp, s.cluster, s.bcp.Status.Destination, true) 196 } 197 198 func (s *azure) Validate(ctx context.Context) error { 199 opts, err := storage.GetOptionsFromBackup(ctx, s.k8sClient, s.cluster, s.bcp) 200 if err != nil { 201 return errors.Wrap(err, "failed to get storage options") 202 } 203 azurecli, err := s.newStorageClient(ctx, opts) 204 if err != nil { 205 return errors.Wrap(err, "failed to create s3 client") 206 } 207 208 backupName := s.bcp.Status.Destination.BackupName() + "/" 209 blobs, err := azurecli.ListObjects(ctx, backupName) 210 if err != nil { 211 return errors.Wrap(err, "list blobs") 212 } 213 214 if len(blobs) == 0 { 215 return errors.New("no backups found") 216 } 217 return nil 218 } 219 220 func (r *ReconcilePerconaXtraDBClusterRestore) getRestorer( 221 cr *api.PerconaXtraDBClusterRestore, 222 bcp *api.PerconaXtraDBClusterBackup, 223 cluster *api.PerconaXtraDBCluster, 224 ) (Restorer, error) { 225 s := restorerOptions{ 226 cr: cr, 227 bcp: bcp, 228 cluster: cluster, 229 k8sClient: r.client, 230 scheme: r.scheme, 231 newStorageClient: r.newStorageClientFunc, 232 } 233 switch s.bcp.Status.Destination.StorageTypePrefix() { 234 case api.PVCStoragePrefix: 235 sr := pvc{&s} 236 return &sr, nil 237 case api.AwsBlobStoragePrefix: 238 sr := s3{&s} 239 return &sr, nil 240 case api.AzureBlobStoragePrefix: 241 sr := azure{&s} 242 return &sr, nil 243 } 244 return nil, errors.Errorf("unknown backup storage type") 245 } 246 247 type restorerOptions struct { 248 cr *api.PerconaXtraDBClusterRestore 249 bcp *api.PerconaXtraDBClusterBackup 250 cluster *api.PerconaXtraDBCluster 251 k8sClient client.Client 252 scheme *runtime.Scheme 253 newStorageClient storage.NewClientFunc 254 } 255 256 func (opts *restorerOptions) ValidateJob(ctx context.Context, job *batchv1.Job) error { 257 cl := opts.k8sClient 258 259 secrets := []string{} 260 for _, container := range job.Spec.Template.Spec.Containers { 261 for _, env := range container.Env { 262 if env.ValueFrom != nil && env.ValueFrom.SecretKeyRef != nil && env.ValueFrom.SecretKeyRef.Name != "" { 263 secrets = append(secrets, env.ValueFrom.SecretKeyRef.Name) 264 } 265 } 266 } 267 268 notExistingSecrets := make(map[string]struct{}) 269 for _, secret := range secrets { 270 err := cl.Get(ctx, types.NamespacedName{ 271 Name: secret, 272 Namespace: job.Namespace, 273 }, new(corev1.Secret)) 274 if err != nil { 275 if k8serrors.IsNotFound(err) { 276 notExistingSecrets[secret] = struct{}{} 277 continue 278 } 279 return err 280 } 281 } 282 if len(notExistingSecrets) > 0 { 283 secrets := make([]string, 0, len(notExistingSecrets)) 284 for k := range notExistingSecrets { 285 secrets = append(secrets, k) 286 } 287 sort.StringSlice(secrets).Sort() 288 return errors.Errorf("secrets %s not found", strings.Join(secrets, ", ")) 289 } 290 291 return nil 292 }