github.com/1aal/kubeblocks@v0.0.0-20231107070852-e1c03e598921/pkg/dataprotection/backup/request.go (about)

     1  /*
     2  Copyright (C) 2022-2023 ApeCloud Co., Ltd
     3  
     4  This file is part of KubeBlocks project
     5  
     6  This program is free software: you can redistribute it and/or modify
     7  it under the terms of the GNU Affero General Public License as published by
     8  the Free Software Foundation, either version 3 of the License, or
     9  (at your option) any later version.
    10  
    11  This program is distributed in the hope that it will be useful
    12  but WITHOUT ANY WARRANTY; without even the implied warranty of
    13  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    14  GNU Affero General Public License for more details.
    15  
    16  You should have received a copy of the GNU Affero General Public License
    17  along with this program.  If not, see <http://www.gnu.org/licenses/>.
    18  */
    19  
    20  package backup
    21  
    22  import (
    23  	"fmt"
    24  	"reflect"
    25  
    26  	corev1 "k8s.io/api/core/v1"
    27  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    28  	"sigs.k8s.io/controller-runtime/pkg/client"
    29  
    30  	dpv1alpha1 "github.com/1aal/kubeblocks/apis/dataprotection/v1alpha1"
    31  	"github.com/1aal/kubeblocks/pkg/common"
    32  	"github.com/1aal/kubeblocks/pkg/constant"
    33  	intctrlutil "github.com/1aal/kubeblocks/pkg/controllerutil"
    34  	"github.com/1aal/kubeblocks/pkg/dataprotection/action"
    35  	dptypes "github.com/1aal/kubeblocks/pkg/dataprotection/types"
    36  	"github.com/1aal/kubeblocks/pkg/dataprotection/utils"
    37  	"github.com/1aal/kubeblocks/pkg/dataprotection/utils/boolptr"
    38  	viper "github.com/1aal/kubeblocks/pkg/viperx"
    39  )
    40  
    41  const (
    42  	BackupDataJobNamePrefix      = "dp-backup"
    43  	prebackupJobNamePrefix       = "dp-prebackup"
    44  	postbackupJobNamePrefix      = "dp-postbackup"
    45  	backupDataContainerName      = "backupdata"
    46  	syncProgressContainerName    = "sync-progress"
    47  	syncProgressSharedVolumeName = "sync-progress-shared-volume"
    48  	syncProgressSharedMountPath  = "/dp-sync-progress"
    49  )
    50  
    51  // Request is a request for a backup, with all references to other objects.
    52  type Request struct {
    53  	*dpv1alpha1.Backup
    54  	intctrlutil.RequestCtx
    55  
    56  	Client           client.Client
    57  	BackupPolicy     *dpv1alpha1.BackupPolicy
    58  	BackupMethod     *dpv1alpha1.BackupMethod
    59  	ActionSet        *dpv1alpha1.ActionSet
    60  	TargetPods       []*corev1.Pod
    61  	BackupRepoPVC    *corev1.PersistentVolumeClaim
    62  	BackupRepo       *dpv1alpha1.BackupRepo
    63  	ToolConfigSecret *corev1.Secret
    64  }
    65  
    66  func (r *Request) GetBackupType() string {
    67  	if r.ActionSet != nil {
    68  		return string(r.ActionSet.Spec.BackupType)
    69  	}
    70  	if r.BackupMethod != nil && boolptr.IsSetToTrue(r.BackupMethod.SnapshotVolumes) {
    71  		return string(dpv1alpha1.BackupTypeFull)
    72  	}
    73  	return ""
    74  }
    75  
    76  // BuildActions builds the actions for the backup.
    77  func (r *Request) BuildActions() ([]action.Action, error) {
    78  	var actions []action.Action
    79  
    80  	appendIgnoreNil := func(elems ...action.Action) {
    81  		for _, elem := range elems {
    82  			if elem == nil || reflect.ValueOf(elem).IsNil() {
    83  				continue
    84  			}
    85  			actions = append(actions, elem)
    86  		}
    87  	}
    88  
    89  	// build pre-backup actions
    90  	preBackupActions, err := r.buildPreBackupActions()
    91  	if err != nil {
    92  		return nil, err
    93  	}
    94  
    95  	// build backup data action
    96  	backupDataAction, err := r.buildBackupDataAction()
    97  	if err != nil {
    98  		return nil, err
    99  	}
   100  
   101  	// build create volume snapshot action
   102  	createVolumeSnapshotAction, err := r.buildCreateVolumeSnapshotAction()
   103  	if err != nil {
   104  		return nil, err
   105  	}
   106  
   107  	// build backup kubernetes resources action
   108  	backupKubeResourcesAction, err := r.buildBackupKubeResourcesAction()
   109  	if err != nil {
   110  		return nil, err
   111  	}
   112  
   113  	// build post-backup actions
   114  	postBackupActions, err := r.buildPostBackupActions()
   115  	if err != nil {
   116  		return nil, err
   117  	}
   118  
   119  	appendIgnoreNil(preBackupActions...)
   120  	appendIgnoreNil(backupDataAction, createVolumeSnapshotAction, backupKubeResourcesAction)
   121  	appendIgnoreNil(postBackupActions...)
   122  	return actions, nil
   123  }
   124  
   125  func (r *Request) buildPreBackupActions() ([]action.Action, error) {
   126  	if !r.backupActionSetExists() ||
   127  		len(r.ActionSet.Spec.Backup.PreBackup) == 0 {
   128  		return nil, nil
   129  	}
   130  
   131  	var actions []action.Action
   132  	for i, preBackup := range r.ActionSet.Spec.Backup.PreBackup {
   133  		a, err := r.buildAction(fmt.Sprintf("%s-%d", prebackupJobNamePrefix, i), &preBackup)
   134  		if err != nil {
   135  			return nil, err
   136  		}
   137  		actions = append(actions, a)
   138  	}
   139  	return actions, nil
   140  }
   141  
   142  func (r *Request) buildPostBackupActions() ([]action.Action, error) {
   143  	if !r.backupActionSetExists() ||
   144  		len(r.ActionSet.Spec.Backup.PostBackup) == 0 {
   145  		return nil, nil
   146  	}
   147  
   148  	var actions []action.Action
   149  	for i, postBackup := range r.ActionSet.Spec.Backup.PostBackup {
   150  		a, err := r.buildAction(fmt.Sprintf("%s-%d", postbackupJobNamePrefix, i), &postBackup)
   151  		if err != nil {
   152  			return nil, err
   153  		}
   154  		actions = append(actions, a)
   155  	}
   156  	return actions, nil
   157  }
   158  
   159  func (r *Request) buildBackupDataAction() (action.Action, error) {
   160  	if !r.backupActionSetExists() ||
   161  		r.ActionSet.Spec.Backup.BackupData == nil {
   162  		return nil, nil
   163  	}
   164  
   165  	backupDataAct := r.ActionSet.Spec.Backup.BackupData
   166  	podSpec, err := r.buildJobActionPodSpec(backupDataContainerName, &backupDataAct.JobActionSpec)
   167  	if err != nil {
   168  		return nil, fmt.Errorf("failed to build job action pod spec: %w", err)
   169  	}
   170  
   171  	if backupDataAct.SyncProgress != nil {
   172  		r.injectSyncProgressContainer(podSpec, backupDataAct.SyncProgress)
   173  	}
   174  
   175  	if r.ActionSet.Spec.BackupType == dpv1alpha1.BackupTypeFull {
   176  		return &action.JobAction{
   177  			Name:         BackupDataJobNamePrefix,
   178  			ObjectMeta:   *buildBackupJobObjMeta(r.Backup, BackupDataJobNamePrefix),
   179  			Owner:        r.Backup,
   180  			PodSpec:      podSpec,
   181  			BackOffLimit: r.BackupPolicy.Spec.BackoffLimit,
   182  		}, nil
   183  	}
   184  	return nil, fmt.Errorf("unsupported backup type %s", r.ActionSet.Spec.BackupType)
   185  }
   186  
   187  func (r *Request) buildCreateVolumeSnapshotAction() (action.Action, error) {
   188  	targetPod := r.TargetPods[0]
   189  	if r.BackupMethod == nil ||
   190  		!boolptr.IsSetToTrue(r.BackupMethod.SnapshotVolumes) {
   191  		return nil, nil
   192  	}
   193  
   194  	if r.BackupMethod.TargetVolumes == nil {
   195  		return nil, fmt.Errorf("targetVolumes is required for snapshotVolumes")
   196  	}
   197  
   198  	if volumeSnapshotEnabled, err := utils.VolumeSnapshotEnabled(r.Ctx, r.Client, targetPod, r.BackupMethod.TargetVolumes.Volumes); err != nil {
   199  		return nil, err
   200  	} else if !volumeSnapshotEnabled {
   201  		return nil, fmt.Errorf("current backup method depends on volume snapshot, but volume snapshot is not enabled")
   202  	}
   203  
   204  	pvcs, err := getPVCsByVolumeNames(r.Client, targetPod, r.BackupMethod.TargetVolumes.Volumes)
   205  	if err != nil {
   206  		return nil, err
   207  	}
   208  
   209  	if len(pvcs) == 0 {
   210  		return nil, fmt.Errorf("no PVCs found for pod %s to back up", targetPod.Name)
   211  	}
   212  
   213  	return &action.CreateVolumeSnapshotAction{
   214  		Name: "createVolumeSnapshot",
   215  		ObjectMeta: metav1.ObjectMeta{
   216  			Namespace: r.Backup.Namespace,
   217  			Name:      r.Backup.Name,
   218  			Labels:    BuildBackupWorkloadLabels(r.Backup),
   219  		},
   220  		Owner:                         r.Backup,
   221  		PersistentVolumeClaimWrappers: pvcs,
   222  	}, nil
   223  }
   224  
   225  // TODO(ldm): implement this
   226  func (r *Request) buildBackupKubeResourcesAction() (action.Action, error) {
   227  	return nil, nil
   228  }
   229  
   230  func (r *Request) buildAction(name string, act *dpv1alpha1.ActionSpec) (action.Action, error) {
   231  	if act.Exec == nil && act.Job == nil {
   232  		return nil, fmt.Errorf("action %s has no exec or job", name)
   233  	}
   234  	if act.Exec != nil && act.Job != nil {
   235  		return nil, fmt.Errorf("action %s should have only one of exec or job", name)
   236  	}
   237  	switch {
   238  	case act.Exec != nil:
   239  		return r.buildExecAction(name, act.Exec), nil
   240  	case act.Job != nil:
   241  		return r.buildJobAction(name, act.Job)
   242  	}
   243  	return nil, nil
   244  }
   245  
   246  func (r *Request) buildExecAction(name string, exec *dpv1alpha1.ExecActionSpec) action.Action {
   247  	targetPod := r.TargetPods[0]
   248  	return &action.ExecAction{
   249  		JobAction: action.JobAction{
   250  			Name:       name,
   251  			ObjectMeta: *buildBackupJobObjMeta(r.Backup, name),
   252  			Owner:      r.Backup,
   253  		},
   254  		Command:            exec.Command,
   255  		Container:          exec.Container,
   256  		Namespace:          targetPod.Namespace,
   257  		PodName:            targetPod.Name,
   258  		Timeout:            exec.Timeout,
   259  		ServiceAccountName: r.targetServiceAccountName(),
   260  	}
   261  }
   262  
   263  func (r *Request) buildJobAction(name string, job *dpv1alpha1.JobActionSpec) (action.Action, error) {
   264  	podSpec, err := r.buildJobActionPodSpec(name, job)
   265  	if err != nil {
   266  		return nil, err
   267  	}
   268  	return &action.JobAction{
   269  		Name:         name,
   270  		ObjectMeta:   *buildBackupJobObjMeta(r.Backup, name),
   271  		Owner:        r.Backup,
   272  		PodSpec:      podSpec,
   273  		BackOffLimit: r.BackupPolicy.Spec.BackoffLimit,
   274  	}, nil
   275  }
   276  
   277  func (r *Request) buildJobActionPodSpec(name string,
   278  	job *dpv1alpha1.JobActionSpec) (*corev1.PodSpec, error) {
   279  	targetPod := r.TargetPods[0]
   280  
   281  	// build environment variables, include built-in envs, envs from backupMethod
   282  	// and envs from actionSet. Latter will override former for the same name.
   283  	// env from backupMethod has the highest priority.
   284  	buildEnv := func() []corev1.EnvVar {
   285  		envVars := []corev1.EnvVar{
   286  			{
   287  				Name:  dptypes.DPBackupName,
   288  				Value: r.Backup.Name,
   289  			},
   290  			{
   291  				Name:  dptypes.DPTargetPodName,
   292  				Value: targetPod.Name,
   293  			},
   294  			{
   295  				Name:  dptypes.DPBackupBasePath,
   296  				Value: BuildBackupPath(r.Backup, r.BackupPolicy.Spec.PathPrefix),
   297  			},
   298  			{
   299  				Name:  dptypes.DPBackupInfoFile,
   300  				Value: syncProgressSharedMountPath + "/" + backupInfoFileName,
   301  			},
   302  			{
   303  				Name:  dptypes.DPTTL,
   304  				Value: r.Spec.RetentionPeriod.String(),
   305  			},
   306  		}
   307  		envVars = append(envVars, utils.BuildEnvByCredential(targetPod, r.BackupPolicy.Spec.Target.ConnectionCredential)...)
   308  		if r.ActionSet != nil {
   309  			envVars = append(envVars, r.ActionSet.Spec.Env...)
   310  		}
   311  		return utils.MergeEnv(envVars, r.BackupMethod.Env)
   312  	}
   313  
   314  	runOnTargetPodNode := func() bool {
   315  		return boolptr.IsSetToTrue(job.RunOnTargetPodNode)
   316  	}
   317  
   318  	buildVolumes := func() []corev1.Volume {
   319  		volumes := []corev1.Volume{
   320  			{
   321  				Name: syncProgressSharedVolumeName,
   322  				VolumeSource: corev1.VolumeSource{
   323  					EmptyDir: &corev1.EmptyDirVolumeSource{},
   324  				},
   325  			},
   326  		}
   327  		// only mount the volumes when the backup pod is running on the target pod node.
   328  		if runOnTargetPodNode() {
   329  			volumes = append(volumes, getVolumesByVolumeInfo(targetPod, r.BackupMethod.TargetVolumes)...)
   330  		}
   331  		return volumes
   332  	}
   333  
   334  	buildVolumeMounts := func() []corev1.VolumeMount {
   335  		volumesMount := []corev1.VolumeMount{
   336  			{
   337  				Name:      syncProgressSharedVolumeName,
   338  				MountPath: syncProgressSharedMountPath,
   339  			},
   340  		}
   341  		// only mount the volumes when the backup pod is running on the target pod node.
   342  		if runOnTargetPodNode() {
   343  			volumesMount = append(volumesMount, getVolumeMountsByVolumeInfo(targetPod, r.BackupMethod.TargetVolumes)...)
   344  		}
   345  		return volumesMount
   346  	}
   347  
   348  	runAsUser := int64(0)
   349  	env := buildEnv()
   350  	container := corev1.Container{
   351  		Name: name,
   352  		// expand the image value with the env variables.
   353  		Image:           common.Expand(job.Image, common.MappingFuncFor(utils.CovertEnvToMap(env))),
   354  		Command:         job.Command,
   355  		Env:             env,
   356  		VolumeMounts:    buildVolumeMounts(),
   357  		ImagePullPolicy: corev1.PullPolicy(viper.GetString(constant.KBImagePullPolicy)),
   358  		SecurityContext: &corev1.SecurityContext{
   359  			AllowPrivilegeEscalation: boolptr.False(),
   360  			RunAsUser:                &runAsUser,
   361  		},
   362  	}
   363  
   364  	if r.BackupMethod.RuntimeSettings != nil {
   365  		container.Resources = r.BackupMethod.RuntimeSettings.Resources
   366  	}
   367  
   368  	if r.ActionSet != nil {
   369  		container.EnvFrom = r.ActionSet.Spec.EnvFrom
   370  	}
   371  
   372  	intctrlutil.InjectZeroResourcesLimitsIfEmpty(&container)
   373  
   374  	podSpec := &corev1.PodSpec{
   375  		Containers:         []corev1.Container{container},
   376  		Volumes:            buildVolumes(),
   377  		ServiceAccountName: r.targetServiceAccountName(),
   378  		RestartPolicy:      corev1.RestartPolicyNever,
   379  	}
   380  
   381  	// if run on target pod node, set backup pod tolerations same as the target pod,
   382  	// that will make sure the backup pod can be scheduled to the target pod node.
   383  	// If not, just use the tolerations built by the environment variables.
   384  	if runOnTargetPodNode() {
   385  		podSpec.Tolerations = targetPod.Spec.Tolerations
   386  		podSpec.NodeSelector = map[string]string{
   387  			corev1.LabelHostname: targetPod.Spec.NodeName,
   388  		}
   389  	} else {
   390  		if err := utils.AddTolerations(podSpec); err != nil {
   391  			return nil, err
   392  		}
   393  	}
   394  
   395  	utils.InjectDatasafed(podSpec, r.BackupRepo, RepoVolumeMountPath,
   396  		BuildBackupPath(r.Backup, r.BackupPolicy.Spec.PathPrefix))
   397  	return podSpec, nil
   398  }
   399  
   400  // injectSyncProgressContainer injects a container to sync the backup progress.
   401  func (r *Request) injectSyncProgressContainer(podSpec *corev1.PodSpec,
   402  	sync *dpv1alpha1.SyncProgress) {
   403  	if !boolptr.IsSetToTrue(sync.Enabled) {
   404  		return
   405  	}
   406  
   407  	// build container to sync backup progress that will update the backup status
   408  	container := podSpec.Containers[0].DeepCopy()
   409  	container.Name = syncProgressContainerName
   410  	container.Image = viper.GetString(constant.KBToolsImage)
   411  	container.ImagePullPolicy = corev1.PullPolicy(viper.GetString(constant.KBImagePullPolicy))
   412  	container.Resources = corev1.ResourceRequirements{Limits: nil, Requests: nil}
   413  	intctrlutil.InjectZeroResourcesLimitsIfEmpty(container)
   414  	container.Command = []string{"sh", "-c"}
   415  
   416  	// append some envs
   417  	checkIntervalSeconds := int32(5)
   418  	if sync.IntervalSeconds != nil && *sync.IntervalSeconds > 0 {
   419  		checkIntervalSeconds = *sync.IntervalSeconds
   420  	}
   421  	container.Env = append(container.Env,
   422  		corev1.EnvVar{
   423  			Name:  dptypes.DPCheckInterval,
   424  			Value: fmt.Sprintf("%d", checkIntervalSeconds)},
   425  	)
   426  
   427  	// sync progress script will wait for the backup info file to be created,
   428  	// if the file is created, it will update the backup status and exit.
   429  	// If an exit file named with the backup info file with .exit suffix exists,
   430  	// it indicates that the container for backing up data exited abnormally,
   431  	// this script will exit.
   432  	args := fmt.Sprintf(`
   433  set -o errexit
   434  set -o nounset
   435  
   436  function update_backup_stauts() {
   437    local backup_info_file="$1"
   438    local exit_file="$1.exit"
   439    local sleep_seconds="$2"
   440    while true; do 
   441      if [ -f "$exit_file" ]; then
   442        echo "exit file $exit_file exists, exit"
   443        exit 1
   444      fi
   445      if [ -f "$backup_info_file" ]; then
   446        break
   447      fi
   448      echo "backup info file not exists, wait for ${sleep_seconds}s"
   449      sleep $sleep_seconds
   450    done
   451    local backup_info=$(cat $backup_info_file)
   452    echo backupInfo:${backup_info}
   453    local namespace="$3"
   454    local backup_name="$4"
   455    eval kubectl -n "$namespace" patch backup "$backup_name" --subresource=status --type=merge --patch '{\"status\":${backup_info}}'
   456  }
   457  update_backup_stauts ${%s} ${%s} %s %s
   458  `, dptypes.DPBackupInfoFile, dptypes.DPCheckInterval, r.Backup.Namespace, r.Backup.Name)
   459  
   460  	container.Args = []string{args}
   461  	podSpec.Containers = append(podSpec.Containers, *container)
   462  }
   463  
   464  func (r *Request) backupActionSetExists() bool {
   465  	return r.ActionSet != nil && r.ActionSet.Spec.Backup != nil
   466  }
   467  
   468  func (r *Request) targetServiceAccountName() string {
   469  	saName := r.BackupPolicy.Spec.Target.ServiceAccountName
   470  	if len(saName) > 0 {
   471  		return saName
   472  	}
   473  	// service account name is not specified, use the target pod service account
   474  	targetPod := r.TargetPods[0]
   475  	return targetPod.Spec.ServiceAccountName
   476  }