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  }