diff --git a/api/v1alpha1/nonadminbackupstoragelocation_types.go b/api/v1alpha1/nonadminbackupstoragelocation_types.go new file mode 100644 index 0000000..73bcfae --- /dev/null +++ b/api/v1alpha1/nonadminbackupstoragelocation_types.go @@ -0,0 +1,101 @@ +/* +Copyright 2024. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha1 + +import ( + velerov1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// NonAdminBackupStorageLocationPhase is a simple one high-level summary of the lifecycle of an NonAdminBackupStorageLocation. +// +kubebuilder:validation:Enum=New;Available;Unavailable;Created;Deleting +type NonAdminBackupStorageLocationPhase string + +// NonAdminBackupStorageLocationPhase constants similar to velerov1.BackupStorageLocationPhase +const ( + NaBSLPhaseNew NonAdminBackupStorageLocationPhase = "New" + NaBSLPhaseAvailable NonAdminBackupStorageLocationPhase = "Available" + NaBSLPhaseUnavailable NonAdminBackupStorageLocationPhase = "Unavailable" + NaBSLPhaseCreated NonAdminBackupStorageLocationPhase = "Created" + NaBSLPhaseDeleting NonAdminBackupStorageLocationPhase = "Deleting" +) + +// NonAdminBSLCondition contains addition conditions to the +// generic ones defined as NonAdminCondition +// +kubebuilder:validation:Enum=SecretSynced;BSLSynced +type NonAdminBSLCondition string + +// Predefined NonAdminBSLConditions +const ( + NonAdminBSLConditionSecretSynced NonAdminBSLCondition = "SecretSynced" + NonAdminBSLConditionBSLSynced NonAdminBSLCondition = "BackupStorageLocationSynced" +) + +// NonAdminBackupStorageLocationSpec defines the desired state of NonAdminBackupStorageLocation +type NonAdminBackupStorageLocationSpec struct { + // Embeds the Velero BackupStorageLocationSpec to inherit all fields + velerov1.BackupStorageLocationSpec `json:",inline"` +} + +// VeleroBackupStorageLocation contains information of the related Velero backup object. +type VeleroBackupStorageLocation struct { + // status captures the current status of the Velero backup storage location. + // +optional + Status *velerov1.BackupStorageLocationStatus `json:"status,omitempty"` + + // nacuuid references the Velero BackupStorageLocation object by it's label containing same NACUUID. + // +optional + NACUUID string `json:"nacuuid,omitempty"` + + // references the Velero BackupStorageLocation object by it's name. + // +optional + Name string `json:"name,omitempty"` + + // namespace references the Namespace in which Velero backup storage location exists. + // +optional + Namespace string `json:"namespace,omitempty"` +} + +// NonAdminBackupStorageLocationStatus defines the observed state of NonAdminBackupStorageLocation +type NonAdminBackupStorageLocationStatus struct { + // Important: Run "make" to regenerate code after modifying this file + // +optional + VeleroBackupStorageLocation *VeleroBackupStorageLocation `json:"veleroBackupStorageLocation,omitempty"` + + Phase NonAdminBackupStorageLocationPhase `json:"phase,omitempty"` + Conditions []metav1.Condition `json:"conditions,omitempty"` +} + +// NonAdminBackupStorageLocation is the Schema for the nonadminbackupstoragelocations API +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +type NonAdminBackupStorageLocation struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec NonAdminBackupStorageLocationSpec `json:"spec,omitempty"` + Status NonAdminBackupStorageLocationStatus `json:"status,omitempty"` +} + +// NonAdminBackupStorageLocationList contains a list of NonAdminBackupStorageLocation +// +kubebuilder:object:root=true +type NonAdminBackupStorageLocationList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []NonAdminBackupStorageLocation `json:"items"` +} + +func init() { + SchemeBuilder.Register(&NonAdminBackupStorageLocation{}, &NonAdminBackupStorageLocationList{}) +} diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 70d86aa..31d5018 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -142,6 +142,108 @@ func (in *NonAdminBackupStatus) DeepCopy() *NonAdminBackupStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NonAdminBackupStorageLocation) DeepCopyInto(out *NonAdminBackupStorageLocation) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NonAdminBackupStorageLocation. +func (in *NonAdminBackupStorageLocation) DeepCopy() *NonAdminBackupStorageLocation { + if in == nil { + return nil + } + out := new(NonAdminBackupStorageLocation) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *NonAdminBackupStorageLocation) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NonAdminBackupStorageLocationList) DeepCopyInto(out *NonAdminBackupStorageLocationList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]NonAdminBackupStorageLocation, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NonAdminBackupStorageLocationList. +func (in *NonAdminBackupStorageLocationList) DeepCopy() *NonAdminBackupStorageLocationList { + if in == nil { + return nil + } + out := new(NonAdminBackupStorageLocationList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *NonAdminBackupStorageLocationList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NonAdminBackupStorageLocationSpec) DeepCopyInto(out *NonAdminBackupStorageLocationSpec) { + *out = *in + in.BackupStorageLocationSpec.DeepCopyInto(&out.BackupStorageLocationSpec) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NonAdminBackupStorageLocationSpec. +func (in *NonAdminBackupStorageLocationSpec) DeepCopy() *NonAdminBackupStorageLocationSpec { + if in == nil { + return nil + } + out := new(NonAdminBackupStorageLocationSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NonAdminBackupStorageLocationStatus) DeepCopyInto(out *NonAdminBackupStorageLocationStatus) { + *out = *in + if in.VeleroBackupStorageLocation != nil { + in, out := &in.VeleroBackupStorageLocation, &out.VeleroBackupStorageLocation + *out = new(VeleroBackupStorageLocation) + (*in).DeepCopyInto(*out) + } + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]metav1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NonAdminBackupStorageLocationStatus. +func (in *NonAdminBackupStorageLocationStatus) DeepCopy() *NonAdminBackupStorageLocationStatus { + if in == nil { + return nil + } + out := new(NonAdminBackupStorageLocationStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *NonAdminRestore) DeepCopyInto(out *NonAdminRestore) { *out = *in @@ -283,6 +385,26 @@ func (in *VeleroBackup) DeepCopy() *VeleroBackup { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VeleroBackupStorageLocation) DeepCopyInto(out *VeleroBackupStorageLocation) { + *out = *in + if in.Status != nil { + in, out := &in.Status, &out.Status + *out = new(v1.BackupStorageLocationStatus) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VeleroBackupStorageLocation. +func (in *VeleroBackupStorageLocation) DeepCopy() *VeleroBackupStorageLocation { + if in == nil { + return nil + } + out := new(VeleroBackupStorageLocation) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *VeleroDeleteBackupRequest) DeepCopyInto(out *VeleroDeleteBackupRequest) { *out = *in diff --git a/config/crd/bases/oadp.openshift.io_nonadminbackupstoragelocations.yaml b/config/crd/bases/oadp.openshift.io_nonadminbackupstoragelocations.yaml new file mode 100644 index 0000000..26610e3 --- /dev/null +++ b/config/crd/bases/oadp.openshift.io_nonadminbackupstoragelocations.yaml @@ -0,0 +1,276 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.14.0 + name: nonadminbackupstoragelocations.oadp.openshift.io +spec: + group: oadp.openshift.io + names: + kind: NonAdminBackupStorageLocation + listKind: NonAdminBackupStorageLocationList + plural: nonadminbackupstoragelocations + singular: nonadminbackupstoragelocation + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: NonAdminBackupStorageLocation is the Schema for the nonadminbackupstoragelocations + API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: NonAdminBackupStorageLocationSpec defines the desired state + of NonAdminBackupStorageLocation + properties: + accessMode: + description: AccessMode defines the permissions for the backup storage + location. + enum: + - ReadOnly + - ReadWrite + type: string + backupSyncPeriod: + description: BackupSyncPeriod defines how frequently to sync backup + API objects from object storage. A value of 0 disables sync. + nullable: true + type: string + config: + additionalProperties: + type: string + description: Config is for provider-specific configuration fields. + type: object + credential: + description: Credential contains the credential information intended + to be used with this location + properties: + key: + description: The key of the secret to select from. Must be a + valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + TODO: Add other useful fields. apiVersion, kind, uid? + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Drop `kubebuilder:default` when controller-gen doesn't need it https://github.com/kubernetes-sigs/kubebuilder/issues/3896. + type: string + optional: + description: Specify whether the Secret or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + default: + description: Default indicates this location is the default backup + storage location. + type: boolean + objectStorage: + description: ObjectStorageLocation specifies the settings necessary + to connect to a provider's object storage. + properties: + bucket: + description: Bucket is the bucket to use for object storage. + type: string + caCert: + description: CACert defines a CA bundle to use when verifying + TLS connections to the provider. + format: byte + type: string + prefix: + description: Prefix is the path inside a bucket to use for Velero + storage. Optional. + type: string + required: + - bucket + type: object + provider: + description: Provider is the provider of the backup storage. + type: string + validationFrequency: + description: ValidationFrequency defines how frequently to validate + the corresponding object storage. A value of 0 disables validation. + nullable: true + type: string + required: + - objectStorage + - provider + type: object + status: + description: NonAdminBackupStorageLocationStatus defines the observed + state of NonAdminBackupStorageLocation + properties: + conditions: + items: + description: "Condition contains details for one aspect of the current + state of this API Resource.\n---\nThis struct is intended for + direct use as an array at the field path .status.conditions. For + example,\n\n\n\ttype FooStatus struct{\n\t // Represents the + observations of a foo's current state.\n\t // Known .status.conditions.type + are: \"Available\", \"Progressing\", and \"Degraded\"\n\t // + +patchMergeKey=type\n\t // +patchStrategy=merge\n\t // +listType=map\n\t + \ // +listMapKey=type\n\t Conditions []metav1.Condition `json:\"conditions,omitempty\" + patchStrategy:\"merge\" patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"`\n\n\n\t + \ // other fields\n\t}" + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: |- + type of condition in CamelCase or in foo.example.com/CamelCase. + --- + Many .condition.type values are consistent across resources like Available, but because arbitrary conditions can be + useful (see .node.status.conditions), the ability to deconflict is important. + The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + phase: + description: NonAdminBackupStorageLocationPhase is a simple one high-level + summary of the lifecycle of an NonAdminBackupStorageLocation. + enum: + - New + - Available + - Unavailable + - Created + - Deleting + type: string + veleroBackupStorageLocation: + description: 'Important: Run "make" to regenerate code after modifying + this file' + properties: + nacuuid: + description: nacuuid references the Velero BackupStorageLocation + object by it's label containing same NACUUID. + type: string + name: + description: references the Velero BackupStorageLocation object + by it's name. + type: string + namespace: + description: namespace references the Namespace in which Velero + backup storage location exists. + type: string + status: + description: status captures the current status of the Velero + backup storage location. + properties: + accessMode: + description: |- + AccessMode is an unused field. + + + Deprecated: there is now an AccessMode field on the Spec and this field + will be removed entirely as of v2.0. + enum: + - ReadOnly + - ReadWrite + type: string + lastSyncedRevision: + description: |- + LastSyncedRevision is the value of the `metadata/revision` file in the backup + storage location the last time the BSL's contents were synced into the cluster. + + + Deprecated: this field is no longer updated or used for detecting changes to + the location's contents and will be removed entirely in v2.0. + type: string + lastSyncedTime: + description: |- + LastSyncedTime is the last time the contents of the location were synced into + the cluster. + format: date-time + nullable: true + type: string + lastValidationTime: + description: |- + LastValidationTime is the last time the backup store location was validated + the cluster. + format: date-time + nullable: true + type: string + message: + description: Message is a message about the backup storage + location's status. + type: string + phase: + description: Phase is the current state of the BackupStorageLocation. + enum: + - Available + - Unavailable + type: string + type: object + type: object + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/internal/common/function/function.go b/internal/common/function/function.go index aa774c8..88724d3 100644 --- a/internal/common/function/function.go +++ b/internal/common/function/function.go @@ -427,3 +427,22 @@ func checkLabelAnnotationValueIsValid(labelsOrAnnotations map[string]string, key func GetLogger(ctx context.Context, obj client.Object, key string) logr.Logger { return log.FromContext(ctx).WithValues(key, types.NamespacedName{Name: obj.GetName(), Namespace: obj.GetNamespace()}) } + +// TODO remove +func GetVeleroBackupStorageLocationByLabel(ctx context.Context, clientInstance client.Client, namespace string, labelValue string) (*velerov1.BackupStorageLocation, error) { + bslList := &velerov1.BackupStorageLocationList{} + + // Call the generic ListLabeledObjectsInNamespace function + if err := ListObjectsByLabel(ctx, clientInstance, namespace, "openshift.io/oadp-nabsl-origin-nacuuid", labelValue, bslList); err != nil { + return nil, err + } + + switch len(bslList.Items) { + case 0: + return nil, nil // No matching VeleroBackupStorageLocation found + case 1: + return &bslList.Items[0], nil // Found 1 matching VeleroBackupStorageLocation + default: + return nil, fmt.Errorf("multiple VeleroBackupStorageLocation objects found with label %s=%s in namespace '%s'", velerov1.StorageLocationLabel, labelValue, namespace) + } +} diff --git a/internal/controller/nonadminbackup_controller.go b/internal/controller/nonadminbackup_controller.go index 8841917..91be448 100644 --- a/internal/controller/nonadminbackup_controller.go +++ b/internal/controller/nonadminbackup_controller.go @@ -89,6 +89,8 @@ func (r *NonAdminBackupReconciler) Reconcile(ctx context.Context, req ctrl.Reque return ctrl.Result{}, err } + _, syncBackup := nab.Labels["openshift.io/oadp-nab-synced-from-nacuuid"] + // Determine which path to take var reconcileSteps []nonAdminBackupReconcileStepFunction @@ -121,6 +123,14 @@ func (r *NonAdminBackupReconciler) Reconcile(ctx context.Context, req ctrl.Reque r.setStatusForDirectKubernetesAPIDeletion, } + case syncBackup: + logger.V(1).Info("Executing nab sync path") + reconcileSteps = []nonAdminBackupReconcileStepFunction{ + r.setBackupUUIDInStatus, + r.setFinalizerOnNonAdminBackup, + r.createVeleroBackupAndSyncWithNonAdminBackup, + } + default: // Standard creation/update path logger.V(1).Info("Executing nab creation/update path") @@ -539,7 +549,12 @@ func (r *NonAdminBackupReconciler) setBackupUUIDInStatus(ctx context.Context, lo } if nab.Status.VeleroBackup == nil || nab.Status.VeleroBackup.NACUUID == constant.EmptyString { - veleroBackupNACUUID := function.GenerateNacObjectUUID(nab.Namespace, nab.Name) + var veleroBackupNACUUID string + if value, ok := nab.Labels["openshift.io/oadp-nab-synced-from-nacuuid"]; ok { + veleroBackupNACUUID = value + } else { + veleroBackupNACUUID = function.GenerateNacObjectUUID(nab.Namespace, nab.Name) + } nab.Status.VeleroBackup = &nacv1alpha1.VeleroBackup{ NACUUID: veleroBackupNACUUID, Namespace: r.OADPNamespace, diff --git a/internal/controller/sync_controller.go b/internal/controller/sync_controller.go new file mode 100644 index 0000000..4bbf5bc --- /dev/null +++ b/internal/controller/sync_controller.go @@ -0,0 +1,237 @@ +/* +TODO example implementation +*/ + +package controller + +import ( + "context" + "fmt" + "time" + + // "github.com/sirupsen/logrus" + velerov1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" + // "github.com/vmware-tanzu/velero/pkg/persistence" + // "github.com/vmware-tanzu/velero/pkg/plugin/clientmgmt" + // "github.com/vmware-tanzu/velero/pkg/plugin/clientmgmt/process" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + // "k8s.io/apimachinery/pkg/util/sets" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" + + nacv1alpha1 "github.com/migtools/oadp-non-admin/api/v1alpha1" + "github.com/migtools/oadp-non-admin/internal/common/constant" + "github.com/migtools/oadp-non-admin/internal/common/function" +) + +// NonAdminBackupSyncReconciler reconciles a NonAdminBackupStorageLocation object +type NonAdminBackupSyncReconciler struct { + client.Client + Scheme *runtime.Scheme + OADPNamespace string + SyncPeriod time.Duration +} + +type nacCredentialsFileStore struct{} + +func (f *nacCredentialsFileStore) Path(*corev1.SecretKeySelector) (string, error) { + return "/tmp/credentials/secret-file", nil +} + +// Reconcile is part of the main kubernetes reconciliation loop which aims to +// move the current state of the cluster closer to the desired state, +// defined in NonAdminBackupStorageLocation object Spec. +func (r *NonAdminBackupSyncReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + logger := log.FromContext(ctx) + // on NonAdminBackupStorageLocation create/update and every configured timestamp + logger.V(1).Info("NonAdminBackup Synchronization start") + + nonAdminBackupStorageLocation := &nacv1alpha1.NonAdminBackupStorageLocation{} + err := r.Get(ctx, req.NamespacedName, nonAdminBackupStorageLocation) + if err != nil { + if apierrors.IsNotFound(err) { + logger.V(1).Info(err.Error()) + return ctrl.Result{}, nil + } + logger.Error(err, "Unable to fetch NonAdminBackupStorageLocation") + return ctrl.Result{}, err + } + + // Get related Velero BSL + if nonAdminBackupStorageLocation.Status.VeleroBackupStorageLocation == nil { + err = fmt.Errorf("VeleroBackupStorageLocation not yet processed") + logger.Error(err, "VeleroBackupStorageLocation not yet processed") + return ctrl.Result{}, err + } + backupStorageLocation, err := function.GetVeleroBackupStorageLocationByLabel( + ctx, r.Client, r.OADPNamespace, + nonAdminBackupStorageLocation.Status.VeleroBackupStorageLocation.NACUUID, + ) + if err != nil { + logger.Error(err, "Failed to get VeleroBackupStorageLocation") + return ctrl.Result{}, err + } + if backupStorageLocation == nil { + err = fmt.Errorf("VeleroBackupStorageLocation not yet created") + logger.Error(err, "VeleroBackupStorageLocation not yet created") + return ctrl.Result{}, err + } + + // // OPTION 1: copy how Velero does + // // PROBLEM: NAC Pod will not have Velero Pod files + // pluginRegistry := process.NewRegistry("/plugins", logger, logger.Level) + // if err := pluginRegistry.DiscoverPlugins(); err != nil { + // return ctrl.Result{}, err + // } + // newPluginManager := func(logger logrus.FieldLogger) clientmgmt.Manager { + // return clientmgmt.NewManager(logger, logLevel, pluginRegistry) + // } + // objectStorageClient := persistence.NewObjectBackupStoreGetter(&nacCredentialsFileStore{}) + // objectStorage, err := objectStorageClient.Get(backupStorageLocation, newPluginManager(logger), logger) + // if err != nil { + // return ctrl.Result{}, err + // } + // objectStorageBackupList, err := objectStorage.ListBackups() + // if err != nil { + // return ctrl.Result{}, nil + // } + + // // get all Velero backups that + // // NAC owns + // // are finished + // // were created from same namespace as NABSL + // var veleroBackupList velerov1.BackupList + // labelSelector := client.MatchingLabels(function.GetNonAdminLabels()) + + // if err := r.List(ctx, &veleroBackupList, client.InNamespace(r.OADPNamespace), labelSelector); err != nil { + // return ctrl.Result{}, err + // } + // logger.V(1).Info(fmt.Sprintf("%v Backups owned by NAC controller in OADP namespace", len(veleroBackupList.Items))) + + // var backupsToSync []velerov1.Backup + // // var possibleBackupsToSync []velerov1.Backup + // var possibleBackupsToSync []string + // for _, backup := range veleroBackupList.Items { + // if backup.Status.CompletionTimestamp != nil && + // backup.Annotations[constant.NabOriginNamespaceAnnotation] == nonAdminBackupStorageLocation.Namespace { + // possibleBackupsToSync = append(possibleBackupsToSync, backup.Name) + // } + // } + + // objectStorageBackupSet := sets.New(objectStorageBackupList...) + // inClusterBackupSet := sets.New(possibleBackupsToSync...) + // backupsToSyncSet := objectStorageBackupSet.Difference(inClusterBackupSet) + + // for backupName := range backupsToSyncSet { + // veleroBackup := velerov1.Backup{} + // err := r.Get(ctx, types.NamespacedName{ + // Namespace: r.OADPNamespace, + // Name: backupName, + // }, &veleroBackup) + // if err != nil { + // if apierrors.IsNotFound(err) { + // // not yet synced by velero + // continue + // } + // logger.Error(err, "Unable to fetch Velero Backup") + // return ctrl.Result{}, err + // } + // backupsToSync = append(backupsToSync, veleroBackup) + // } + + // logger.V(1).Info(fmt.Sprintf("%v Backup(s) to sync to NonAdminBackupStorageLocation namespace", len(backupsToSync))) + // for _, backup := range backupsToSync { + // nab := &nacv1alpha1.NonAdminBackup{ + // ObjectMeta: v1.ObjectMeta{ + // Name: backup.Annotations[constant.NabOriginNameAnnotation], + // Namespace: backup.Annotations[constant.NabOriginNamespaceAnnotation], + // // TODO sync operation does not preserve labels + // Labels: map[string]string{ + // "openshift.io/oadp-nab-synced-from-nacuuid": backup.Labels[constant.NabOriginNACUUIDLabel], + // }, + // }, + // Spec: nacv1alpha1.NonAdminBackupSpec{ + // BackupSpec: &backup.Spec, + // }, + // } + // nab.Spec.BackupSpec.StorageLocation = nonAdminBackupStorageLocation.Name + // r.Create(ctx, nab) + // } + + // ------------------------------------------------------------------------ + + // OPTION 2: compare BSLs specs + // PROBLEM: if user deletes NABSL, and them recreates it, velero backup will point to a BSL that does not exist + + // get all Velero backups that + // NAC owns + // are finished + // were created from same namespace as NABSL + // were created from this NABSL + var veleroBackupList velerov1.BackupList + labelSelector := client.MatchingLabels(function.GetNonAdminLabels()) + + if err := r.List(ctx, &veleroBackupList, client.InNamespace(r.OADPNamespace), labelSelector); err != nil { + return ctrl.Result{}, err + } + + var backupsToSync []velerov1.Backup + var possibleBackupsToSync []velerov1.Backup + for _, backup := range veleroBackupList.Items { + if backup.Status.CompletionTimestamp != nil && + backup.Annotations[constant.NabOriginNamespaceAnnotation] == nonAdminBackupStorageLocation.Namespace && + backup.Spec.StorageLocation == backupStorageLocation.Name { + possibleBackupsToSync = append(possibleBackupsToSync, backup) + } + } + logger.V(1).Info(fmt.Sprintf("%v possible Backup(s) to be synced to NonAdminBackupStorageLocation namespace", len(possibleBackupsToSync))) + + for _, backup := range possibleBackupsToSync { + nab := &nacv1alpha1.NonAdminBackup{} + err := r.Get(ctx, types.NamespacedName{ + Namespace: backup.Annotations[constant.NabOriginNamespaceAnnotation], + Name: backup.Annotations[constant.NabOriginNameAnnotation], + }, nab) + if err != nil { + if apierrors.IsNotFound(err) { + backupsToSync = append(backupsToSync, backup) + continue + } + logger.Error(err, "Unable to fetch NonAdminBackup") + return ctrl.Result{}, err + } + } + logger.V(1).Info(fmt.Sprintf("%v Backup(s) to sync to NonAdminBackupStorageLocation namespace", len(backupsToSync))) + for _, backup := range backupsToSync { + nab := &nacv1alpha1.NonAdminBackup{ + ObjectMeta: v1.ObjectMeta{ + Name: backup.Annotations[constant.NabOriginNameAnnotation], + Namespace: backup.Annotations[constant.NabOriginNamespaceAnnotation], + // TODO sync operation does not preserve labels + Labels: map[string]string{ + "openshift.io/oadp-nab-synced-from-nacuuid": backup.Labels[constant.NabOriginNACUUIDLabel], + }, + }, + Spec: nacv1alpha1.NonAdminBackupSpec{ + BackupSpec: &backup.Spec, + }, + } + nab.Spec.BackupSpec.StorageLocation = nonAdminBackupStorageLocation.Name + r.Create(ctx, nab) + } + + logger.V(1).Info("NonAdminBackup Synchronization exit") + return ctrl.Result{RequeueAfter: r.SyncPeriod}, nil +} + +// SetupWithManager sets up the controller with the Manager. +func (r *NonAdminBackupSyncReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&nacv1alpha1.NonAdminBackupStorageLocation{}). + Complete(r) +} diff --git a/internal/controller/sync_controller_test.go b/internal/controller/sync_controller_test.go new file mode 100644 index 0000000..f08cea2 --- /dev/null +++ b/internal/controller/sync_controller_test.go @@ -0,0 +1,171 @@ +/* +Copyright 2024. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controller + +import ( + "context" + "fmt" + "time" + + "github.com/onsi/ginkgo/v2" + "github.com/onsi/gomega" + velerov1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" + v1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/wait" + ctrl "sigs.k8s.io/controller-runtime" + + nacv1alpha1 "github.com/migtools/oadp-non-admin/api/v1alpha1" +) + +type nonAdminBackupStorageLocationFullReconcileScenario struct { + spec nacv1alpha1.NonAdminBackupStorageLocationSpec +} + +func buildTestNonAdminBackupStorageLocation(nonAdminNamespace string, nonAdminName string, spec nacv1alpha1.NonAdminBackupStorageLocationSpec) *nacv1alpha1.NonAdminBackupStorageLocation { + return &nacv1alpha1.NonAdminBackupStorageLocation{ + ObjectMeta: metav1.ObjectMeta{ + Name: nonAdminName, + Namespace: nonAdminNamespace, + }, + Spec: spec, + } +} + +var _ = ginkgo.Describe("Test full reconcile loop of NonAdminBackupSyncReconciler Controller", func() { + var ( + ctx context.Context + cancel context.CancelFunc + nonAdminBackupStorageLocationName string + nonAdminBackupStorageLocationNamespace string + oadpNamespace string + counter int + ) + + ginkgo.BeforeEach(func() { + counter++ + nonAdminBackupStorageLocationName = fmt.Sprintf("non-admin-bsl-object-%v", counter) + nonAdminBackupStorageLocationNamespace = fmt.Sprintf("test-non-admin-bsl-reconcile-full-%v", counter) + oadpNamespace = nonAdminBackupStorageLocationNamespace + "-oadp" + }) + + ginkgo.AfterEach(func() { + gomega.Expect(deleteTestNamespaces(ctx, nonAdminBackupStorageLocationNamespace, oadpNamespace)).To(gomega.Succeed()) + + cancel() + // wait cancel + time.Sleep(1 * time.Second) + }) + + ginkgo.FDescribeTable("Reconcile triggered by NonAdminBackupStorageLocation Create event", + func(scenario nonAdminBackupStorageLocationFullReconcileScenario) { + ctx, cancel = context.WithCancel(context.Background()) + + gomega.Expect(createTestNamespaces(ctx, nonAdminBackupStorageLocationNamespace, oadpNamespace)).To(gomega.Succeed()) + + k8sManager, err := ctrl.NewManager(cfg, ctrl.Options{ + Scheme: k8sClient.Scheme(), + }) + gomega.Expect(err).ToNot(gomega.HaveOccurred()) + + err = (&NonAdminBackupSyncReconciler{ + Client: k8sManager.GetClient(), + Scheme: k8sManager.GetScheme(), + OADPNamespace: oadpNamespace, + SyncPeriod: 3 * time.Second, + }).SetupWithManager(k8sManager) + gomega.Expect(err).ToNot(gomega.HaveOccurred()) + + go func() { + defer ginkgo.GinkgoRecover() + err = k8sManager.Start(ctx) + gomega.Expect(err).ToNot(gomega.HaveOccurred(), "failed to run manager") + }() + // wait manager start + managerStartTimeout := 30 * time.Second + pollInterval := 100 * time.Millisecond + ctxTimeout, cancel := context.WithTimeout(ctx, managerStartTimeout) + defer cancel() + + // TODO read logs + err = wait.PollUntilContextTimeout(ctxTimeout, pollInterval, managerStartTimeout, true, func(ctx context.Context) (done bool, err error) { + select { + case <-ctx.Done(): + return false, ctx.Err() + default: + // Check if the manager has started by verifying if the client is initialized + return k8sManager.GetClient() != nil, nil + } + }) + // Check if the context timeout or another error occurred + gomega.Expect(err).ToNot(gomega.HaveOccurred(), "Manager failed to start within the timeout period") + + ginkgo.By("Waiting Reconcile of create event") + nonAdminBackupStorageLocation := buildTestNonAdminBackupStorageLocation(nonAdminBackupStorageLocationNamespace, nonAdminBackupStorageLocationName, scenario.spec) + gomega.Expect(k8sClient.Create(ctxTimeout, nonAdminBackupStorageLocation)).To(gomega.Succeed()) + // wait NonAdminRestore reconcile + time.Sleep(13 * time.Second) + + // TODO check synced NonAdminBackups specs are expected + + ginkgo.By("Waiting NonAdminBackupStorageLocation deletion") + gomega.Expect(k8sClient.Delete(ctxTimeout, nonAdminBackupStorageLocation)).To(gomega.Succeed()) + gomega.Eventually(func() (bool, error) { + err := k8sClient.Get( + ctxTimeout, + types.NamespacedName{ + Name: nonAdminBackupStorageLocationName, + Namespace: nonAdminBackupStorageLocationNamespace, + }, + nonAdminBackupStorageLocation, + ) + if apierrors.IsNotFound(err) { + return true, nil + } + return false, err + }, 10*time.Second, 1*time.Second).Should(gomega.BeTrue()) + }, + ginkgo.Entry("Should sync NonAdminBackup 5 times, then delete NonAdminBackupStorageLocation", nonAdminBackupStorageLocationFullReconcileScenario{ + spec: nacv1alpha1.NonAdminBackupStorageLocationSpec{ + BackupStorageLocationSpec: velerov1.BackupStorageLocationSpec{ + Provider: "aws", + StorageType: velerov1.StorageType{ + ObjectStorage: &velerov1.ObjectStorageLocation{ + Bucket: "my-bucket-name", + Prefix: "velero", + }, + }, + Config: map[string]string{ + "bucket": "my-bucket-name", + "prefix": "velero", + }, + Credential: &v1.SecretKeySelector{ + LocalObjectReference: v1.LocalObjectReference{ + Name: "cloud-credentials", + }, + Key: "cloud", + }, + }, + }, + }), + // ginkgo.Entry("Should check that NonAdminBackupStorageLocation update changes next sync time, then delete NonAdminBackupStorageLocation", nonAdminBackupStorageLocationFullReconcileScenario{}), + // ginkgo.Entry("Should sync only finished NonAdminBackups, then delete NonAdminBackupStorageLocation", nonAdminBackupStorageLocationFullReconcileScenario{}), + // ginkgo.Entry("Should sync only related NonAdminBackupStorageLocation NonAdminBackups, then delete NonAdminBackupStorageLocation", nonAdminBackupStorageLocationFullReconcileScenario{}), + ) +})