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 }