package datamover import ( "context" "encoding/json" "fmt" "github.com/cockroachdb/errors" "github.com/sirupsen/logrus" corev1api "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "sigs.k8s.io/controller-runtime/pkg/client" velerov1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" velerov2alpha1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v2alpha1" "github.com/vmware-tanzu/velero/pkg/label" "github.com/vmware-tanzu/velero/pkg/plugin/velero" repotypes "github.com/vmware-tanzu/velero/pkg/repository/types" datamoverutil "github.com/vmware-tanzu/velero/pkg/util/datamover" ) type DataUploadDeleteAction struct { logger logrus.FieldLogger client client.Client } func (d *DataUploadDeleteAction) AppliesTo() (velero.ResourceSelector, error) { return velero.ResourceSelector{ IncludedResources: []string{"datauploads.velero.io"}, }, nil } func (d *DataUploadDeleteAction) Execute(input *velero.DeleteItemActionExecuteInput) error { d.logger.Infof("Executing DataUploadDeleteAction") du := &velerov2alpha1.DataUpload{} if err := runtime.DefaultUnstructuredConverter.FromUnstructured(input.Item.UnstructuredContent(), &du); err != nil { return errors.WithStack(errors.Wrapf(err, "failed to convert input.Item from unstructured")) } // Only create a snapshot-info ConfigMap when the DataUpload's owning // backup (its velero.io/backup-name label) matches the backup currently // being deleted. Two other cases reach this code path and must be // skipped, because the resulting CM would be unmatchable and only adds // etcd churn: // // 1. The label is missing. We have no verifiable owner, so a CM created // with the executing backup's label is a guess that deleteMovedSnapshots // cannot rely on. // 2. The label names a different backup. Velero does not support // self-protection, so this almost always means the velero namespace // was captured in a backup tarball and the DataUpload CR belongs to // an unrelated backup. Creating a CM labeled with the executing // backup mislabels the snapshot and causes the real owning backup's // deleteMovedSnapshots query to miss it, leaking the Kopia snapshot // in the object store. // // Both cases warn so misconfigured installs surface in logs. owner := du.Labels[velerov1.BackupNameLabel] switch { case owner == "": d.logger.Warnf( "DataUpload %q has no %q label, so its owning backup cannot be verified; "+ "skipping snapshot-info ConfigMap creation because a CM without a verifiable owner "+ "cannot be matched back to its snapshot at backup deletion time.", du.Name, velerov1.BackupNameLabel, ) return nil case owner != label.GetValidName(input.Backup.Name): d.logger.Warnf( "DataUpload %q belongs to backup %q but is being deleted under backup %q; "+ "this almost always means the velero namespace was included in a backup tarball. "+ "Velero does not support self-protection — exclude the velero namespace from your schedules. "+ "Skipping snapshot-info ConfigMap creation to avoid mislabeling.", du.Name, owner, input.Backup.Name, ) return nil } cm := genConfigmap(input.Backup, *du) if cm == nil { // will not fail the backup deletion return nil } err := d.client.Create(context.Background(), cm) if err != nil { return errors.WithStack(errors.Wrapf(err, "failed to create the configmap for DataUpload %s/%s", du.Namespace, du.Name)) } return nil } // generate the configmap which is to be created and used as a way to communicate the snapshot info to the backup deletion controller func genConfigmap(bak *velerov1.Backup, du velerov2alpha1.DataUpload) *corev1api.ConfigMap { if !datamoverutil.IsBuiltInDataMover(du.Spec.DataMover) || du.Status.SnapshotID == "" { return nil } snapshot := repotypes.SnapshotIdentifier{ VolumeNamespace: du.Spec.SourceNamespace, BackupStorageLocation: bak.Spec.StorageLocation, SnapshotID: du.Status.SnapshotID, RepositoryType: velerov1.BackupRepositoryTypeKopia, UploaderType: GetUploaderType(du.Spec.DataMover), Source: GetRealSource(du.Spec.SourceNamespace, du.Spec.SourcePVC), } b, err := json.Marshal(snapshot) if err != nil { return nil } data := make(map[string]string) if err := json.Unmarshal(b, &data); err != nil { return nil } return &corev1api.ConfigMap{ TypeMeta: metav1.TypeMeta{ APIVersion: corev1api.SchemeGroupVersion.String(), Kind: "ConfigMap", }, ObjectMeta: metav1.ObjectMeta{ Namespace: bak.Namespace, Name: fmt.Sprintf("%s-info", du.Name), Labels: map[string]string{ velerov1.BackupNameLabel: bak.Name, velerov1.DataUploadSnapshotInfoLabel: "true", }, }, Data: data, } } func NewDataUploadDeleteAction(logger logrus.FieldLogger, client client.Client) *DataUploadDeleteAction { return &DataUploadDeleteAction{ logger: logger, client: client, } }