From 7b6777f686a1b75249a47576822ed4e757bc1f78 Mon Sep 17 00:00:00 2001 From: Shubham Pampattiwar Date: Mon, 3 Nov 2025 19:32:42 -0800 Subject: [PATCH 1/6] Add VM file restore feature integration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit integrates the VM file restore feature into OADP operator, following the same pattern as the non-admin controller integration. Features: - Enable/disable VM file restore via DPA.Spec.VMFileRestore.Enable - Automatic deployment of oadp-vm-file-restore-controller when enabled - Support for custom resource limits via DPA configuration - Image override support for all 4 required images: * VM file restore controller * File access container * SSH sidecar * FileBrowser sidecar Changes: - API: Added VMFileRestore struct to DataProtectionApplication spec - API: Added 4 image key constants for unsupportedOverrides - Controller: Created vmfilerestore_controller.go with reconciliation logic - Validation: Added VM file restore validation requiring kubevirt and openshift plugins - CRDs: Added VirtualMachineBackupsDiscovery and VirtualMachineFileRestore - RBAC: Added ClusterRole, binding, and ServiceAccount for controller - Bundle: Updated CSV with new CRDs, environment variables, and related images - Documentation: Created comprehensive user guide at docs/config/vm_file_restore.md - Tests: Added 33 unit test scenarios with full coverage Prerequisites: - KubeVirt must be installed in the cluster - kubevirt-velero-plugin must be configured in defaultPlugins (required) - openshift-velero-plugin must be configured in defaultPlugins (required) Implements: https://github.com/migtools/oadp-vm-file-restore/issues/10 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- README.md | 1 + .../dataprotectionapplication_types.go | 23 + api/v1alpha1/zz_generated.deepcopy.go | 30 + .../oadp-operator.clusterserviceversion.yaml | 22 + ...enshift.io_dataprotectionapplications.yaml | 72 ++ ...t.io_virtualmachinebackupsdiscoveries.yaml | 417 ++++++++ ...enshift.io_virtualmachinefilerestores.yaml | 444 +++++++++ ...enshift.io_dataprotectionapplications.yaml | 72 ++ ...t.io_virtualmachinebackupsdiscoveries.yaml | 411 ++++++++ ...enshift.io_virtualmachinefilerestores.yaml | 438 +++++++++ config/crd/kustomization.yaml | 2 + config/manager/manager.yaml | 8 + config/rbac/role.yaml | 22 + .../vm-file-restore-controller_rbac/role.yaml | 134 +++ .../role_binding.yaml | 19 + .../service_account.yaml | 12 + docs/config/vm_file_restore.md | 231 +++++ .../dataprotectionapplication_controller.go | 1 + internal/controller/validator.go | 54 ++ internal/controller/validator_test.go | 287 ++++++ .../controller/vmfilerestore_controller.go | 411 ++++++++ .../vmfilerestore_controller_test.go | 897 ++++++++++++++++++ 22 files changed, 4008 insertions(+) create mode 100644 bundle/manifests/oadp.openshift.io_virtualmachinebackupsdiscoveries.yaml create mode 100644 bundle/manifests/oadp.openshift.io_virtualmachinefilerestores.yaml create mode 100644 config/crd/bases/oadp.openshift.io_virtualmachinebackupsdiscoveries.yaml create mode 100644 config/crd/bases/oadp.openshift.io_virtualmachinefilerestores.yaml create mode 100644 config/vm-file-restore-controller_rbac/role.yaml create mode 100644 config/vm-file-restore-controller_rbac/role_binding.yaml create mode 100644 config/vm-file-restore-controller_rbac/service_account.yaml create mode 100644 docs/config/vm_file_restore.md create mode 100644 internal/controller/vmfilerestore_controller.go create mode 100644 internal/controller/vmfilerestore_controller_test.go diff --git a/README.md b/README.md index b870d50e59..d3a76cc073 100644 --- a/README.md +++ b/README.md @@ -97,6 +97,7 @@ Documentation in this repository are considered unofficial and for development p 5. [Use NooBaa as a Backup Storage Location](docs/config/noobaa/install_oadp_noobaa.md) 6. [Use Velero --features flag](docs/config/features_flag.md) 7. [Use Custom Plugin Images for Velero ](docs/config/custom_plugin_images.md) + 8. [Enable VM File Restore](docs/config/vm_file_restore.md) 5. Examples 1. [Sample Apps used in OADP CI](https://github.com/openshift/oadp-operator/tree/oadp-dev/tests/e2e/sample-applications) 2. [Stateless App Backup/Restore](docs/examples/stateless.md) diff --git a/api/v1alpha1/dataprotectionapplication_types.go b/api/v1alpha1/dataprotectionapplication_types.go index 75aa1e3808..4253846ae5 100644 --- a/api/v1alpha1/dataprotectionapplication_types.go +++ b/api/v1alpha1/dataprotectionapplication_types.go @@ -73,6 +73,10 @@ const GCPPluginImageKey UnsupportedImageKey = "gcpPluginImageFqin" const KubeVirtPluginImageKey UnsupportedImageKey = "kubevirtPluginImageFqin" const HypershiftPluginImageKey UnsupportedImageKey = "hypershiftPluginImageFqin" const NonAdminControllerImageKey UnsupportedImageKey = "nonAdminControllerImageFqin" +const VMFileRestoreControllerImageKey UnsupportedImageKey = "vmFileRestoreControllerImageFqin" +const VMFileRestoreAccessImageKey UnsupportedImageKey = "vmFileRestoreAccessImageFqin" +const VMFileRestoreSSHImageKey UnsupportedImageKey = "vmFileRestoreSSHImageFqin" +const VMFileRestoreBrowserImageKey UnsupportedImageKey = "vmFileRestoreBrowserImageFqin" const OperatorTypeKey UnsupportedImageKey = "operator-type" const OperatorTypeMTC = "mtc" @@ -728,6 +732,18 @@ type NonAdmin struct { BackupSyncPeriod *metav1.Duration `json:"backupSyncPeriod,omitempty"` } +// VMFileRestore defines VM file restore feature configuration +type VMFileRestore struct { + // Enable flag to deploy VM file restore controller + // By default is disabled + // +optional + Enable *bool `json:"enable,omitempty"` + + // Resource requirements for the VM file restore controller + // +optional + Resources *corev1.ResourceRequirements `json:"resources,omitempty"` +} + // DataMover defines the various config for DPA data mover type DataMover struct { // enable flag is used to specify whether you want to deploy the volume snapshot mover controller @@ -840,6 +856,10 @@ type DataProtectionApplicationSpec struct { // - kubevirtPluginImageFqin // - hypershiftPluginImageFqin // - nonAdminControllerImageFqin + // - vmFileRestoreControllerImageFqin + // - vmFileRestoreAccessImageFqin + // - vmFileRestoreSSHImageFqin + // - vmFileRestoreBrowserImageFqin // - operator-type // - tech-preview-ack // +optional @@ -873,6 +893,9 @@ type DataProtectionApplicationSpec struct { // nonAdmin defines the configuration for the DPA to enable backup and restore operations for non-admin users // +optional NonAdmin *NonAdmin `json:"nonAdmin,omitempty"` + // vmFileRestore defines the configuration for the DPA to enable VM file restore feature + // +optional + VMFileRestore *VMFileRestore `json:"vmFileRestore,omitempty"` // The format for log output. Valid values are text, json. (default text) // +kubebuilder:validation:Enum=text;json // +kubebuilder:default=text diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 8b88717200..c614e51c92 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -464,6 +464,11 @@ func (in *DataProtectionApplicationSpec) DeepCopyInto(out *DataProtectionApplica *out = new(NonAdmin) (*in).DeepCopyInto(*out) } + if in.VMFileRestore != nil { + in, out := &in.VMFileRestore, &out.VMFileRestore + *out = new(VMFileRestore) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DataProtectionApplicationSpec. @@ -1259,6 +1264,31 @@ func (in *UploadTestStatus) DeepCopy() *UploadTestStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VMFileRestore) DeepCopyInto(out *VMFileRestore) { + *out = *in + if in.Enable != nil { + in, out := &in.Enable, &out.Enable + *out = new(bool) + **out = **in + } + if in.Resources != nil { + in, out := &in.Resources, &out.Resources + *out = new(corev1.ResourceRequirements) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VMFileRestore. +func (in *VMFileRestore) DeepCopy() *VMFileRestore { + if in == nil { + return nil + } + out := new(VMFileRestore) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *VeleroConfig) DeepCopyInto(out *VeleroConfig) { *out = *in diff --git a/bundle/manifests/oadp-operator.clusterserviceversion.yaml b/bundle/manifests/oadp-operator.clusterserviceversion.yaml index a0e19c2fca..9a402494a6 100644 --- a/bundle/manifests/oadp-operator.clusterserviceversion.yaml +++ b/bundle/manifests/oadp-operator.clusterserviceversion.yaml @@ -688,6 +688,12 @@ spec: displayName: Plugins path: plugins version: v1 + - kind: VirtualMachineBackupsDiscovery + name: virtualmachinebackupsdiscoveries.oadp.openshift.io + version: v1alpha1 + - kind: VirtualMachineFileRestore + name: virtualmachinefilerestores.oadp.openshift.io + version: v1alpha1 - description: VolumeSnapshotLocation is a location where Velero stores volume snapshots. displayName: VolumeSnapshotLocation @@ -1135,6 +1141,14 @@ spec: value: registry.redhat.io/oadp/oadp-mustgather-rhel8:v1.2 - name: RELATED_IMAGE_NON_ADMIN_CONTROLLER value: quay.io/konveyor/oadp-non-admin:latest + - name: RELATED_IMAGE_VM_FILE_RESTORE_CONTROLLER + value: quay.io/konveyor/oadp-vm-file-restore:latest + - name: RELATED_IMAGE_VM_FILE_RESTORE_ACCESS + value: quay.io/konveyor/oadp-vmfr-access:latest + - name: RELATED_IMAGE_VM_FILE_RESTORE_SSH + value: quay.io/konveyor/oadp-vmfr-access-sshd:latest + - name: RELATED_IMAGE_VM_FILE_RESTORE_BROWSER + value: quay.io/konveyor/oadp-vmfr-access-filebrowser:latest image: quay.io/konveyor/oadp-operator:latest imagePullPolicy: Always livenessProbe: @@ -1297,4 +1311,12 @@ spec: name: mustgather - image: quay.io/konveyor/oadp-non-admin:latest name: non-admin-controller + - image: quay.io/konveyor/oadp-vm-file-restore:latest + name: vm-file-restore-controller + - image: quay.io/konveyor/oadp-vmfr-access:latest + name: vm-file-restore-access + - image: quay.io/konveyor/oadp-vmfr-access-sshd:latest + name: vm-file-restore-ssh + - image: quay.io/konveyor/oadp-vmfr-access-filebrowser:latest + name: vm-file-restore-browser version: 99.0.0 diff --git a/bundle/manifests/oadp.openshift.io_dataprotectionapplications.yaml b/bundle/manifests/oadp.openshift.io_dataprotectionapplications.yaml index 9a57440e1e..a74be011ff 100644 --- a/bundle/manifests/oadp.openshift.io_dataprotectionapplications.yaml +++ b/bundle/manifests/oadp.openshift.io_dataprotectionapplications.yaml @@ -2669,9 +2669,81 @@ spec: - kubevirtPluginImageFqin - hypershiftPluginImageFqin - nonAdminControllerImageFqin + - vmFileRestoreControllerImageFqin + - vmFileRestoreAccessImageFqin + - vmFileRestoreSSHImageFqin + - vmFileRestoreBrowserImageFqin - operator-type - tech-preview-ack type: object + vmFileRestore: + description: vmFileRestore defines the configuration for the DPA to enable VM file restore feature + properties: + enable: + description: |- + Enable flag to deploy VM file restore controller + By default is disabled + type: boolean + resources: + description: Resource requirements for the VM file restore controller + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + + This is an alpha field and requires enabling the + DynamicResourceAllocation feature gate. + + This field is immutable. It can only be set for containers. + items: + description: ResourceClaim references one entry in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. It makes that resource available + inside a container. + type: string + request: + description: |- + Request is the name chosen for a request in the referenced claim. + If empty, everything from the claim is made available, otherwise + only the result of this request. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Requests describes the minimum amount of compute resources required. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object + type: object required: - configuration type: object diff --git a/bundle/manifests/oadp.openshift.io_virtualmachinebackupsdiscoveries.yaml b/bundle/manifests/oadp.openshift.io_virtualmachinebackupsdiscoveries.yaml new file mode 100644 index 0000000000..718a621e70 --- /dev/null +++ b/bundle/manifests/oadp.openshift.io_virtualmachinebackupsdiscoveries.yaml @@ -0,0 +1,417 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.18.0 + creationTimestamp: null + name: virtualmachinebackupsdiscoveries.oadp.openshift.io +spec: + group: oadp.openshift.io + names: + kind: VirtualMachineBackupsDiscovery + listKind: VirtualMachineBackupsDiscoveryList + plural: virtualmachinebackupsdiscoveries + shortNames: + - vmbd + singular: virtualmachinebackupsdiscovery + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .spec.virtualMachineName + name: VM + type: string + - jsonPath: .spec.virtualMachineNamespace + name: VMNS + type: string + - jsonPath: .status.phase + name: Phase + type: string + - jsonPath: .status.discoveryStats.completed + name: Valid + type: integer + - jsonPath: .status.discoveryStats.skipped + name: Invalid + type: integer + name: v1alpha1 + schema: + openAPIV3Schema: + description: VirtualMachineBackupsDiscovery is the Schema for the virtualmachinebackupsdiscoveries + 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: spec defines the desired state of VirtualMachineBackupsDiscovery + properties: + endTime: + description: |- + Only include backups created before this time (optional time range filtering). + Supports both date-only (YYYY-MM-DD) and full RFC3339 (YYYY-MM-DDTHH:MM:SSZ) formats. + Date-only format defaults to end of day (23:59:59Z). + type: string + requestedBackups: + description: |- + Specific backup names to include in addition to any time-based filtering. + If specified, these backups will be included even if they fall outside the time range. + items: + type: string + type: array + startTime: + description: |- + Only include backups created after this time (optional time range filtering). + Supports both date-only (YYYY-MM-DD) and full RFC3339 (YYYY-MM-DDTHH:MM:SSZ) formats. + Date-only format defaults to start of day (00:00:00Z). + type: string + virtualMachineName: + description: Name of the VirtualMachine to discover backups for. + minLength: 1 + type: string + virtualMachineNamespace: + description: Namespace where the VirtualMachine is located. + minLength: 1 + type: string + required: + - virtualMachineName + - virtualMachineNamespace + type: object + status: + description: status defines the observed state of VirtualMachineBackupsDiscovery + properties: + backupDiscoveryProgress: + description: Detailed discovery progress for each candidate backup. + items: + description: BackupDiscoveryProgress contains detailed information + about backup discovery progress + properties: + createdAt: + description: |- + When the backup was taken (from backup.status.completionTimestamp). + For synced backups, this reflects when the backup actually completed, not when it was imported. + format: date-time + type: string + lastUpdated: + description: When this backup's discovery status was last updated. + format: date-time + type: string + message: + description: Human-readable message about the discovery status. + maxLength: 1024 + type: string + name: + description: Name of the backup resource. + type: string + namespace: + description: Namespace is the namespace of the backup resource + type: string + pvcs: + description: |- + PVCs contains the list of PVCs available in this backup + For a given VM + This field is populated during file restore processing + items: + description: |- + PVCInfo represents a PVC from a backup and all restores associated with it. + The combination of PVCUID + PVCName ensures uniqueness across multiple backups. + properties: + pvcName: + description: Name of the PVC at the time of the backup + type: string + pvcNamespace: + description: Namespace of the PVC at the time of the backup + type: string + pvcUID: + description: UID of the PVC at the time of the backup + type: string + size: + description: Size of the PVC in human-readable format + (e.g., "5Gi", "30Gi") + type: string + required: + - pvcName + - pvcNamespace + - pvcUID + type: object + type: array + status: + description: Current status of backup discovery for this backup. + enum: + - New + - InProgress + - Completed + - Skipped + - Failed + type: string + required: + - name + - namespace + - status + type: object + type: array + conditions: + description: |- + Conditions represent the current state of the VirtualMachineBackupsDiscovery resource. + This is the PRIMARY source of truth for resource state. + Each condition has a unique type and reflects the status of a specific aspect of the resource. + + Standard condition types for this resource (defined in types package): + - types.ConditionTypeProgressing: Discovery is actively running + - types.ConditionTypeAvailable: Valid backups are available for use + - types.ConditionTypeDegraded: Partial failures occurred (may still be usable) + - types.ConditionTypeReady: Summary condition (resource is usable) + + The status of each condition is one of True, False, or Unknown. + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + 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. + 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 + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + discoveryStats: + description: Summary statistics about the backup discovery process. + properties: + completed: + minimum: 0 + type: integer + completionTime: + format: date-time + type: string + failed: + minimum: 0 + type: integer + inProgress: + minimum: 0 + type: integer + pending: + minimum: 0 + type: integer + skipped: + minimum: 0 + type: integer + startTime: + format: date-time + type: string + totalCandidates: + minimum: 0 + type: integer + required: + - completed + - failed + - inProgress + - pending + - skipped + - totalCandidates + type: object + invalidBackups: + description: Requested backups that don't contain the VM (only populated + when RequestedBackups is used). + items: + description: InvalidBackupInfo contains information about a backup + that doesn't contain the VM + properties: + createdAt: + description: |- + When the backup was taken (from backup.status.completionTimestamp). + For synced backups, this reflects when the backup actually completed, not when it was imported. + format: date-time + type: string + name: + description: Name of the backup resource. + type: string + namespace: + description: Namespace is the namespace of the backup resource + type: string + pvcs: + description: |- + PVCs contains the list of PVCs available in this backup + For a given VM + This field is populated during file restore processing + items: + description: |- + PVCInfo represents a PVC from a backup and all restores associated with it. + The combination of PVCUID + PVCName ensures uniqueness across multiple backups. + properties: + pvcName: + description: Name of the PVC at the time of the backup + type: string + pvcNamespace: + description: Namespace of the PVC at the time of the backup + type: string + pvcUID: + description: UID of the PVC at the time of the backup + type: string + size: + description: Size of the PVC in human-readable format + (e.g., "5Gi", "30Gi") + type: string + required: + - pvcName + - pvcNamespace + - pvcUID + type: object + type: array + reason: + description: Reason why this backup doesn't contain the VM or + couldn't be processed. + maxLength: 1024 + type: string + required: + - name + - namespace + type: object + type: array + observedGeneration: + description: |- + ObservedGeneration represents the .metadata.generation that the status was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.observedGeneration is 9, + the status is out of date with respect to the current state of the instance. + + IMPORTANT: Controllers must set this at the START of reconciliation, not at the end. + This prevents race conditions where clients see updated conditions but stale observedGeneration. + format: int64 + type: integer + phase: + description: |- + Phase indicates the overall phase of the backup discovery operation. + Derived from conditions for human readability. Matches Velero's phase model. + Automation should rely on conditions, not phase. + enum: + - New + - InProgress + - Completed + - PartiallyFailed + - Failed + type: string + validBackups: + description: Backups that contain the specified virtual machine and + are ready for file serving. + items: + description: VeleroBackupInfo contains information about a discovered + backup + properties: + createdAt: + description: |- + When the backup was taken (from backup.status.completionTimestamp). + For synced backups, this reflects when the backup actually completed, not when it was imported. + format: date-time + type: string + name: + description: Name of the backup resource. + type: string + namespace: + description: Namespace is the namespace of the backup resource + type: string + pvcs: + description: |- + PVCs contains the list of PVCs available in this backup + For a given VM + This field is populated during file restore processing + items: + description: |- + PVCInfo represents a PVC from a backup and all restores associated with it. + The combination of PVCUID + PVCName ensures uniqueness across multiple backups. + properties: + pvcName: + description: Name of the PVC at the time of the backup + type: string + pvcNamespace: + description: Namespace of the PVC at the time of the backup + type: string + pvcUID: + description: UID of the PVC at the time of the backup + type: string + size: + description: Size of the PVC in human-readable format + (e.g., "5Gi", "30Gi") + type: string + required: + - pvcName + - pvcNamespace + - pvcUID + type: object + type: array + required: + - name + - namespace + type: object + type: array + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: null + storedVersions: null diff --git a/bundle/manifests/oadp.openshift.io_virtualmachinefilerestores.yaml b/bundle/manifests/oadp.openshift.io_virtualmachinefilerestores.yaml new file mode 100644 index 0000000000..2f346583f7 --- /dev/null +++ b/bundle/manifests/oadp.openshift.io_virtualmachinefilerestores.yaml @@ -0,0 +1,444 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.18.0 + creationTimestamp: null + name: virtualmachinefilerestores.oadp.openshift.io +spec: + group: oadp.openshift.io + names: + kind: VirtualMachineFileRestore + listKind: VirtualMachineFileRestoreList + plural: virtualmachinefilerestores + shortNames: + - vmfr + singular: virtualmachinefilerestore + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .status.phase + name: Phase + type: string + - jsonPath: .spec.backupsDiscoveryRef + name: Discovery + type: string + - jsonPath: .status.fileServingInfo.podName + name: Pod + type: string + name: v1alpha1 + schema: + openAPIV3Schema: + description: VirtualMachineFileRestore is the Schema for the virtualmachinefilerestores + 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: spec defines the desired state of VirtualMachineFileRestore + properties: + backupsDiscoveryRef: + description: |- + Reference to the VirtualMachineBackupsDiscovery resource in the same namespace + that contains the discovered backups to serve files from. + minLength: 1 + type: string + fileAccess: + description: |- + FileAccess defines which file access methods are enabled for this restore. + If not specified, defaults to HTTP file browser only. + properties: + fileBrowser: + description: |- + FileBrowser enables HTTPS web-based file browser access + If present (non-nil), FileBrowser access is enabled + properties: + credentialsSecretRef: + description: |- + CredentialsSecretRef references a Secret containing FileBrowser credentials. + The Secret must have a "password" key and optionally a "username" key. + If "username" is not provided in the Secret, it defaults to "oadp". + If CredentialsSecretRef is not specified, the controller generates both + username (defaults to "oadp") and password, storing them in a Secret + in the temporary restore namespace. + properties: + name: + description: Name of the Secret + minLength: 1 + type: string + namespace: + description: |- + Namespace where the Secret is located. + Defaults to the OADP namespace when not specified. + Note: Secrets outside TemporaryRestoreNamespace are automatically + copied to that namespace for mounting in the serving pod. + type: string + required: + - name + type: object + exposeExternally: + description: |- + ExposeExternally enables creation of an OpenShift Route for the FileBrowser service. + When enabled, creates a Route with reencrypt TLS termination for external HTTPS access. + Only effective on OpenShift clusters. + type: boolean + type: object + ssh: + description: |- + SSH provides read-only access to restored files via chrooted OpenSSH. + Supports SFTP, SCP, and rsync for file transfer only (no interactive shell access). + The SSH server runs in a chroot environment for security isolation. + properties: + credentialsSecretRef: + description: |- + CredentialsSecretRef references a Secret containing SSH authentication credentials. + The Secret must have an "authorized_keys" key for SSH key-based authentication. + The "username" key is optional and defaults to "oadp" if not provided. + Note: Only SSH key-based authentication is supported; password authentication is not available. + Takes precedence over inline Username and PublicKey fields. + properties: + name: + description: Name of the Secret + minLength: 1 + type: string + namespace: + description: |- + Namespace where the Secret is located. + Defaults to the OADP namespace when not specified. + Note: Secrets outside TemporaryRestoreNamespace are automatically + copied to that namespace for mounting in the serving pod. + type: string + required: + - name + type: object + publicKey: + description: |- + PublicKey for SSH key-based authentication + Public keys are not sensitive and can be specified inline + If both PublicKey and CredentialsSecretRef are empty, controller generates keypair + type: string + username: + description: |- + Username for SSH access + Defaults to "oadp" if not specified + type: string + type: object + type: object + x-kubernetes-validations: + - message: At least one of ssh or fileBrowser must be specified + rule: has(self.ssh) || has(self.fileBrowser) + namespacePrefix: + description: |- + NamespacePrefix specifies a prefix for automatically generated temporary namespaces. + Only used when RestoreNamespace is not specified. + If not specified, the generated namespace name will use the VM's namespace-name format. + The final namespace name will be: --- + type: string + restoreNamespace: + description: |- + RestoreNamespace specifies an existing namespace where file serving resources will be created. + If not specified, a temporary namespace will be created automatically. + The namespace must exist and be accessible to the controller. + type: string + selectedBackups: + description: |- + Specific backup names to serve files from, selected from the discovery results. + If not specified, all valid backups from the discovery will be used for file serving. + All specified backup names must exist in the ValidBackups list of the referenced discovery. + items: + type: string + type: array + required: + - backupsDiscoveryRef + type: object + status: + description: status defines the observed state of VirtualMachineFileRestore + properties: + conditions: + description: |- + Conditions represent the current state of the VirtualMachineFileRestore resource. + This is the PRIMARY source of truth for resource state. + Each condition has a unique type and reflects the status of a specific aspect of the resource. + + Standard condition types for this resource (defined in types package): + - types.ConditionTypeProgressing: Restore is actively running + - types.ConditionTypeAvailable: File serving resources are ready and accessible + - types.ConditionTypeDegraded: Partial failures occurred (may still be usable) + - types.ConditionTypeReady: Summary condition (resource is usable) + + The status of each condition is one of True, False, or Unknown. + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + 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. + 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 + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + createdNamespace: + description: |- + CreatedNamespace contains information about the namespace used for file serving. + This will be set to the specified RestoreNamespace or the name of the auto-generated temporary namespace. + type: string + fileServingInfo: + description: Information about the file serving resources that have + been created. + properties: + fileBrowser: + description: FileBrowser contains HTTPS file browser access information, + if enabled. + properties: + clusterAccess: + description: |- + ClusterAccess provides the internal HTTPS URL, usable from within the cluster network. + Example: "https://vmfr-browser.restore-tmp.svc.cluster.local" + type: string + credentialsSecretRef: + description: |- + CredentialsSecretRef references a Secret containing login credentials for the file browser: + - "username" + - "password" + If not specified, the controller creates and manages this Secret automatically. + properties: + name: + description: Name of the Secret + minLength: 1 + type: string + namespace: + description: |- + Namespace where the Secret is located. + Defaults to the OADP namespace when not specified. + Note: Secrets outside TemporaryRestoreNamespace are automatically + copied to that namespace for mounting in the serving pod. + type: string + required: + - name + type: object + publicAccess: + description: |- + PublicAccess provides the external HTTPS URL, if exposed via Route or Ingress. + Example: "https://restore-files.apps.example.com" + type: string + type: object + ssh: + description: SSH contains SSH/SFTP/SCP/rsync access information, + if enabled. + properties: + clusterAccess: + description: |- + ClusterAccess provides the internal SSH endpoint, accessible within the cluster + or from environments connected to the cluster network (e.g. via VPN, oc port-forward). + SSH is only exposed within the cluster for security reasons. + Use 'oc port-forward' or 'kubectl port-forward' for external access. + Example: "ssh://vmfr-ssh.restore-tmp.svc.cluster.local:22" + type: string + credentialsSecretRef: + description: |- + CredentialsSecretRef references a Secret containing SSH connection details: + - "username" + - "authorized_keys" + - optionally "privateKey" + The Secret is created or referenced by the controller. + properties: + name: + description: Name of the Secret + minLength: 1 + type: string + namespace: + description: |- + Namespace where the Secret is located. + Defaults to the OADP namespace when not specified. + Note: Secrets outside TemporaryRestoreNamespace are automatically + copied to that namespace for mounting in the serving pod. + type: string + required: + - name + type: object + type: object + type: object + observedGeneration: + description: |- + ObservedGeneration is the most recent generation observed by the controller. + IMPORTANT: Controllers must set this at the START of reconciliation, not at the end. + This prevents race conditions where clients see updated conditions but stale observedGeneration. + format: int64 + type: integer + phase: + description: |- + Phase indicates the overall phase of the file restore operation. + Derived from conditions for human readability. Matches Velero's phase model. + Automation should rely on conditions, not phase. + enum: + - New + - InProgress + - Completed + - PartiallyFailed + - Failed + - Deleting + type: string + pvcRestores: + description: |- + PVCRestores contains PVC-grouped restore information showing which backups each PVC was restored from. + This provides a user-friendly view of the restoration data organized by PVC. + items: + description: |- + PVCRestoreInfo combines PVC metadata with restores. + PVC metadata is inlined for simplicity in JSON output. + properties: + pvcName: + description: Name of the PVC at the time of the backup + type: string + pvcNamespace: + description: Namespace of the PVC at the time of the backup + type: string + pvcUID: + description: UID of the PVC at the time of the backup + type: string + restores: + description: Restores contains all backup restores for this + PVC + items: + description: RestoreInfo contains information about a specific + restore of a PVC from a backup. + properties: + completedAt: + description: When the Velero Restore completed + format: date-time + type: string + createdAt: + description: When the Velero Restore object was created + format: date-time + type: string + failureReason: + description: Reason for failure if the restore failed + type: string + phase: + description: Phase of the Velero Restore object + enum: + - New + - FailedValidation + - InProgress + - WaitingForPluginOperations + - WaitingForPluginOperationsPartiallyFailed + - Completed + - PartiallyFailed + - Failed + - Finalizing + - FinalizingPartiallyFailed + type: string + state: + description: |- + State indicates the compatibility and processing state of this backup + Values: "available", "backup-deleted", "backup-missing", "unsupported-plugin", "extraction-failed", "processing", "failed" + type: string + timestamp: + description: Timestamp indicates when the backup was created + format: date-time + type: string + veleroBackupName: + description: Name of the backup this restore came from + type: string + veleroBackupNamespace: + description: Namespace of the backup this restore came + from + type: string + veleroRestoreName: + description: Name of the Velero Restore object + type: string + veleroRestoreNamespace: + description: Namespace of the Velero Restore object + type: string + required: + - veleroBackupName + - veleroBackupNamespace + type: object + type: array + size: + description: Size of the PVC in human-readable format (e.g., + "5Gi", "30Gi") + type: string + required: + - pvcName + - pvcNamespace + - pvcUID + type: object + type: array + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: null + storedVersions: null diff --git a/config/crd/bases/oadp.openshift.io_dataprotectionapplications.yaml b/config/crd/bases/oadp.openshift.io_dataprotectionapplications.yaml index 4e761b3091..26a2ffc872 100644 --- a/config/crd/bases/oadp.openshift.io_dataprotectionapplications.yaml +++ b/config/crd/bases/oadp.openshift.io_dataprotectionapplications.yaml @@ -2669,9 +2669,81 @@ spec: - kubevirtPluginImageFqin - hypershiftPluginImageFqin - nonAdminControllerImageFqin + - vmFileRestoreControllerImageFqin + - vmFileRestoreAccessImageFqin + - vmFileRestoreSSHImageFqin + - vmFileRestoreBrowserImageFqin - operator-type - tech-preview-ack type: object + vmFileRestore: + description: vmFileRestore defines the configuration for the DPA to enable VM file restore feature + properties: + enable: + description: |- + Enable flag to deploy VM file restore controller + By default is disabled + type: boolean + resources: + description: Resource requirements for the VM file restore controller + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + + This is an alpha field and requires enabling the + DynamicResourceAllocation feature gate. + + This field is immutable. It can only be set for containers. + items: + description: ResourceClaim references one entry in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. It makes that resource available + inside a container. + type: string + request: + description: |- + Request is the name chosen for a request in the referenced claim. + If empty, everything from the claim is made available, otherwise + only the result of this request. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Requests describes the minimum amount of compute resources required. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object + type: object required: - configuration type: object diff --git a/config/crd/bases/oadp.openshift.io_virtualmachinebackupsdiscoveries.yaml b/config/crd/bases/oadp.openshift.io_virtualmachinebackupsdiscoveries.yaml new file mode 100644 index 0000000000..87a7a91c89 --- /dev/null +++ b/config/crd/bases/oadp.openshift.io_virtualmachinebackupsdiscoveries.yaml @@ -0,0 +1,411 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.18.0 + name: virtualmachinebackupsdiscoveries.oadp.openshift.io +spec: + group: oadp.openshift.io + names: + kind: VirtualMachineBackupsDiscovery + listKind: VirtualMachineBackupsDiscoveryList + plural: virtualmachinebackupsdiscoveries + shortNames: + - vmbd + singular: virtualmachinebackupsdiscovery + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .spec.virtualMachineName + name: VM + type: string + - jsonPath: .spec.virtualMachineNamespace + name: VMNS + type: string + - jsonPath: .status.phase + name: Phase + type: string + - jsonPath: .status.discoveryStats.completed + name: Valid + type: integer + - jsonPath: .status.discoveryStats.skipped + name: Invalid + type: integer + name: v1alpha1 + schema: + openAPIV3Schema: + description: VirtualMachineBackupsDiscovery is the Schema for the virtualmachinebackupsdiscoveries + 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: spec defines the desired state of VirtualMachineBackupsDiscovery + properties: + endTime: + description: |- + Only include backups created before this time (optional time range filtering). + Supports both date-only (YYYY-MM-DD) and full RFC3339 (YYYY-MM-DDTHH:MM:SSZ) formats. + Date-only format defaults to end of day (23:59:59Z). + type: string + requestedBackups: + description: |- + Specific backup names to include in addition to any time-based filtering. + If specified, these backups will be included even if they fall outside the time range. + items: + type: string + type: array + startTime: + description: |- + Only include backups created after this time (optional time range filtering). + Supports both date-only (YYYY-MM-DD) and full RFC3339 (YYYY-MM-DDTHH:MM:SSZ) formats. + Date-only format defaults to start of day (00:00:00Z). + type: string + virtualMachineName: + description: Name of the VirtualMachine to discover backups for. + minLength: 1 + type: string + virtualMachineNamespace: + description: Namespace where the VirtualMachine is located. + minLength: 1 + type: string + required: + - virtualMachineName + - virtualMachineNamespace + type: object + status: + description: status defines the observed state of VirtualMachineBackupsDiscovery + properties: + backupDiscoveryProgress: + description: Detailed discovery progress for each candidate backup. + items: + description: BackupDiscoveryProgress contains detailed information + about backup discovery progress + properties: + createdAt: + description: |- + When the backup was taken (from backup.status.completionTimestamp). + For synced backups, this reflects when the backup actually completed, not when it was imported. + format: date-time + type: string + lastUpdated: + description: When this backup's discovery status was last updated. + format: date-time + type: string + message: + description: Human-readable message about the discovery status. + maxLength: 1024 + type: string + name: + description: Name of the backup resource. + type: string + namespace: + description: Namespace is the namespace of the backup resource + type: string + pvcs: + description: |- + PVCs contains the list of PVCs available in this backup + For a given VM + This field is populated during file restore processing + items: + description: |- + PVCInfo represents a PVC from a backup and all restores associated with it. + The combination of PVCUID + PVCName ensures uniqueness across multiple backups. + properties: + pvcName: + description: Name of the PVC at the time of the backup + type: string + pvcNamespace: + description: Namespace of the PVC at the time of the backup + type: string + pvcUID: + description: UID of the PVC at the time of the backup + type: string + size: + description: Size of the PVC in human-readable format + (e.g., "5Gi", "30Gi") + type: string + required: + - pvcName + - pvcNamespace + - pvcUID + type: object + type: array + status: + description: Current status of backup discovery for this backup. + enum: + - New + - InProgress + - Completed + - Skipped + - Failed + type: string + required: + - name + - namespace + - status + type: object + type: array + conditions: + description: |- + Conditions represent the current state of the VirtualMachineBackupsDiscovery resource. + This is the PRIMARY source of truth for resource state. + Each condition has a unique type and reflects the status of a specific aspect of the resource. + + Standard condition types for this resource (defined in types package): + - types.ConditionTypeProgressing: Discovery is actively running + - types.ConditionTypeAvailable: Valid backups are available for use + - types.ConditionTypeDegraded: Partial failures occurred (may still be usable) + - types.ConditionTypeReady: Summary condition (resource is usable) + + The status of each condition is one of True, False, or Unknown. + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + 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. + 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 + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + discoveryStats: + description: Summary statistics about the backup discovery process. + properties: + completed: + minimum: 0 + type: integer + completionTime: + format: date-time + type: string + failed: + minimum: 0 + type: integer + inProgress: + minimum: 0 + type: integer + pending: + minimum: 0 + type: integer + skipped: + minimum: 0 + type: integer + startTime: + format: date-time + type: string + totalCandidates: + minimum: 0 + type: integer + required: + - completed + - failed + - inProgress + - pending + - skipped + - totalCandidates + type: object + invalidBackups: + description: Requested backups that don't contain the VM (only populated + when RequestedBackups is used). + items: + description: InvalidBackupInfo contains information about a backup + that doesn't contain the VM + properties: + createdAt: + description: |- + When the backup was taken (from backup.status.completionTimestamp). + For synced backups, this reflects when the backup actually completed, not when it was imported. + format: date-time + type: string + name: + description: Name of the backup resource. + type: string + namespace: + description: Namespace is the namespace of the backup resource + type: string + pvcs: + description: |- + PVCs contains the list of PVCs available in this backup + For a given VM + This field is populated during file restore processing + items: + description: |- + PVCInfo represents a PVC from a backup and all restores associated with it. + The combination of PVCUID + PVCName ensures uniqueness across multiple backups. + properties: + pvcName: + description: Name of the PVC at the time of the backup + type: string + pvcNamespace: + description: Namespace of the PVC at the time of the backup + type: string + pvcUID: + description: UID of the PVC at the time of the backup + type: string + size: + description: Size of the PVC in human-readable format + (e.g., "5Gi", "30Gi") + type: string + required: + - pvcName + - pvcNamespace + - pvcUID + type: object + type: array + reason: + description: Reason why this backup doesn't contain the VM or + couldn't be processed. + maxLength: 1024 + type: string + required: + - name + - namespace + type: object + type: array + observedGeneration: + description: |- + ObservedGeneration represents the .metadata.generation that the status was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.observedGeneration is 9, + the status is out of date with respect to the current state of the instance. + + IMPORTANT: Controllers must set this at the START of reconciliation, not at the end. + This prevents race conditions where clients see updated conditions but stale observedGeneration. + format: int64 + type: integer + phase: + description: |- + Phase indicates the overall phase of the backup discovery operation. + Derived from conditions for human readability. Matches Velero's phase model. + Automation should rely on conditions, not phase. + enum: + - New + - InProgress + - Completed + - PartiallyFailed + - Failed + type: string + validBackups: + description: Backups that contain the specified virtual machine and + are ready for file serving. + items: + description: VeleroBackupInfo contains information about a discovered + backup + properties: + createdAt: + description: |- + When the backup was taken (from backup.status.completionTimestamp). + For synced backups, this reflects when the backup actually completed, not when it was imported. + format: date-time + type: string + name: + description: Name of the backup resource. + type: string + namespace: + description: Namespace is the namespace of the backup resource + type: string + pvcs: + description: |- + PVCs contains the list of PVCs available in this backup + For a given VM + This field is populated during file restore processing + items: + description: |- + PVCInfo represents a PVC from a backup and all restores associated with it. + The combination of PVCUID + PVCName ensures uniqueness across multiple backups. + properties: + pvcName: + description: Name of the PVC at the time of the backup + type: string + pvcNamespace: + description: Namespace of the PVC at the time of the backup + type: string + pvcUID: + description: UID of the PVC at the time of the backup + type: string + size: + description: Size of the PVC in human-readable format + (e.g., "5Gi", "30Gi") + type: string + required: + - pvcName + - pvcNamespace + - pvcUID + type: object + type: array + required: + - name + - namespace + type: object + type: array + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/bases/oadp.openshift.io_virtualmachinefilerestores.yaml b/config/crd/bases/oadp.openshift.io_virtualmachinefilerestores.yaml new file mode 100644 index 0000000000..5c2df75140 --- /dev/null +++ b/config/crd/bases/oadp.openshift.io_virtualmachinefilerestores.yaml @@ -0,0 +1,438 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.18.0 + name: virtualmachinefilerestores.oadp.openshift.io +spec: + group: oadp.openshift.io + names: + kind: VirtualMachineFileRestore + listKind: VirtualMachineFileRestoreList + plural: virtualmachinefilerestores + shortNames: + - vmfr + singular: virtualmachinefilerestore + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .status.phase + name: Phase + type: string + - jsonPath: .spec.backupsDiscoveryRef + name: Discovery + type: string + - jsonPath: .status.fileServingInfo.podName + name: Pod + type: string + name: v1alpha1 + schema: + openAPIV3Schema: + description: VirtualMachineFileRestore is the Schema for the virtualmachinefilerestores + 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: spec defines the desired state of VirtualMachineFileRestore + properties: + backupsDiscoveryRef: + description: |- + Reference to the VirtualMachineBackupsDiscovery resource in the same namespace + that contains the discovered backups to serve files from. + minLength: 1 + type: string + fileAccess: + description: |- + FileAccess defines which file access methods are enabled for this restore. + If not specified, defaults to HTTP file browser only. + properties: + fileBrowser: + description: |- + FileBrowser enables HTTPS web-based file browser access + If present (non-nil), FileBrowser access is enabled + properties: + credentialsSecretRef: + description: |- + CredentialsSecretRef references a Secret containing FileBrowser credentials. + The Secret must have a "password" key and optionally a "username" key. + If "username" is not provided in the Secret, it defaults to "oadp". + If CredentialsSecretRef is not specified, the controller generates both + username (defaults to "oadp") and password, storing them in a Secret + in the temporary restore namespace. + properties: + name: + description: Name of the Secret + minLength: 1 + type: string + namespace: + description: |- + Namespace where the Secret is located. + Defaults to the OADP namespace when not specified. + Note: Secrets outside TemporaryRestoreNamespace are automatically + copied to that namespace for mounting in the serving pod. + type: string + required: + - name + type: object + exposeExternally: + description: |- + ExposeExternally enables creation of an OpenShift Route for the FileBrowser service. + When enabled, creates a Route with reencrypt TLS termination for external HTTPS access. + Only effective on OpenShift clusters. + type: boolean + type: object + ssh: + description: |- + SSH provides read-only access to restored files via chrooted OpenSSH. + Supports SFTP, SCP, and rsync for file transfer only (no interactive shell access). + The SSH server runs in a chroot environment for security isolation. + properties: + credentialsSecretRef: + description: |- + CredentialsSecretRef references a Secret containing SSH authentication credentials. + The Secret must have an "authorized_keys" key for SSH key-based authentication. + The "username" key is optional and defaults to "oadp" if not provided. + Note: Only SSH key-based authentication is supported; password authentication is not available. + Takes precedence over inline Username and PublicKey fields. + properties: + name: + description: Name of the Secret + minLength: 1 + type: string + namespace: + description: |- + Namespace where the Secret is located. + Defaults to the OADP namespace when not specified. + Note: Secrets outside TemporaryRestoreNamespace are automatically + copied to that namespace for mounting in the serving pod. + type: string + required: + - name + type: object + publicKey: + description: |- + PublicKey for SSH key-based authentication + Public keys are not sensitive and can be specified inline + If both PublicKey and CredentialsSecretRef are empty, controller generates keypair + type: string + username: + description: |- + Username for SSH access + Defaults to "oadp" if not specified + type: string + type: object + type: object + x-kubernetes-validations: + - message: At least one of ssh or fileBrowser must be specified + rule: has(self.ssh) || has(self.fileBrowser) + namespacePrefix: + description: |- + NamespacePrefix specifies a prefix for automatically generated temporary namespaces. + Only used when RestoreNamespace is not specified. + If not specified, the generated namespace name will use the VM's namespace-name format. + The final namespace name will be: --- + type: string + restoreNamespace: + description: |- + RestoreNamespace specifies an existing namespace where file serving resources will be created. + If not specified, a temporary namespace will be created automatically. + The namespace must exist and be accessible to the controller. + type: string + selectedBackups: + description: |- + Specific backup names to serve files from, selected from the discovery results. + If not specified, all valid backups from the discovery will be used for file serving. + All specified backup names must exist in the ValidBackups list of the referenced discovery. + items: + type: string + type: array + required: + - backupsDiscoveryRef + type: object + status: + description: status defines the observed state of VirtualMachineFileRestore + properties: + conditions: + description: |- + Conditions represent the current state of the VirtualMachineFileRestore resource. + This is the PRIMARY source of truth for resource state. + Each condition has a unique type and reflects the status of a specific aspect of the resource. + + Standard condition types for this resource (defined in types package): + - types.ConditionTypeProgressing: Restore is actively running + - types.ConditionTypeAvailable: File serving resources are ready and accessible + - types.ConditionTypeDegraded: Partial failures occurred (may still be usable) + - types.ConditionTypeReady: Summary condition (resource is usable) + + The status of each condition is one of True, False, or Unknown. + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + 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. + 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 + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + createdNamespace: + description: |- + CreatedNamespace contains information about the namespace used for file serving. + This will be set to the specified RestoreNamespace or the name of the auto-generated temporary namespace. + type: string + fileServingInfo: + description: Information about the file serving resources that have + been created. + properties: + fileBrowser: + description: FileBrowser contains HTTPS file browser access information, + if enabled. + properties: + clusterAccess: + description: |- + ClusterAccess provides the internal HTTPS URL, usable from within the cluster network. + Example: "https://vmfr-browser.restore-tmp.svc.cluster.local" + type: string + credentialsSecretRef: + description: |- + CredentialsSecretRef references a Secret containing login credentials for the file browser: + - "username" + - "password" + If not specified, the controller creates and manages this Secret automatically. + properties: + name: + description: Name of the Secret + minLength: 1 + type: string + namespace: + description: |- + Namespace where the Secret is located. + Defaults to the OADP namespace when not specified. + Note: Secrets outside TemporaryRestoreNamespace are automatically + copied to that namespace for mounting in the serving pod. + type: string + required: + - name + type: object + publicAccess: + description: |- + PublicAccess provides the external HTTPS URL, if exposed via Route or Ingress. + Example: "https://restore-files.apps.example.com" + type: string + type: object + ssh: + description: SSH contains SSH/SFTP/SCP/rsync access information, + if enabled. + properties: + clusterAccess: + description: |- + ClusterAccess provides the internal SSH endpoint, accessible within the cluster + or from environments connected to the cluster network (e.g. via VPN, oc port-forward). + SSH is only exposed within the cluster for security reasons. + Use 'oc port-forward' or 'kubectl port-forward' for external access. + Example: "ssh://vmfr-ssh.restore-tmp.svc.cluster.local:22" + type: string + credentialsSecretRef: + description: |- + CredentialsSecretRef references a Secret containing SSH connection details: + - "username" + - "authorized_keys" + - optionally "privateKey" + The Secret is created or referenced by the controller. + properties: + name: + description: Name of the Secret + minLength: 1 + type: string + namespace: + description: |- + Namespace where the Secret is located. + Defaults to the OADP namespace when not specified. + Note: Secrets outside TemporaryRestoreNamespace are automatically + copied to that namespace for mounting in the serving pod. + type: string + required: + - name + type: object + type: object + type: object + observedGeneration: + description: |- + ObservedGeneration is the most recent generation observed by the controller. + IMPORTANT: Controllers must set this at the START of reconciliation, not at the end. + This prevents race conditions where clients see updated conditions but stale observedGeneration. + format: int64 + type: integer + phase: + description: |- + Phase indicates the overall phase of the file restore operation. + Derived from conditions for human readability. Matches Velero's phase model. + Automation should rely on conditions, not phase. + enum: + - New + - InProgress + - Completed + - PartiallyFailed + - Failed + - Deleting + type: string + pvcRestores: + description: |- + PVCRestores contains PVC-grouped restore information showing which backups each PVC was restored from. + This provides a user-friendly view of the restoration data organized by PVC. + items: + description: |- + PVCRestoreInfo combines PVC metadata with restores. + PVC metadata is inlined for simplicity in JSON output. + properties: + pvcName: + description: Name of the PVC at the time of the backup + type: string + pvcNamespace: + description: Namespace of the PVC at the time of the backup + type: string + pvcUID: + description: UID of the PVC at the time of the backup + type: string + restores: + description: Restores contains all backup restores for this + PVC + items: + description: RestoreInfo contains information about a specific + restore of a PVC from a backup. + properties: + completedAt: + description: When the Velero Restore completed + format: date-time + type: string + createdAt: + description: When the Velero Restore object was created + format: date-time + type: string + failureReason: + description: Reason for failure if the restore failed + type: string + phase: + description: Phase of the Velero Restore object + enum: + - New + - FailedValidation + - InProgress + - WaitingForPluginOperations + - WaitingForPluginOperationsPartiallyFailed + - Completed + - PartiallyFailed + - Failed + - Finalizing + - FinalizingPartiallyFailed + type: string + state: + description: |- + State indicates the compatibility and processing state of this backup + Values: "available", "backup-deleted", "backup-missing", "unsupported-plugin", "extraction-failed", "processing", "failed" + type: string + timestamp: + description: Timestamp indicates when the backup was created + format: date-time + type: string + veleroBackupName: + description: Name of the backup this restore came from + type: string + veleroBackupNamespace: + description: Namespace of the backup this restore came + from + type: string + veleroRestoreName: + description: Name of the Velero Restore object + type: string + veleroRestoreNamespace: + description: Namespace of the Velero Restore object + type: string + required: + - veleroBackupName + - veleroBackupNamespace + type: object + type: array + size: + description: Size of the PVC in human-readable format (e.g., + "5Gi", "30Gi") + type: string + required: + - pvcName + - pvcNamespace + - pvcUID + type: object + type: array + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index 7194143c21..f867d09cd4 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -22,6 +22,8 @@ resources: - bases/oadp.openshift.io_nonadminbackups.yaml - bases/oadp.openshift.io_nonadminrestores.yaml - bases/oadp.openshift.io_nonadmindownloadrequests.yaml +- bases/oadp.openshift.io_virtualmachinebackupsdiscoveries.yaml +- bases/oadp.openshift.io_virtualmachinefilerestores.yaml - bases/oadp.openshift.io_dataprotectiontests.yaml #+kubebuilder:scaffold:crdkustomizeresource diff --git a/config/manager/manager.yaml b/config/manager/manager.yaml index 1304283022..866b4464c0 100644 --- a/config/manager/manager.yaml +++ b/config/manager/manager.yaml @@ -88,6 +88,14 @@ spec: value: registry.redhat.io/oadp/oadp-mustgather-rhel8:v1.2 - name: RELATED_IMAGE_NON_ADMIN_CONTROLLER value: quay.io/konveyor/oadp-non-admin:latest + - name: RELATED_IMAGE_VM_FILE_RESTORE_CONTROLLER + value: quay.io/konveyor/oadp-vm-file-restore:latest + - name: RELATED_IMAGE_VM_FILE_RESTORE_ACCESS + value: quay.io/konveyor/oadp-vmfr-access:latest + - name: RELATED_IMAGE_VM_FILE_RESTORE_SSH + value: quay.io/konveyor/oadp-vmfr-access-sshd:latest + - name: RELATED_IMAGE_VM_FILE_RESTORE_BROWSER + value: quay.io/konveyor/oadp-vmfr-access-filebrowser:latest args: - --leader-elect image: controller:latest diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index fbd9cb5985..34509819d7 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -35,6 +35,12 @@ rules: - patch - update - watch +- apiGroups: + - '*' + resources: + - '*' + verbs: + - '*' - apiGroups: - apps resources: @@ -93,6 +99,7 @@ rules: - oadp.openshift.io resources: - '*' + - buckets - cloudstorages - dataprotectionapplications - dataprotectiontests @@ -107,6 +114,7 @@ rules: - apiGroups: - oadp.openshift.io resources: + - buckets/finalizers - cloudstorages/finalizers - dataprotectionapplications/finalizers - dataprotectiontests/finalizers @@ -115,6 +123,7 @@ rules: - apiGroups: - oadp.openshift.io resources: + - buckets/status - cloudstorages/status - dataprotectionapplications/status - dataprotectiontests/status @@ -154,6 +163,15 @@ rules: - securitycontextconstraints verbs: - use +- apiGroups: + - security.openshift.io + resourceNames: + - privileged + - velero-privileged + resources: + - securitycontextconstraints + verbs: + - use - apiGroups: - snapshot.storage.k8s.io resources: @@ -180,6 +198,10 @@ rules: - velero.io resources: - '*' + - backups + - backupstoragelocations + - restores + - volumesnapshotlocations verbs: - create - delete diff --git a/config/vm-file-restore-controller_rbac/role.yaml b/config/vm-file-restore-controller_rbac/role.yaml new file mode 100644 index 0000000000..9f5a2ff299 --- /dev/null +++ b/config/vm-file-restore-controller_rbac/role.yaml @@ -0,0 +1,134 @@ +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: oadp-vm-file-restore-controller-manager-role +rules: +- apiGroups: + - "" + resources: + - namespaces + - pods + - secrets + - serviceaccounts + - services + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - "" + resources: + - persistentvolumeclaims + verbs: + - get + - list + - watch +- apiGroups: + - apps + resources: + - deployments + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - oadp.openshift.io + resources: + - virtualmachinebackupsdiscoveries + - virtualmachinefilerestores + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - oadp.openshift.io + resources: + - virtualmachinebackupsdiscoveries/finalizers + - virtualmachinefilerestores/finalizers + verbs: + - update +- apiGroups: + - oadp.openshift.io + resources: + - virtualmachinebackupsdiscoveries/status + - virtualmachinefilerestores/status + verbs: + - get + - patch + - update +- apiGroups: + - rbac.authorization.k8s.io + resources: + - rolebindings + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - route.openshift.io + resources: + - routes + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - velero.io + resources: + - backups + verbs: + - get + - list + - watch +- apiGroups: + - velero.io + resources: + - datadownloads + verbs: + - get + - list + - patch + - watch +- apiGroups: + - velero.io + resources: + - downloadrequests + verbs: + - create + - delete + - get + - list + - watch +- apiGroups: + - velero.io + resources: + - restores + verbs: + - create + - delete + - get + - list + - patch + - update + - watch diff --git a/config/vm-file-restore-controller_rbac/role_binding.yaml b/config/vm-file-restore-controller_rbac/role_binding.yaml new file mode 100644 index 0000000000..7244a050fe --- /dev/null +++ b/config/vm-file-restore-controller_rbac/role_binding.yaml @@ -0,0 +1,19 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + labels: + app.kubernetes.io/name: clusterrolebinding + app.kubernetes.io/instance: oadp-vm-file-restore-controller-manager-rolebinding + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: oadp-operator + app.kubernetes.io/part-of: oadp-operator + app.kubernetes.io/managed-by: kustomize + name: oadp-vm-file-restore-controller-manager-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: oadp-vm-file-restore-controller-manager-role +subjects: +- kind: ServiceAccount + name: oadp-vm-file-restore-controller-manager + namespace: system diff --git a/config/vm-file-restore-controller_rbac/service_account.yaml b/config/vm-file-restore-controller_rbac/service_account.yaml new file mode 100644 index 0000000000..7810ec02c4 --- /dev/null +++ b/config/vm-file-restore-controller_rbac/service_account.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + labels: + app.kubernetes.io/name: serviceaccount + app.kubernetes.io/instance: oadp-vm-file-restore-controller-manager-sa + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: oadp-operator + app.kubernetes.io/part-of: oadp-operator + app.kubernetes.io/managed-by: kustomize + name: oadp-vm-file-restore-controller-manager + namespace: system diff --git a/docs/config/vm_file_restore.md b/docs/config/vm_file_restore.md new file mode 100644 index 0000000000..bf6af2fab0 --- /dev/null +++ b/docs/config/vm_file_restore.md @@ -0,0 +1,231 @@ +
+

VM File Restore Configuration

+
+ +### Enable VM File Restore + +The VM File Restore feature allows you to perform granular file-level restore from VM backups without restoring the entire virtual machine. This is particularly useful when you only need to recover specific files or directories from a backed-up VM. + +### Prerequisites + +Before enabling VM File Restore, ensure the following prerequisites are met: + +1. **OpenShift Virtualization**: KubeVirt must be installed and configured in your OpenShift cluster +2. **Required Plugins**: The following Velero plugins must be configured in your DPA: + - `kubevirt` - Provides VM backup and restore capabilities + - `openshift` - Required for OpenShift-specific functionality + +### Basic Configuration + +To enable VM File Restore, add the `vmFileRestore` section to your DataProtectionApplication CR: + +```yaml +apiVersion: oadp.openshift.io/v1alpha1 +kind: DataProtectionApplication +metadata: + name: dpa-sample + namespace: openshift-adp +spec: + configuration: + velero: + defaultPlugins: + - openshift + - aws + - kubevirt # Required for VM file restore + + # Enable VM File Restore + vmFileRestore: + enable: true + + backupLocations: + - name: default + velero: + provider: aws + default: true + objectStorage: + bucket: my-backup-bucket + prefix: velero + config: + region: us-east-1 +``` + +### Custom Resource Configuration + +You can specify custom resource limits and requests for the VM File Restore controller: + +```yaml +apiVersion: oadp.openshift.io/v1alpha1 +kind: DataProtectionApplication +metadata: + name: dpa-sample + namespace: openshift-adp +spec: + configuration: + velero: + defaultPlugins: + - openshift + - aws + - kubevirt + + vmFileRestore: + enable: true + resources: + limits: + cpu: "1" + memory: 256Mi + requests: + cpu: 100m + memory: 128Mi + + backupLocations: + - name: default + velero: + provider: aws + default: true + objectStorage: + bucket: my-backup-bucket + prefix: velero + config: + region: us-east-1 +``` + +**Default Resources** (if not specified): +- CPU Limit: 500m +- Memory Limit: 128Mi +- CPU Request: 10m +- Memory Request: 64Mi + +### Image Overrides + +For disconnected environments or testing, you can override the VM File Restore controller images using `unsupportedOverrides`: + +```yaml +apiVersion: oadp.openshift.io/v1alpha1 +kind: DataProtectionApplication +metadata: + name: dpa-sample + namespace: openshift-adp +spec: + configuration: + velero: + defaultPlugins: + - openshift + - aws + - kubevirt + + vmFileRestore: + enable: true + + unsupportedOverrides: + vmFileRestoreControllerImageFqin: "my-registry.io/oadp-vm-file-restore:v1.0.0" + vmFileRestoreAccessImageFqin: "my-registry.io/oadp-vmfr-access:v1.0.0" + vmFileRestoreSSHImageFqin: "my-registry.io/oadp-vmfr-access-sshd:v1.0.0" + vmFileRestoreBrowserImageFqin: "my-registry.io/oadp-vmfr-access-filebrowser:v1.0.0" + + backupLocations: + - name: default + velero: + provider: aws + default: true + objectStorage: + bucket: my-backup-bucket + prefix: velero + config: + region: us-east-1 +``` + +### Using VM File Restore + +Once enabled, the VM File Restore controller will be deployed in the OADP namespace. You can then use the following Custom Resources: + +#### 1. VirtualMachineBackupsDiscovery (VMBD) + +Discover available VM backups: + +```yaml +apiVersion: oadp.openshift.io/v1alpha1 +kind: VirtualMachineBackupsDiscovery +metadata: + name: my-vm-discovery + namespace: openshift-adp +spec: + backupName: my-vm-backup +``` + +#### 2. VirtualMachineFileRestore (VMFR) + +Restore specific files from a VM backup: + +```yaml +apiVersion: oadp.openshift.io/v1alpha1 +kind: VirtualMachineFileRestore +metadata: + name: my-vm-file-restore + namespace: openshift-adp +spec: + backupName: my-vm-backup + volumeName: my-vm-disk + fileAccessMethod: ssh # or filebrowser +``` + +### File Access Methods + +The VM File Restore feature supports two methods for accessing restored files: + +1. **SSH Access**: Connect to restored files via SSH (requires SSH client) +2. **FileBrowser**: Web-based file browser interface for easy file access + +### Validation Requirements + +When VM File Restore is enabled, OADP validates: + +- ✓ Velero configuration is present +- ✓ kubevirt-velero-plugin is configured in defaultPlugins +- ✓ openshift-velero-plugin is configured in defaultPlugins +- ✓ Only one DPA instance across the cluster has VM File Restore enabled + +### Disabling VM File Restore + +To disable VM File Restore, set `enable` to `false`: + +```yaml +apiVersion: oadp.openshift.io/v1alpha1 +kind: DataProtectionApplication +metadata: + name: dpa-sample + namespace: openshift-adp +spec: + configuration: + velero: + defaultPlugins: + - openshift + - aws + - kubevirt + + vmFileRestore: + enable: false # Disables VM file restore +``` + +The VM File Restore controller deployment will be automatically removed when disabled. + +### Troubleshooting + +**VM File Restore controller not deploying:** +- Check that both `kubevirt` and `openshift` plugins are in defaultPlugins +- Verify that KubeVirt is installed in your cluster +- Check OADP operator logs for validation errors + +**Cannot create VirtualMachineFileRestore resources:** +- Ensure VM File Restore is enabled in the DPA +- Verify the controller deployment is running: `oc get deployment -n openshift-adp oadp-vm-file-restore-controller-manager` +- Check controller logs for errors: `oc logs -n openshift-adp deployment/oadp-vm-file-restore-controller-manager` + +**Multiple DPA error:** +- Only one DPA instance across the entire cluster can have VM File Restore enabled +- Disable VM File Restore in other DPA instances if the error occurs + +### Additional Resources + +- [OpenShift Virtualization Documentation](https://docs.openshift.com/container-platform/latest/virt/about_virt/about-virt.html) +- [KubeVirt Project](https://kubevirt.io/) +- [OADP VM File Restore Repository](https://github.com/migtools/oadp-vm-file-restore) diff --git a/internal/controller/dataprotectionapplication_controller.go b/internal/controller/dataprotectionapplication_controller.go index 72af6b3ef2..abb253fed6 100644 --- a/internal/controller/dataprotectionapplication_controller.go +++ b/internal/controller/dataprotectionapplication_controller.go @@ -111,6 +111,7 @@ func (r *DataProtectionApplicationReconciler) Reconcile(ctx context.Context, req r.ReconcileNodeAgentDaemonset, r.ReconcileVeleroMetricsSVC, r.ReconcileNonAdminController, + r.ReconcileVMFileRestoreController, ) if err != nil { diff --git a/internal/controller/validator.go b/internal/controller/validator.go index 63956b3139..afc2f553d2 100644 --- a/internal/controller/validator.go +++ b/internal/controller/validator.go @@ -305,6 +305,60 @@ func (r *DataProtectionApplicationReconciler) ValidateDataProtectionCR(log logr. } } + // validate VM file restore enable + if r.checkVMFileRestoreEnabled() { + dpaList := &oadpv1alpha1.DataProtectionApplicationList{} + err = r.ClusterWideClient.List(r.Context, dpaList) + if err != nil { + return false, err + } + for _, dpa := range dpaList.Items { + if dpa.Namespace != r.NamespacedName.Namespace && (&DataProtectionApplicationReconciler{dpa: &dpa}).checkVMFileRestoreEnabled() { + vmFileRestoreDeployment := &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: vmFileRestoreObjectName, + Namespace: dpa.Namespace, + }, + } + if err := r.ClusterWideClient.Get( + r.Context, + types.NamespacedName{ + Name: vmFileRestoreDeployment.Name, + Namespace: vmFileRestoreDeployment.Namespace, + }, + vmFileRestoreDeployment, + ); err == nil { + return false, fmt.Errorf("only a single instance of VM File Restore Controller can be installed across the entire cluster. VM File Restore controller is already configured and installed in %s namespace", dpa.Namespace) + } + } + } + + // Check if Velero is configured + if r.dpa.Spec.Configuration == nil || r.dpa.Spec.Configuration.Velero == nil { + return false, errors.New("Velero must be configured to enable VM file restore") + } + + // Check if required plugins are configured + hasKubevirtPlugin := false + hasOpenShiftPlugin := false + if r.dpa.Spec.Configuration.Velero.DefaultPlugins != nil { + for _, plugin := range r.dpa.Spec.Configuration.Velero.DefaultPlugins { + if plugin == oadpv1alpha1.DefaultPluginKubeVirt { + hasKubevirtPlugin = true + } + if plugin == oadpv1alpha1.DefaultPluginOpenShift { + hasOpenShiftPlugin = true + } + } + } + if !hasKubevirtPlugin { + return false, errors.New("VM file restore requires kubevirt-velero-plugin. Please add 'kubevirt' to spec.configuration.velero.defaultPlugins") + } + if !hasOpenShiftPlugin { + return false, errors.New("VM file restore requires openshift-velero-plugin. Please add 'openshift' to spec.configuration.velero.defaultPlugins") + } + } + return true, nil } diff --git a/internal/controller/validator_test.go b/internal/controller/validator_test.go index 67a74da5b8..816f09bb91 100644 --- a/internal/controller/validator_test.go +++ b/internal/controller/validator_test.go @@ -2836,3 +2836,290 @@ func (t *testLogSink) Error(err error, msg string, keysAndValues ...interface{}) } func (t *testLogSink) WithValues(keysAndValues ...interface{}) logr.LogSink { return t } func (t *testLogSink) WithName(name string) logr.LogSink { return t } + +func TestVMFileRestoreValidation(t *testing.T) { + tests := []struct { + name string + namespace string + dpa *oadpv1alpha1.DataProtectionApplication + clusterWideDPAs []client.Object + wantErr bool + errorContains string + }{ + { + name: "VM file restore disabled - no validation", + namespace: "test-ns", + dpa: &oadpv1alpha1.DataProtectionApplication{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-dpa", + Namespace: "test-ns", + }, + Spec: oadpv1alpha1.DataProtectionApplicationSpec{ + Configuration: &oadpv1alpha1.ApplicationConfig{ + Velero: &oadpv1alpha1.VeleroConfig{ + NoDefaultBackupLocation: true, + }, + }, + VMFileRestore: &oadpv1alpha1.VMFileRestore{ + Enable: ptr.To(false), + }, + BackupImages: ptr.To(false), + }, + }, + wantErr: false, + }, + { + name: "VM file restore enabled but Velero not configured - error", + namespace: "test-ns", + dpa: &oadpv1alpha1.DataProtectionApplication{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-dpa", + Namespace: "test-ns", + }, + Spec: oadpv1alpha1.DataProtectionApplicationSpec{ + VMFileRestore: &oadpv1alpha1.VMFileRestore{ + Enable: ptr.To(true), + }, + }, + }, + wantErr: true, + errorContains: "DPA CR Velero configuration cannot be nil", + }, + { + name: "VM file restore enabled but kubevirt plugin missing - error", + namespace: "test-ns", + dpa: &oadpv1alpha1.DataProtectionApplication{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-dpa", + Namespace: "test-ns", + }, + Spec: oadpv1alpha1.DataProtectionApplicationSpec{ + Configuration: &oadpv1alpha1.ApplicationConfig{ + Velero: &oadpv1alpha1.VeleroConfig{ + DefaultPlugins: []oadpv1alpha1.DefaultPlugin{ + oadpv1alpha1.DefaultPluginOpenShift, + }, + NoDefaultBackupLocation: true, + }, + }, + VMFileRestore: &oadpv1alpha1.VMFileRestore{ + Enable: ptr.To(true), + }, + BackupImages: ptr.To(false), + }, + }, + wantErr: true, + errorContains: "VM file restore requires kubevirt-velero-plugin", + }, + { + name: "VM file restore enabled but openshift plugin missing - error", + namespace: "test-ns", + dpa: &oadpv1alpha1.DataProtectionApplication{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-dpa", + Namespace: "test-ns", + }, + Spec: oadpv1alpha1.DataProtectionApplicationSpec{ + Configuration: &oadpv1alpha1.ApplicationConfig{ + Velero: &oadpv1alpha1.VeleroConfig{ + DefaultPlugins: []oadpv1alpha1.DefaultPlugin{ + oadpv1alpha1.DefaultPluginKubeVirt, + }, + NoDefaultBackupLocation: true, + }, + }, + VMFileRestore: &oadpv1alpha1.VMFileRestore{ + Enable: ptr.To(true), + }, + BackupImages: ptr.To(false), + }, + }, + wantErr: true, + errorContains: "VM file restore requires openshift-velero-plugin", + }, + { + name: "VM file restore enabled with all requirements - success", + namespace: "test-ns", + dpa: &oadpv1alpha1.DataProtectionApplication{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-dpa", + Namespace: "test-ns", + }, + Spec: oadpv1alpha1.DataProtectionApplicationSpec{ + Configuration: &oadpv1alpha1.ApplicationConfig{ + Velero: &oadpv1alpha1.VeleroConfig{ + DefaultPlugins: []oadpv1alpha1.DefaultPlugin{ + oadpv1alpha1.DefaultPluginKubeVirt, + oadpv1alpha1.DefaultPluginOpenShift, + }, + NoDefaultBackupLocation: true, + }, + }, + VMFileRestore: &oadpv1alpha1.VMFileRestore{ + Enable: ptr.To(true), + }, + BackupImages: ptr.To(false), + }, + }, + wantErr: false, + }, + { + name: "Multiple DPAs with VM file restore in different namespaces - error", + namespace: "test-ns-1", + dpa: &oadpv1alpha1.DataProtectionApplication{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-dpa-1", + Namespace: "test-ns-1", + }, + Spec: oadpv1alpha1.DataProtectionApplicationSpec{ + Configuration: &oadpv1alpha1.ApplicationConfig{ + Velero: &oadpv1alpha1.VeleroConfig{ + DefaultPlugins: []oadpv1alpha1.DefaultPlugin{ + oadpv1alpha1.DefaultPluginKubeVirt, + oadpv1alpha1.DefaultPluginOpenShift, + }, + NoDefaultBackupLocation: true, + }, + }, + VMFileRestore: &oadpv1alpha1.VMFileRestore{ + Enable: ptr.To(true), + }, + BackupImages: ptr.To(false), + }, + }, + clusterWideDPAs: []client.Object{ + &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: vmFileRestoreObjectName, + Namespace: "test-ns-2", + }, + }, + &oadpv1alpha1.DataProtectionApplication{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-dpa-2", + Namespace: "test-ns-2", + }, + Spec: oadpv1alpha1.DataProtectionApplicationSpec{ + Configuration: &oadpv1alpha1.ApplicationConfig{ + Velero: &oadpv1alpha1.VeleroConfig{ + DefaultPlugins: []oadpv1alpha1.DefaultPlugin{ + oadpv1alpha1.DefaultPluginKubeVirt, + oadpv1alpha1.DefaultPluginOpenShift, + }, + NoDefaultBackupLocation: true, + }, + }, + VMFileRestore: &oadpv1alpha1.VMFileRestore{ + Enable: ptr.To(true), + }, + BackupImages: ptr.To(false), + }, + }, + }, + wantErr: true, + errorContains: "only a single instance of VM File Restore Controller can be installed across the entire cluster", + }, + { + name: "VM file restore enabled with custom resources - success", + namespace: "test-ns", + dpa: &oadpv1alpha1.DataProtectionApplication{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-dpa", + Namespace: "test-ns", + }, + Spec: oadpv1alpha1.DataProtectionApplicationSpec{ + Configuration: &oadpv1alpha1.ApplicationConfig{ + Velero: &oadpv1alpha1.VeleroConfig{ + DefaultPlugins: []oadpv1alpha1.DefaultPlugin{ + oadpv1alpha1.DefaultPluginKubeVirt, + oadpv1alpha1.DefaultPluginOpenShift, + }, + NoDefaultBackupLocation: true, + }, + }, + VMFileRestore: &oadpv1alpha1.VMFileRestore{ + Enable: ptr.To(true), + Resources: &corev1.ResourceRequirements{ + Limits: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("1"), + corev1.ResourceMemory: resource.MustParse("256Mi"), + }, + Requests: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("100m"), + corev1.ResourceMemory: resource.MustParse("128Mi"), + }, + }, + }, + BackupImages: ptr.To(false), + }, + }, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Setup fake client + objects := []client.Object{tt.dpa} + if tt.clusterWideDPAs != nil { + objects = append(objects, tt.clusterWideDPAs...) + } + + fakeClient, err := getFakeClientFromObjects(objects...) + if err != nil { + t.Errorf("error in creating fake client, likely programmer error: %v", err) + return + } + + r := &DataProtectionApplicationReconciler{ + Client: fakeClient, + ClusterWideClient: fakeClient, + Scheme: fakeClient.Scheme(), + Context: newContextForTest(), + NamespacedName: types.NamespacedName{ + Namespace: tt.namespace, + Name: tt.dpa.Name, + }, + dpa: tt.dpa, + EventRecorder: record.NewFakeRecorder(10), + } + + // Run validation + valid, err := r.ValidateDataProtectionCR(logr.Discard()) + + if tt.wantErr { + if err == nil { + t.Errorf("ValidateDataProtectionCR() expected error but got none") + return + } + if tt.errorContains != "" && !contains(err.Error(), tt.errorContains) { + t.Errorf("ValidateDataProtectionCR() error = %v, want error containing %v", err, tt.errorContains) + } + if valid { + t.Errorf("ValidateDataProtectionCR() = %v, want false when error", valid) + } + } else { + if err != nil { + t.Errorf("ValidateDataProtectionCR() unexpected error = %v", err) + return + } + if !valid { + t.Errorf("ValidateDataProtectionCR() = %v, want true", valid) + } + } + }) + } +} + +func contains(s, substr string) bool { + return len(s) >= len(substr) && (s == substr || len(substr) == 0 || (len(s) > 0 && len(substr) > 0 && containsHelper(s, substr))) +} + +func containsHelper(s, substr string) bool { + for i := 0; i <= len(s)-len(substr); i++ { + if s[i:i+len(substr)] == substr { + return true + } + } + return false +} diff --git a/internal/controller/vmfilerestore_controller.go b/internal/controller/vmfilerestore_controller.go new file mode 100644 index 0000000000..8af36f946c --- /dev/null +++ b/internal/controller/vmfilerestore_controller.go @@ -0,0 +1,411 @@ +package controller + +import ( + "fmt" + "os" + "reflect" + "strconv" + + "github.com/go-logr/logr" + "github.com/sirupsen/logrus" + "golang.org/x/exp/maps" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + k8serror "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/intstr" + "k8s.io/utils/ptr" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + + oadpv1alpha1 "github.com/openshift/oadp-operator/api/v1alpha1" + "github.com/openshift/oadp-operator/pkg/common" +) + +const ( + vmFileRestoreObjectName = "oadp-vm-file-restore-controller-manager" + vmFileRestoreControlPlaneKey = "control-plane" + vmFileRestoreControlPlaneValue = "oadp-vm-file-restore-controller" + vmfrDpaResourceVersionAnnotation = oadpv1alpha1.OadpOperatorLabel + "-vmfr-dpa-resource-version" +) + +var ( + vmFileRestoreControlPlaneLabel = map[string]string{ + vmFileRestoreControlPlaneKey: vmFileRestoreControlPlaneValue, + } + vmFileRestoreDeploymentLabels = map[string]string{ + "app.kubernetes.io/component": "manager", + "app.kubernetes.io/created-by": common.OADPOperator, + "app.kubernetes.io/instance": vmFileRestoreObjectName, + "app.kubernetes.io/managed-by": "kustomize", + "app.kubernetes.io/name": "deployment", + "app.kubernetes.io/part-of": common.OADPOperator, + } + + vmfrDpaResourceVersion = "" + previousVMFileRestoreConfiguration *oadpv1alpha1.VMFileRestore = nil +) + +// ReconcileVMFileRestoreController manages the VM file restore controller deployment +func (r *DataProtectionApplicationReconciler) ReconcileVMFileRestoreController(log logr.Logger) (bool, error) { + vmFileRestoreDeployment := &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: vmFileRestoreObjectName, + Namespace: r.NamespacedName.Namespace, + }, + } + + // Delete (possible) previously deployment + if !r.checkVMFileRestoreEnabled() { + if err := r.Get( + r.Context, + types.NamespacedName{ + Name: vmFileRestoreDeployment.Name, + Namespace: vmFileRestoreDeployment.Namespace, + }, + vmFileRestoreDeployment, + ); err != nil { + if k8serror.IsNotFound(err) { + return true, nil + } + return false, err + } + + if err := r.Delete( + r.Context, + vmFileRestoreDeployment, + &client.DeleteOptions{PropagationPolicy: ptr.To(metav1.DeletePropagationForeground)}, + ); err != nil { + r.EventRecorder.Event( + vmFileRestoreDeployment, + corev1.EventTypeWarning, + "VMFileRestoreDeploymentDeleteFailed", + fmt.Sprintf("Could not delete VM file restore controller deployment %s/%s: %s", vmFileRestoreDeployment.Namespace, vmFileRestoreDeployment.Name, err), + ) + return false, err + } + r.EventRecorder.Event( + vmFileRestoreDeployment, + corev1.EventTypeNormal, + "VMFileRestoreDeploymentDeleteSucceed", + fmt.Sprintf("VM file restore controller deployment %s/%s deleted", vmFileRestoreDeployment.Namespace, vmFileRestoreDeployment.Name), + ) + return true, nil + } + + operation, err := controllerutil.CreateOrUpdate( + r.Context, + r.Client, + vmFileRestoreDeployment, + func() error { + err := r.buildVMFileRestoreDeployment(vmFileRestoreDeployment) + if err != nil { + return err + } + + // Setting controller owner reference on the VM file restore controller deployment + return controllerutil.SetControllerReference(r.dpa, vmFileRestoreDeployment, r.Scheme) + }, + ) + if err != nil { + return false, err + } + + if operation != controllerutil.OperationResultNone { + r.EventRecorder.Event( + vmFileRestoreDeployment, + corev1.EventTypeNormal, + "VMFileRestoreDeploymentReconciled", + fmt.Sprintf("VM file restore controller deployment %s/%s %s", vmFileRestoreDeployment.Namespace, vmFileRestoreDeployment.Name, operation), + ) + } + return true, nil +} + +func (r *DataProtectionApplicationReconciler) buildVMFileRestoreDeployment(deploymentObject *appsv1.Deployment) error { + vmFileRestoreControllerImage := r.getVMFileRestoreControllerImage() + imagePullPolicy, err := common.GetImagePullPolicy(r.dpa.Spec.ImagePullPolicy, vmFileRestoreControllerImage) + if err != nil { + r.Log.Error(err, "imagePullPolicy regex failed") + } + ensureVMFileRestoreRequiredLabels(deploymentObject) + err = ensureVMFileRestoreRequiredSpecs(deploymentObject, r.dpa, vmFileRestoreControllerImage, imagePullPolicy, r) + if err != nil { + return err + } + return nil +} + +func ensureVMFileRestoreRequiredLabels(deploymentObject *appsv1.Deployment) { + maps.Copy(vmFileRestoreDeploymentLabels, vmFileRestoreControlPlaneLabel) + deploymentObjectLabels := deploymentObject.GetLabels() + if deploymentObjectLabels == nil { + deploymentObject.SetLabels(vmFileRestoreDeploymentLabels) + } else { + for key, value := range vmFileRestoreDeploymentLabels { + deploymentObjectLabels[key] = value + } + deploymentObject.SetLabels(deploymentObjectLabels) + } +} + +func ensureVMFileRestoreRequiredSpecs( + deploymentObject *appsv1.Deployment, + dpa *oadpv1alpha1.DataProtectionApplication, + image string, + imagePullPolicy corev1.PullPolicy, + r *DataProtectionApplicationReconciler, +) error { + // Build environment variables + envVars := []corev1.EnvVar{ + { + Name: "WATCH_NAMESPACE", + Value: deploymentObject.Namespace, + }, + { + Name: "VMFR_ACCESS_IMAGE", + Value: r.getVMFileRestoreAccessImage(), + }, + { + Name: "VMFR_SSH_IMAGE", + Value: r.getVMFileRestoreSSHImage(), + }, + { + Name: "VMFR_BROWSER_IMAGE", + Value: r.getVMFileRestoreBrowserImage(), + }, + } + + // Add log level if configured + if dpa.Spec.Configuration != nil && dpa.Spec.Configuration.Velero != nil { + envVars = append(envVars, corev1.EnvVar{ + Name: common.LogLevelEnvVar, + Value: func() string { + level, err := logrus.ParseLevel(dpa.Spec.Configuration.Velero.LogLevel) + if err != nil { + return "" + } + return strconv.FormatUint(uint64(level), 10) + }(), + }) + } + + // Add log format if configured + if len(dpa.Spec.LogFormat) > 0 { + envVars = append(envVars, corev1.EnvVar{ + Name: common.LogFormatEnvVar, + Value: string(dpa.Spec.LogFormat), + }) + } + + // Track DPA resource version for change detection + if len(vmfrDpaResourceVersion) == 0 || + !reflect.DeepEqual(dpa.Spec.VMFileRestore, previousVMFileRestoreConfiguration) { + vmfrDpaResourceVersion = dpa.GetResourceVersion() + previousVMFileRestoreConfiguration = dpa.Spec.VMFileRestore + } + + podAnnotations := map[string]string{ + vmfrDpaResourceVersionAnnotation: vmfrDpaResourceVersion, + } + + // Get resource requirements + resources := r.getVMFileRestoreResources() + + // Set deployment spec + deploymentObject.Spec.Replicas = ptr.To(int32(1)) + deploymentObject.Spec.Selector = &metav1.LabelSelector{ + MatchLabels: vmFileRestoreControlPlaneLabel, + } + + // Set template labels + templateObjectLabels := deploymentObject.Spec.Template.GetLabels() + if templateObjectLabels == nil { + deploymentObject.Spec.Template.SetLabels(vmFileRestoreControlPlaneLabel) + } else { + templateObjectLabels[vmFileRestoreControlPlaneKey] = vmFileRestoreControlPlaneLabel[vmFileRestoreControlPlaneKey] + deploymentObject.Spec.Template.SetLabels(templateObjectLabels) + } + + // Set template annotations + templateObjectAnnotations := deploymentObject.Spec.Template.GetAnnotations() + if templateObjectAnnotations == nil { + deploymentObject.Spec.Template.SetAnnotations(podAnnotations) + } else { + templateObjectAnnotations[vmfrDpaResourceVersionAnnotation] = podAnnotations[vmfrDpaResourceVersionAnnotation] + deploymentObject.Spec.Template.SetAnnotations(templateObjectAnnotations) + } + + // Set pod security context + deploymentObject.Spec.Template.Spec.SecurityContext = &corev1.PodSecurityContext{ + RunAsNonRoot: ptr.To(true), + SeccompProfile: &corev1.SeccompProfile{ + Type: corev1.SeccompProfileTypeRuntimeDefault, + }, + } + + // Build container spec + vmFileRestoreContainerFound := false + containerSpec := corev1.Container{ + Name: "manager", + Image: image, + ImagePullPolicy: imagePullPolicy, + Command: []string{"/manager"}, + Args: []string{ + "--leader-elect", + "--health-probe-bind-address=:8081", + "--metrics-bind-address=:8443", + "--metrics-secure=true", + }, + Env: envVars, + Ports: []corev1.ContainerPort{ + { + Name: "https", + ContainerPort: 8443, + Protocol: corev1.ProtocolTCP, + }, + }, + Resources: resources, + SecurityContext: &corev1.SecurityContext{ + AllowPrivilegeEscalation: ptr.To(false), + Capabilities: &corev1.Capabilities{ + Drop: []corev1.Capability{"ALL"}, + }, + ReadOnlyRootFilesystem: ptr.To(true), + }, + LivenessProbe: &corev1.Probe{ + ProbeHandler: corev1.ProbeHandler{ + HTTPGet: &corev1.HTTPGetAction{ + Path: "/healthz", + Port: intstr.FromInt(8081), + Scheme: corev1.URISchemeHTTP, + }, + }, + InitialDelaySeconds: 15, + PeriodSeconds: 20, + }, + ReadinessProbe: &corev1.Probe{ + ProbeHandler: corev1.ProbeHandler{ + HTTPGet: &corev1.HTTPGetAction{ + Path: "/readyz", + Port: intstr.FromInt(8081), + Scheme: corev1.URISchemeHTTP, + }, + }, + InitialDelaySeconds: 5, + PeriodSeconds: 10, + }, + TerminationMessagePolicy: corev1.TerminationMessageFallbackToLogsOnError, + } + + if len(deploymentObject.Spec.Template.Spec.Containers) == 0 { + deploymentObject.Spec.Template.Spec.Containers = []corev1.Container{containerSpec} + vmFileRestoreContainerFound = true + } else { + for index, container := range deploymentObject.Spec.Template.Spec.Containers { + if container.Name == "manager" { + deploymentObject.Spec.Template.Spec.Containers[index] = containerSpec + vmFileRestoreContainerFound = true + break + } + } + } + + if !vmFileRestoreContainerFound { + return fmt.Errorf("could not find VM file restore container in Deployment") + } + + deploymentObject.Spec.Template.Spec.RestartPolicy = corev1.RestartPolicyAlways + deploymentObject.Spec.Template.Spec.ServiceAccountName = vmFileRestoreObjectName + return nil +} + +func (r *DataProtectionApplicationReconciler) checkVMFileRestoreEnabled() bool { + if r.dpa.Spec.VMFileRestore != nil && r.dpa.Spec.VMFileRestore.Enable != nil { + return *r.dpa.Spec.VMFileRestore.Enable + } + return false +} + +func (r *DataProtectionApplicationReconciler) getVMFileRestoreControllerImage() string { + dpa := r.dpa + unsupportedOverride := dpa.Spec.UnsupportedOverrides[oadpv1alpha1.VMFileRestoreControllerImageKey] + if unsupportedOverride != "" { + return unsupportedOverride + } + + environmentVariable := os.Getenv("RELATED_IMAGE_VM_FILE_RESTORE_CONTROLLER") + if environmentVariable != "" { + return environmentVariable + } + + return "quay.io/konveyor/oadp-vm-file-restore:latest" +} + +func (r *DataProtectionApplicationReconciler) getVMFileRestoreAccessImage() string { + dpa := r.dpa + unsupportedOverride := dpa.Spec.UnsupportedOverrides[oadpv1alpha1.VMFileRestoreAccessImageKey] + if unsupportedOverride != "" { + return unsupportedOverride + } + + environmentVariable := os.Getenv("RELATED_IMAGE_VM_FILE_RESTORE_ACCESS") + if environmentVariable != "" { + return environmentVariable + } + + return "quay.io/konveyor/oadp-vmfr-access:latest" +} + +func (r *DataProtectionApplicationReconciler) getVMFileRestoreSSHImage() string { + dpa := r.dpa + unsupportedOverride := dpa.Spec.UnsupportedOverrides[oadpv1alpha1.VMFileRestoreSSHImageKey] + if unsupportedOverride != "" { + return unsupportedOverride + } + + environmentVariable := os.Getenv("RELATED_IMAGE_VM_FILE_RESTORE_SSH") + if environmentVariable != "" { + return environmentVariable + } + + return "quay.io/konveyor/oadp-vmfr-access-sshd:latest" +} + +func (r *DataProtectionApplicationReconciler) getVMFileRestoreBrowserImage() string { + dpa := r.dpa + unsupportedOverride := dpa.Spec.UnsupportedOverrides[oadpv1alpha1.VMFileRestoreBrowserImageKey] + if unsupportedOverride != "" { + return unsupportedOverride + } + + environmentVariable := os.Getenv("RELATED_IMAGE_VM_FILE_RESTORE_BROWSER") + if environmentVariable != "" { + return environmentVariable + } + + return "quay.io/konveyor/oadp-vmfr-access-filebrowser:latest" +} + +func (r *DataProtectionApplicationReconciler) getVMFileRestoreResources() corev1.ResourceRequirements { + dpa := r.dpa + + // If custom resources specified in DPA, use them + if dpa.Spec.VMFileRestore != nil && dpa.Spec.VMFileRestore.Resources != nil { + return *dpa.Spec.VMFileRestore.Resources + } + + // Default resource requirements (matching upstream oadp-vm-file-restore) + return corev1.ResourceRequirements{ + Limits: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("500m"), + corev1.ResourceMemory: resource.MustParse("128Mi"), + }, + Requests: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("10m"), + corev1.ResourceMemory: resource.MustParse("64Mi"), + }, + } +} diff --git a/internal/controller/vmfilerestore_controller_test.go b/internal/controller/vmfilerestore_controller_test.go new file mode 100644 index 0000000000..3d94ef6c30 --- /dev/null +++ b/internal/controller/vmfilerestore_controller_test.go @@ -0,0 +1,897 @@ +package controller + +import ( + "context" + "os" + "testing" + + "github.com/go-logr/logr" + "github.com/onsi/ginkgo/v2" + "github.com/onsi/gomega" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/tools/record" + "k8s.io/utils/ptr" + + oadpv1alpha1 "github.com/openshift/oadp-operator/api/v1alpha1" +) + +const ( + defaultVMFileRestoreControllerImage = "quay.io/konveyor/oadp-vm-file-restore:latest" + defaultVMFileRestoreAccessImage = "quay.io/konveyor/oadp-vmfr-access:latest" + defaultVMFileRestoreSSHImage = "quay.io/konveyor/oadp-vmfr-access-sshd:latest" + defaultVMFileRestoreBrowserImage = "quay.io/konveyor/oadp-vmfr-access-filebrowser:latest" +) + +type ReconcileVMFileRestoreControllerScenario struct { + namespace string + dpa string + errMessage string + eventWords []string + vmFileRestoreEnabled bool + deployment *appsv1.Deployment +} + +func createTestVMFileRestoreDeployment(namespace string) *appsv1.Deployment { + return &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: vmFileRestoreObjectName, + Namespace: namespace, + Labels: map[string]string{ + "test": "test", + "app.kubernetes.io/name": "wrong", + vmFileRestoreControlPlaneKey: "super-wrong", + }, + }, + Spec: appsv1.DeploymentSpec{ + Replicas: ptr.To(int32(2)), + Selector: &metav1.LabelSelector{ + MatchLabels: vmFileRestoreControlPlaneLabel, + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: vmFileRestoreControlPlaneLabel, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "manager", + Image: "wrong", + }, + }, + ServiceAccountName: "wrong-one", + }, + }, + }, + } +} + +func runReconcileVMFileRestoreControllerTest( + scenario ReconcileVMFileRestoreControllerScenario, + updateTestScenario func(scenario ReconcileVMFileRestoreControllerScenario), + ctx context.Context, + controllerImageEnvValue string, + accessImageEnvValue string, + sshImageEnvValue string, + browserImageEnvValue string, +) { + updateTestScenario(scenario) + + namespace := &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: scenario.namespace, + }, + } + gomega.Expect(k8sClient.Create(ctx, namespace)).To(gomega.Succeed()) + + dpa := &oadpv1alpha1.DataProtectionApplication{ + ObjectMeta: metav1.ObjectMeta{ + Name: scenario.dpa, + Namespace: scenario.namespace, + }, + Spec: oadpv1alpha1.DataProtectionApplicationSpec{ + Configuration: &oadpv1alpha1.ApplicationConfig{ + Velero: &oadpv1alpha1.VeleroConfig{}, + }, + VMFileRestore: &oadpv1alpha1.VMFileRestore{ + Enable: ptr.To(scenario.vmFileRestoreEnabled), + }, + }, + } + gomega.Expect(k8sClient.Create(ctx, dpa)).To(gomega.Succeed()) + + if scenario.deployment != nil { + gomega.Expect(k8sClient.Create(ctx, scenario.deployment)).To(gomega.Succeed()) + } + + os.Setenv("RELATED_IMAGE_VM_FILE_RESTORE_CONTROLLER", controllerImageEnvValue) + os.Setenv("RELATED_IMAGE_VM_FILE_RESTORE_ACCESS", accessImageEnvValue) + os.Setenv("RELATED_IMAGE_VM_FILE_RESTORE_SSH", sshImageEnvValue) + os.Setenv("RELATED_IMAGE_VM_FILE_RESTORE_BROWSER", browserImageEnvValue) + + event := record.NewFakeRecorder(5) + r := &DataProtectionApplicationReconciler{ + Client: k8sClient, + Scheme: testEnv.Scheme, + Context: ctx, + NamespacedName: types.NamespacedName{ + Name: scenario.dpa, + Namespace: scenario.namespace, + }, + EventRecorder: event, + dpa: dpa, + } + result, err := r.ReconcileVMFileRestoreController(logr.Discard()) + + if len(scenario.errMessage) == 0 { + gomega.Expect(result).To(gomega.BeTrue()) + gomega.Expect(err).To(gomega.Not(gomega.HaveOccurred())) + } else { + gomega.Expect(result).To(gomega.BeFalse()) + gomega.Expect(err).To(gomega.HaveOccurred()) + gomega.Expect(err.Error()).To(gomega.ContainSubstring(scenario.errMessage)) + } + + if scenario.eventWords != nil { + gomega.Expect(len(event.Events)).To(gomega.Equal(1)) + message := <-event.Events + for _, word := range scenario.eventWords { + gomega.Expect(message).To(gomega.ContainSubstring(word)) + } + } else { + gomega.Expect(len(event.Events)).To(gomega.Equal(0)) + } +} + +var _ = ginkgo.Describe("Test ReconcileVMFileRestoreController function", func() { + var ( + ctx = context.Background() + currentTestScenario ReconcileVMFileRestoreControllerScenario + updateTestScenario = func(scenario ReconcileVMFileRestoreControllerScenario) { + currentTestScenario = scenario + } + ) + + ginkgo.AfterEach(func() { + os.Unsetenv("RELATED_IMAGE_VM_FILE_RESTORE_CONTROLLER") + os.Unsetenv("RELATED_IMAGE_VM_FILE_RESTORE_ACCESS") + os.Unsetenv("RELATED_IMAGE_VM_FILE_RESTORE_SSH") + os.Unsetenv("RELATED_IMAGE_VM_FILE_RESTORE_BROWSER") + + deployment := &appsv1.Deployment{} + if k8sClient.Get( + ctx, + types.NamespacedName{ + Name: vmFileRestoreObjectName, + Namespace: currentTestScenario.namespace, + }, + deployment, + ) == nil { + gomega.Expect(k8sClient.Delete(ctx, deployment)).To(gomega.Succeed()) + } + + dpa := &oadpv1alpha1.DataProtectionApplication{ + ObjectMeta: metav1.ObjectMeta{ + Name: currentTestScenario.dpa, + Namespace: currentTestScenario.namespace, + }, + } + gomega.Expect(k8sClient.Delete(ctx, dpa)).To(gomega.Succeed()) + + namespace := &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: currentTestScenario.namespace, + }, + } + gomega.Expect(k8sClient.Delete(ctx, namespace)).To(gomega.Succeed()) + }) + + ginkgo.DescribeTable("Reconcile is true", + func(scenario ReconcileVMFileRestoreControllerScenario) { + runReconcileVMFileRestoreControllerTest( + scenario, + updateTestScenario, + ctx, + defaultVMFileRestoreControllerImage, + defaultVMFileRestoreAccessImage, + defaultVMFileRestoreSSHImage, + defaultVMFileRestoreBrowserImage, + ) + }, + ginkgo.Entry("Should create VM file restore deployment", ReconcileVMFileRestoreControllerScenario{ + namespace: "vmfr-test-1", + dpa: "vmfr-test-1-dpa", + eventWords: []string{"Normal", "VMFileRestoreDeploymentReconciled", "created"}, + vmFileRestoreEnabled: true, + }), + ginkgo.Entry("Should update VM file restore deployment", ReconcileVMFileRestoreControllerScenario{ + namespace: "vmfr-test-2", + dpa: "vmfr-test-2-dpa", + eventWords: []string{"Normal", "VMFileRestoreDeploymentReconciled", "updated"}, + vmFileRestoreEnabled: true, + deployment: createTestVMFileRestoreDeployment("vmfr-test-2"), + }), + ginkgo.Entry("Should delete VM file restore deployment", ReconcileVMFileRestoreControllerScenario{ + namespace: "vmfr-test-3", + dpa: "vmfr-test-3-dpa", + eventWords: []string{"Normal", "VMFileRestoreDeploymentDeleteSucceed", "deleted"}, + vmFileRestoreEnabled: false, + deployment: createTestVMFileRestoreDeployment("vmfr-test-3"), + }), + ginkgo.Entry("Should do nothing when disabled", ReconcileVMFileRestoreControllerScenario{ + namespace: "vmfr-test-4", + dpa: "vmfr-test-4-dpa", + vmFileRestoreEnabled: false, + }), + ) + + ginkgo.DescribeTable("Reconcile is false", + func(scenario ReconcileVMFileRestoreControllerScenario) { + runReconcileVMFileRestoreControllerTest( + scenario, + updateTestScenario, + ctx, + defaultVMFileRestoreControllerImage, + defaultVMFileRestoreAccessImage, + defaultVMFileRestoreSSHImage, + defaultVMFileRestoreBrowserImage, + ) + }, + ginkgo.Entry("Should error because manager container was not found in Deployment", ReconcileVMFileRestoreControllerScenario{ + namespace: "vmfr-test-error-1", + dpa: "vmfr-test-error-1-dpa", + errMessage: "could not find VM file restore container in Deployment", + vmFileRestoreEnabled: true, + deployment: &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: vmFileRestoreObjectName, + Namespace: "vmfr-test-error-1", + }, + Spec: appsv1.DeploymentSpec{ + Selector: &metav1.LabelSelector{ + MatchLabels: vmFileRestoreControlPlaneLabel, + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: vmFileRestoreControlPlaneLabel, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{{ + Name: "wrong", + Image: defaultVMFileRestoreControllerImage, + }}, + }, + }, + }, + }, + }), + ) +}) + +func TestGetVMFileRestoreControllerImage(t *testing.T) { + tests := []struct { + name string + envValue string + overrideValue string + expectedResult string + }{ + { + name: "Should return override value when set", + envValue: "env-image:v1", + overrideValue: "override-image:v1", + expectedResult: "override-image:v1", + }, + { + name: "Should return env value when override not set", + envValue: "env-image:v1", + overrideValue: "", + expectedResult: "env-image:v1", + }, + { + name: "Should return default when neither set", + envValue: "", + overrideValue: "", + expectedResult: defaultVMFileRestoreControllerImage, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.envValue != "" { + os.Setenv("RELATED_IMAGE_VM_FILE_RESTORE_CONTROLLER", tt.envValue) + defer os.Unsetenv("RELATED_IMAGE_VM_FILE_RESTORE_CONTROLLER") + } + + dpa := &oadpv1alpha1.DataProtectionApplication{ + Spec: oadpv1alpha1.DataProtectionApplicationSpec{}, + } + + if tt.overrideValue != "" { + dpa.Spec.UnsupportedOverrides = map[oadpv1alpha1.UnsupportedImageKey]string{ + oadpv1alpha1.VMFileRestoreControllerImageKey: tt.overrideValue, + } + } + + r := &DataProtectionApplicationReconciler{dpa: dpa} + result := r.getVMFileRestoreControllerImage() + + if result != tt.expectedResult { + t.Errorf("expected %s, got %s", tt.expectedResult, result) + } + }) + } +} + +func TestGetVMFileRestoreAccessImage(t *testing.T) { + tests := []struct { + name string + envValue string + overrideValue string + expectedResult string + }{ + { + name: "Should return override value when set", + envValue: "env-access-image:v1", + overrideValue: "override-access-image:v1", + expectedResult: "override-access-image:v1", + }, + { + name: "Should return env value when override not set", + envValue: "env-access-image:v1", + overrideValue: "", + expectedResult: "env-access-image:v1", + }, + { + name: "Should return default when neither set", + envValue: "", + overrideValue: "", + expectedResult: defaultVMFileRestoreAccessImage, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.envValue != "" { + os.Setenv("RELATED_IMAGE_VM_FILE_RESTORE_ACCESS", tt.envValue) + defer os.Unsetenv("RELATED_IMAGE_VM_FILE_RESTORE_ACCESS") + } + + dpa := &oadpv1alpha1.DataProtectionApplication{ + Spec: oadpv1alpha1.DataProtectionApplicationSpec{}, + } + + if tt.overrideValue != "" { + dpa.Spec.UnsupportedOverrides = map[oadpv1alpha1.UnsupportedImageKey]string{ + oadpv1alpha1.VMFileRestoreAccessImageKey: tt.overrideValue, + } + } + + r := &DataProtectionApplicationReconciler{dpa: dpa} + result := r.getVMFileRestoreAccessImage() + + if result != tt.expectedResult { + t.Errorf("expected %s, got %s", tt.expectedResult, result) + } + }) + } +} + +func TestGetVMFileRestoreResources(t *testing.T) { + tests := []struct { + name string + customResources *corev1.ResourceRequirements + expectedCPULimit string + expectedMemLimit string + expectedCPURequest string + expectedMemRequest string + }{ + { + name: "Should return default resources when not specified", + customResources: nil, + expectedCPULimit: "500m", + expectedMemLimit: "128Mi", + expectedCPURequest: "10m", + expectedMemRequest: "64Mi", + }, + { + name: "Should return custom resources when specified", + customResources: &corev1.ResourceRequirements{ + Limits: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("1"), + corev1.ResourceMemory: resource.MustParse("256Mi"), + }, + Requests: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("100m"), + corev1.ResourceMemory: resource.MustParse("128Mi"), + }, + }, + expectedCPULimit: "1", + expectedMemLimit: "256Mi", + expectedCPURequest: "100m", + expectedMemRequest: "128Mi", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + dpa := &oadpv1alpha1.DataProtectionApplication{ + Spec: oadpv1alpha1.DataProtectionApplicationSpec{ + VMFileRestore: &oadpv1alpha1.VMFileRestore{}, + }, + } + + if tt.customResources != nil { + dpa.Spec.VMFileRestore.Resources = tt.customResources + } + + r := &DataProtectionApplicationReconciler{dpa: dpa} + result := r.getVMFileRestoreResources() + + if result.Limits.Cpu().String() != tt.expectedCPULimit { + t.Errorf("CPU limit: expected %s, got %s", tt.expectedCPULimit, result.Limits.Cpu().String()) + } + if result.Limits.Memory().String() != tt.expectedMemLimit { + t.Errorf("Memory limit: expected %s, got %s", tt.expectedMemLimit, result.Limits.Memory().String()) + } + if result.Requests.Cpu().String() != tt.expectedCPURequest { + t.Errorf("CPU request: expected %s, got %s", tt.expectedCPURequest, result.Requests.Cpu().String()) + } + if result.Requests.Memory().String() != tt.expectedMemRequest { + t.Errorf("Memory request: expected %s, got %s", tt.expectedMemRequest, result.Requests.Memory().String()) + } + }) + } +} + +func TestGetVMFileRestoreSSHImage(t *testing.T) { + tests := []struct { + name string + envValue string + overrideValue string + expectedResult string + }{ + { + name: "Should return override value when set", + envValue: "env-ssh-image:v1", + overrideValue: "override-ssh-image:v1", + expectedResult: "override-ssh-image:v1", + }, + { + name: "Should return env value when override not set", + envValue: "env-ssh-image:v1", + overrideValue: "", + expectedResult: "env-ssh-image:v1", + }, + { + name: "Should return default when neither set", + envValue: "", + overrideValue: "", + expectedResult: defaultVMFileRestoreSSHImage, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.envValue != "" { + os.Setenv("RELATED_IMAGE_VM_FILE_RESTORE_SSH", tt.envValue) + defer os.Unsetenv("RELATED_IMAGE_VM_FILE_RESTORE_SSH") + } + + dpa := &oadpv1alpha1.DataProtectionApplication{ + Spec: oadpv1alpha1.DataProtectionApplicationSpec{}, + } + + if tt.overrideValue != "" { + dpa.Spec.UnsupportedOverrides = map[oadpv1alpha1.UnsupportedImageKey]string{ + oadpv1alpha1.VMFileRestoreSSHImageKey: tt.overrideValue, + } + } + + r := &DataProtectionApplicationReconciler{dpa: dpa} + result := r.getVMFileRestoreSSHImage() + + if result != tt.expectedResult { + t.Errorf("expected %s, got %s", tt.expectedResult, result) + } + }) + } +} + +func TestGetVMFileRestoreBrowserImage(t *testing.T) { + tests := []struct { + name string + envValue string + overrideValue string + expectedResult string + }{ + { + name: "Should return override value when set", + envValue: "env-browser-image:v1", + overrideValue: "override-browser-image:v1", + expectedResult: "override-browser-image:v1", + }, + { + name: "Should return env value when override not set", + envValue: "env-browser-image:v1", + overrideValue: "", + expectedResult: "env-browser-image:v1", + }, + { + name: "Should return default when neither set", + envValue: "", + overrideValue: "", + expectedResult: defaultVMFileRestoreBrowserImage, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.envValue != "" { + os.Setenv("RELATED_IMAGE_VM_FILE_RESTORE_BROWSER", tt.envValue) + defer os.Unsetenv("RELATED_IMAGE_VM_FILE_RESTORE_BROWSER") + } + + dpa := &oadpv1alpha1.DataProtectionApplication{ + Spec: oadpv1alpha1.DataProtectionApplicationSpec{}, + } + + if tt.overrideValue != "" { + dpa.Spec.UnsupportedOverrides = map[oadpv1alpha1.UnsupportedImageKey]string{ + oadpv1alpha1.VMFileRestoreBrowserImageKey: tt.overrideValue, + } + } + + r := &DataProtectionApplicationReconciler{dpa: dpa} + result := r.getVMFileRestoreBrowserImage() + + if result != tt.expectedResult { + t.Errorf("expected %s, got %s", tt.expectedResult, result) + } + }) + } +} + +func TestCheckVMFileRestoreEnabled(t *testing.T) { + tests := []struct { + name string + vmFileRestore *oadpv1alpha1.VMFileRestore + expectedResult bool + }{ + { + name: "Should return false when VMFileRestore is nil", + vmFileRestore: nil, + expectedResult: false, + }, + { + name: "Should return false when Enable is nil", + vmFileRestore: &oadpv1alpha1.VMFileRestore{ + Enable: nil, + }, + expectedResult: false, + }, + { + name: "Should return false when Enable is false", + vmFileRestore: &oadpv1alpha1.VMFileRestore{ + Enable: ptr.To(false), + }, + expectedResult: false, + }, + { + name: "Should return true when Enable is true", + vmFileRestore: &oadpv1alpha1.VMFileRestore{ + Enable: ptr.To(true), + }, + expectedResult: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + dpa := &oadpv1alpha1.DataProtectionApplication{ + Spec: oadpv1alpha1.DataProtectionApplicationSpec{ + VMFileRestore: tt.vmFileRestore, + }, + } + + r := &DataProtectionApplicationReconciler{dpa: dpa} + result := r.checkVMFileRestoreEnabled() + + if result != tt.expectedResult { + t.Errorf("expected %v, got %v", tt.expectedResult, result) + } + }) + } +} + +func TestEnsureVMFileRestoreRequiredLabels(t *testing.T) { + tests := []struct { + name string + existingLabels map[string]string + expectedLabels map[string]string + }{ + { + name: "Should set labels when deployment has no labels", + existingLabels: nil, + expectedLabels: map[string]string{ + "control-plane": "oadp-vm-file-restore-controller", + "app.kubernetes.io/component": "manager", + "app.kubernetes.io/created-by": "oadp-operator", + "app.kubernetes.io/instance": "oadp-vm-file-restore-controller-manager", + "app.kubernetes.io/managed-by": "kustomize", + "app.kubernetes.io/name": "deployment", + "app.kubernetes.io/part-of": "oadp-operator", + }, + }, + { + name: "Should preserve existing labels and add required labels", + existingLabels: map[string]string{ + "custom-label": "custom-value", + }, + expectedLabels: map[string]string{ + "custom-label": "custom-value", + "control-plane": "oadp-vm-file-restore-controller", + "app.kubernetes.io/component": "manager", + "app.kubernetes.io/created-by": "oadp-operator", + "app.kubernetes.io/instance": "oadp-vm-file-restore-controller-manager", + "app.kubernetes.io/managed-by": "kustomize", + "app.kubernetes.io/name": "deployment", + "app.kubernetes.io/part-of": "oadp-operator", + }, + }, + { + name: "Should overwrite existing required labels with correct values", + existingLabels: map[string]string{ + "control-plane": "wrong-value", + "app.kubernetes.io/component": "wrong-component", + }, + expectedLabels: map[string]string{ + "control-plane": "oadp-vm-file-restore-controller", + "app.kubernetes.io/component": "manager", + "app.kubernetes.io/created-by": "oadp-operator", + "app.kubernetes.io/instance": "oadp-vm-file-restore-controller-manager", + "app.kubernetes.io/managed-by": "kustomize", + "app.kubernetes.io/name": "deployment", + "app.kubernetes.io/part-of": "oadp-operator", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + deployment := &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Labels: tt.existingLabels, + }, + } + + ensureVMFileRestoreRequiredLabels(deployment) + + labels := deployment.GetLabels() + for key, expectedValue := range tt.expectedLabels { + if actualValue, exists := labels[key]; !exists { + t.Errorf("expected label %s to exist", key) + } else if actualValue != expectedValue { + t.Errorf("label %s: expected %s, got %s", key, expectedValue, actualValue) + } + } + + // Verify no unexpected labels were added (beyond expected and existing custom ones) + for key := range labels { + if _, expected := tt.expectedLabels[key]; !expected { + t.Errorf("unexpected label %s in deployment", key) + } + } + }) + } +} + +func TestBuildVMFileRestoreDeployment(t *testing.T) { + tests := []struct { + name string + dpa *oadpv1alpha1.DataProtectionApplication + envVars map[string]string + expectedImage string + expectedEnvCount int + expectedReplicas int32 + expectedSAName string + expectError bool + errorContains string + }{ + { + name: "Should build deployment with default resources and images", + dpa: &oadpv1alpha1.DataProtectionApplication{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-dpa", + Namespace: "test-namespace", + ResourceVersion: "12345", + }, + Spec: oadpv1alpha1.DataProtectionApplicationSpec{ + VMFileRestore: &oadpv1alpha1.VMFileRestore{ + Enable: ptr.To(true), + }, + Configuration: &oadpv1alpha1.ApplicationConfig{ + Velero: &oadpv1alpha1.VeleroConfig{ + LogLevel: "info", + }, + }, + LogFormat: "text", + }, + }, + expectedImage: "quay.io/konveyor/oadp-vm-file-restore:latest", + expectedEnvCount: 6, // WATCH_NAMESPACE, VMFR_ACCESS_IMAGE, VMFR_SSH_IMAGE, VMFR_BROWSER_IMAGE, LOG_LEVEL, LOG_FORMAT + expectedReplicas: 1, + expectedSAName: "oadp-vm-file-restore-controller-manager", + expectError: false, + }, + { + name: "Should use custom resources when specified in DPA", + dpa: &oadpv1alpha1.DataProtectionApplication{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-dpa", + Namespace: "test-namespace", + ResourceVersion: "12345", + }, + Spec: oadpv1alpha1.DataProtectionApplicationSpec{ + VMFileRestore: &oadpv1alpha1.VMFileRestore{ + Enable: ptr.To(true), + Resources: &corev1.ResourceRequirements{ + Limits: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("1"), + corev1.ResourceMemory: resource.MustParse("256Mi"), + }, + Requests: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("100m"), + corev1.ResourceMemory: resource.MustParse("128Mi"), + }, + }, + }, + Configuration: &oadpv1alpha1.ApplicationConfig{ + Velero: &oadpv1alpha1.VeleroConfig{}, + }, + }, + }, + expectedImage: "quay.io/konveyor/oadp-vm-file-restore:latest", + expectedEnvCount: 5, // WATCH_NAMESPACE, VMFR_ACCESS_IMAGE, VMFR_SSH_IMAGE, VMFR_BROWSER_IMAGE, LOG_LEVEL + expectedReplicas: 1, + expectedSAName: "oadp-vm-file-restore-controller-manager", + expectError: false, + }, + { + name: "Should use override images from unsupportedOverrides", + dpa: &oadpv1alpha1.DataProtectionApplication{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-dpa", + Namespace: "test-namespace", + ResourceVersion: "12345", + }, + Spec: oadpv1alpha1.DataProtectionApplicationSpec{ + VMFileRestore: &oadpv1alpha1.VMFileRestore{ + Enable: ptr.To(true), + }, + UnsupportedOverrides: map[oadpv1alpha1.UnsupportedImageKey]string{ + oadpv1alpha1.VMFileRestoreControllerImageKey: "custom-registry.io/vmfr:v1.0", + }, + Configuration: &oadpv1alpha1.ApplicationConfig{ + Velero: &oadpv1alpha1.VeleroConfig{}, + }, + }, + }, + expectedImage: "custom-registry.io/vmfr:v1.0", + expectedEnvCount: 5, // WATCH_NAMESPACE, VMFR_ACCESS_IMAGE, VMFR_SSH_IMAGE, VMFR_BROWSER_IMAGE, LOG_LEVEL + expectedReplicas: 1, + expectedSAName: "oadp-vm-file-restore-controller-manager", + expectError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Set environment variables if provided + for key, value := range tt.envVars { + os.Setenv(key, value) + defer os.Unsetenv(key) + } + + r := &DataProtectionApplicationReconciler{ + dpa: tt.dpa, + } + + deployment := &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: vmFileRestoreObjectName, + Namespace: tt.dpa.Namespace, + }, + } + + err := r.buildVMFileRestoreDeployment(deployment) + + if tt.expectError { + if err == nil { + t.Errorf("expected error but got none") + } else if tt.errorContains != "" && !contains(err.Error(), tt.errorContains) { + t.Errorf("expected error to contain '%s', got: %v", tt.errorContains, err) + } + return + } + + if err != nil { + t.Errorf("unexpected error: %v", err) + return + } + + // Verify deployment spec + if *deployment.Spec.Replicas != tt.expectedReplicas { + t.Errorf("replicas: expected %d, got %d", tt.expectedReplicas, *deployment.Spec.Replicas) + } + + if deployment.Spec.Template.Spec.ServiceAccountName != tt.expectedSAName { + t.Errorf("serviceAccountName: expected %s, got %s", tt.expectedSAName, deployment.Spec.Template.Spec.ServiceAccountName) + } + + // Verify container exists and has correct image + if len(deployment.Spec.Template.Spec.Containers) == 0 { + t.Fatalf("no containers found in deployment") + } + + container := deployment.Spec.Template.Spec.Containers[0] + if container.Name != "manager" { + t.Errorf("container name: expected 'manager', got %s", container.Name) + } + + if container.Image != tt.expectedImage { + t.Errorf("container image: expected %s, got %s", tt.expectedImage, container.Image) + } + + // Verify environment variables + if len(container.Env) != tt.expectedEnvCount { + t.Errorf("env var count: expected %d, got %d", tt.expectedEnvCount, len(container.Env)) + } + + // Verify security context + if deployment.Spec.Template.Spec.SecurityContext.RunAsNonRoot == nil || !*deployment.Spec.Template.Spec.SecurityContext.RunAsNonRoot { + t.Error("expected runAsNonRoot to be true") + } + + if container.SecurityContext.AllowPrivilegeEscalation == nil || *container.SecurityContext.AllowPrivilegeEscalation { + t.Error("expected allowPrivilegeEscalation to be false") + } + + if container.SecurityContext.ReadOnlyRootFilesystem == nil || !*container.SecurityContext.ReadOnlyRootFilesystem { + t.Error("expected readOnlyRootFilesystem to be true") + } + + // Verify probes exist + if container.LivenessProbe == nil { + t.Error("expected liveness probe to be set") + } + + if container.ReadinessProbe == nil { + t.Error("expected readiness probe to be set") + } + + // Verify labels + labels := deployment.GetLabels() + if labels["control-plane"] != "oadp-vm-file-restore-controller" { + t.Errorf("control-plane label: expected 'oadp-vm-file-restore-controller', got %s", labels["control-plane"]) + } + + // Verify pod annotations include DPA resource version + podAnnotations := deployment.Spec.Template.GetAnnotations() + if _, exists := podAnnotations[vmfrDpaResourceVersionAnnotation]; !exists { + t.Error("expected pod annotation with DPA resource version") + } + + // Verify custom resources if specified + if tt.dpa.Spec.VMFileRestore.Resources != nil { + expectedCPULimit := tt.dpa.Spec.VMFileRestore.Resources.Limits.Cpu().String() + actualCPULimit := container.Resources.Limits.Cpu().String() + if actualCPULimit != expectedCPULimit { + t.Errorf("CPU limit: expected %s, got %s", expectedCPULimit, actualCPULimit) + } + } + }) + } +} From a7441f6323442a090e9f59908f345aafd1ce1c6c Mon Sep 17 00:00:00 2001 From: Shubham Pampattiwar Date: Thu, 6 Nov 2025 13:20:28 -0800 Subject: [PATCH 2/6] Fix VM file restore controller deployment and permissions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit fixes issues found during live cluster testing: 1. Add missing RBAC permissions: - events permission for controller to create events - coordination.k8s.io/leases for leader election 2. Fix reconciliation loop in OADP operator: - Only update dynamic container fields (Image, ImagePullPolicy, Env, Resources) - Make PodSecurityContext conditional (set only if nil) - Prevents continuous reconciliation by leaving static fields unchanged 3. Bundle generation fixes: - Add vm-file-restore RBAC kustomization.yaml - Add oadp-vm-file-restore-controller-manager to Makefile BUNDLE_GEN_FLAGS - Reference vm-file-restore RBAC in config/manifests/kustomization.yaml - Clean unwanted RBAC from CSV (buckets, velero-privileged SCC, wildcards) Tested on live cluster - controller now deploys successfully, performs leader election, and no longer causes reconciliation loops. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- Makefile | 2 +- .../oadp-operator.clusterserviceversion.yaml | 157 +++++++++++++++++- config/manifests/kustomization.yaml | 1 + .../kustomization.yaml | 9 + .../vm-file-restore-controller_rbac/role.yaml | 13 ++ .../controller/vmfilerestore_controller.go | 23 ++- 6 files changed, 195 insertions(+), 10 deletions(-) create mode 100644 config/vm-file-restore-controller_rbac/kustomization.yaml diff --git a/Makefile b/Makefile index 2190e3914a..1582637572 100644 --- a/Makefile +++ b/Makefile @@ -47,7 +47,7 @@ IMAGE_TAG_BASE ?= openshift.io/oadp-operator BUNDLE_IMG ?= $(IMAGE_TAG_BASE)-bundle:v$(VERSION) # BUNDLE_GEN_FLAGS are the flags passed to the operator-sdk generate bundle command -BUNDLE_GEN_FLAGS ?= -q --extra-service-accounts "velero,non-admin-controller" --overwrite --version $(VERSION) $(BUNDLE_METADATA_OPTS) +BUNDLE_GEN_FLAGS ?= -q --extra-service-accounts "velero,non-admin-controller,oadp-vm-file-restore-controller-manager" --overwrite --version $(VERSION) $(BUNDLE_METADATA_OPTS) # USE_IMAGE_DIGESTS defines if images are resolved via tags or digests # You can enable this value if you would like to use SHA Based Digests diff --git a/bundle/manifests/oadp-operator.clusterserviceversion.yaml b/bundle/manifests/oadp-operator.clusterserviceversion.yaml index 9a402494a6..35064ce364 100644 --- a/bundle/manifests/oadp-operator.clusterserviceversion.yaml +++ b/bundle/manifests/oadp-operator.clusterserviceversion.yaml @@ -841,6 +841,149 @@ spec: verbs: - get serviceAccountName: non-admin-controller + - rules: + - apiGroups: + - "" + resources: + - events + - namespaces + - pods + - secrets + - serviceaccounts + - services + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - "" + resources: + - persistentvolumeclaims + verbs: + - get + - list + - watch + - apiGroups: + - apps + resources: + - deployments + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - coordination.k8s.io + resources: + - leases + verbs: + - get + - list + - watch + - create + - update + - patch + - delete + - apiGroups: + - oadp.openshift.io + resources: + - virtualmachinebackupsdiscoveries + - virtualmachinefilerestores + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - oadp.openshift.io + resources: + - virtualmachinebackupsdiscoveries/finalizers + - virtualmachinefilerestores/finalizers + verbs: + - update + - apiGroups: + - oadp.openshift.io + resources: + - virtualmachinebackupsdiscoveries/status + - virtualmachinefilerestores/status + verbs: + - get + - patch + - update + - apiGroups: + - rbac.authorization.k8s.io + resources: + - rolebindings + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - route.openshift.io + resources: + - routes + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - velero.io + resources: + - backups + verbs: + - get + - list + - watch + - apiGroups: + - velero.io + resources: + - datadownloads + verbs: + - get + - list + - patch + - watch + - apiGroups: + - velero.io + resources: + - downloadrequests + verbs: + - create + - delete + - get + - list + - watch + - apiGroups: + - velero.io + resources: + - restores + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + serviceAccountName: oadp-vm-file-restore-controller-manager - rules: - apiGroups: - "" @@ -930,7 +1073,6 @@ spec: - apiGroups: - oadp.openshift.io resources: - - '*' - cloudstorages - dataprotectionapplications - dataprotectiontests @@ -992,6 +1134,14 @@ spec: - securitycontextconstraints verbs: - use + - apiGroups: + - security.openshift.io + resourceNames: + - privileged + resources: + - securitycontextconstraints + verbs: + - use - apiGroups: - snapshot.storage.k8s.io resources: @@ -1017,7 +1167,10 @@ spec: - apiGroups: - velero.io resources: - - '*' + - backups + - backupstoragelocations + - restores + - volumesnapshotlocations verbs: - create - delete diff --git a/config/manifests/kustomization.yaml b/config/manifests/kustomization.yaml index 5a625e6754..a0106525ec 100644 --- a/config/manifests/kustomization.yaml +++ b/config/manifests/kustomization.yaml @@ -7,6 +7,7 @@ resources: - ../scorecard - ../velero - ../non-admin-controller_rbac +- ../vm-file-restore-controller_rbac # [WEBHOOK] To enable webhooks, uncomment all the sections with [WEBHOOK] prefix. # Do NOT uncomment sections with prefix [CERTMANAGER], as OLM does not support cert-manager. diff --git a/config/vm-file-restore-controller_rbac/kustomization.yaml b/config/vm-file-restore-controller_rbac/kustomization.yaml new file mode 100644 index 0000000000..55d4d0cc20 --- /dev/null +++ b/config/vm-file-restore-controller_rbac/kustomization.yaml @@ -0,0 +1,9 @@ +resources: +# All RBAC will be applied under this service account in +# the deployment namespace. You may comment out this resource +# if your manager will use a service account that exists at +# runtime. Be sure to update RoleBinding and ClusterRoleBinding +# subjects if changing service account names. +- service_account.yaml +- role.yaml +- role_binding.yaml diff --git a/config/vm-file-restore-controller_rbac/role.yaml b/config/vm-file-restore-controller_rbac/role.yaml index 9f5a2ff299..87b6a929b9 100644 --- a/config/vm-file-restore-controller_rbac/role.yaml +++ b/config/vm-file-restore-controller_rbac/role.yaml @@ -7,6 +7,7 @@ rules: - apiGroups: - "" resources: + - events - namespaces - pods - secrets @@ -40,6 +41,18 @@ rules: - patch - update - watch +- apiGroups: + - coordination.k8s.io + resources: + - leases + verbs: + - get + - list + - watch + - create + - update + - patch + - delete - apiGroups: - oadp.openshift.io resources: diff --git a/internal/controller/vmfilerestore_controller.go b/internal/controller/vmfilerestore_controller.go index 8af36f946c..e6173e3d5d 100644 --- a/internal/controller/vmfilerestore_controller.go +++ b/internal/controller/vmfilerestore_controller.go @@ -238,12 +238,14 @@ func ensureVMFileRestoreRequiredSpecs( deploymentObject.Spec.Template.SetAnnotations(templateObjectAnnotations) } - // Set pod security context - deploymentObject.Spec.Template.Spec.SecurityContext = &corev1.PodSecurityContext{ - RunAsNonRoot: ptr.To(true), - SeccompProfile: &corev1.SeccompProfile{ - Type: corev1.SeccompProfileTypeRuntimeDefault, - }, + // Set pod security context only if not already set (to avoid reconciliation loops) + if deploymentObject.Spec.Template.Spec.SecurityContext == nil { + deploymentObject.Spec.Template.Spec.SecurityContext = &corev1.PodSecurityContext{ + RunAsNonRoot: ptr.To(true), + SeccompProfile: &corev1.SeccompProfile{ + Type: corev1.SeccompProfileTypeRuntimeDefault, + }, + } } // Build container spec @@ -306,7 +308,14 @@ func ensureVMFileRestoreRequiredSpecs( } else { for index, container := range deploymentObject.Spec.Template.Spec.Containers { if container.Name == "manager" { - deploymentObject.Spec.Template.Spec.Containers[index] = containerSpec + // Update only dynamic fields that can change based on DPA configuration + // Static fields (Command, Args, Ports, SecurityContext, Probes) are left as-is + vmFileRestoreContainer := &deploymentObject.Spec.Template.Spec.Containers[index] + vmFileRestoreContainer.Image = image + vmFileRestoreContainer.ImagePullPolicy = imagePullPolicy + vmFileRestoreContainer.Env = envVars + vmFileRestoreContainer.Resources = resources + vmFileRestoreContainer.TerminationMessagePolicy = corev1.TerminationMessageFallbackToLogsOnError vmFileRestoreContainerFound = true break } From d41b9e129defdcf191355f989c0d9292c3c1e0a6 Mon Sep 17 00:00:00 2001 From: Shubham Pampattiwar Date: Mon, 10 Nov 2025 16:03:09 -0800 Subject: [PATCH 3/6] Fix code formatting and clean unwanted RBAC permissions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix import grouping (gci) in validator_test.go, vmfilerestore_controller.go, and vmfilerestore_controller_test.go - Replace custom contains helper with strings.Contains from stdlib - Remove unwanted RBAC from config/rbac/role.yaml: - Full wildcard permissions (apiGroups: *, resources: *, verbs: *) - buckets resources (3 locations) - velero-privileged SCC reference - Duplicate privileged SCC entry - Extra velero.io resource listings - Remove unwanted RBAC from bundle CSV: - Full wildcard from velero ServiceAccount All tests pass. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../oadp-operator.clusterserviceversion.yaml | 14 ---- config/rbac/role.yaml | 22 ------ internal/controller/validator_test.go | 31 +++----- .../controller/vmfilerestore_controller.go | 15 ++-- .../vmfilerestore_controller_test.go | 70 +++++++++---------- 5 files changed, 51 insertions(+), 101 deletions(-) diff --git a/bundle/manifests/oadp-operator.clusterserviceversion.yaml b/bundle/manifests/oadp-operator.clusterserviceversion.yaml index 35064ce364..efdb3b301f 100644 --- a/bundle/manifests/oadp-operator.clusterserviceversion.yaml +++ b/bundle/manifests/oadp-operator.clusterserviceversion.yaml @@ -1214,20 +1214,6 @@ spec: - packagemanifests verbs: - '*' - - apiGroups: - - '*' - resources: - - '*' - verbs: - - get - - watch - - list - - update - - patch - - create - - delete - - assign - - deletecollection - nonResourceURLs: - '*' verbs: diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 34509819d7..fbd9cb5985 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -35,12 +35,6 @@ rules: - patch - update - watch -- apiGroups: - - '*' - resources: - - '*' - verbs: - - '*' - apiGroups: - apps resources: @@ -99,7 +93,6 @@ rules: - oadp.openshift.io resources: - '*' - - buckets - cloudstorages - dataprotectionapplications - dataprotectiontests @@ -114,7 +107,6 @@ rules: - apiGroups: - oadp.openshift.io resources: - - buckets/finalizers - cloudstorages/finalizers - dataprotectionapplications/finalizers - dataprotectiontests/finalizers @@ -123,7 +115,6 @@ rules: - apiGroups: - oadp.openshift.io resources: - - buckets/status - cloudstorages/status - dataprotectionapplications/status - dataprotectiontests/status @@ -163,15 +154,6 @@ rules: - securitycontextconstraints verbs: - use -- apiGroups: - - security.openshift.io - resourceNames: - - privileged - - velero-privileged - resources: - - securitycontextconstraints - verbs: - - use - apiGroups: - snapshot.storage.k8s.io resources: @@ -198,10 +180,6 @@ rules: - velero.io resources: - '*' - - backups - - backupstoragelocations - - restores - - volumesnapshotlocations verbs: - create - delete diff --git a/internal/controller/validator_test.go b/internal/controller/validator_test.go index 816f09bb91..9a21c07aed 100644 --- a/internal/controller/validator_test.go +++ b/internal/controller/validator_test.go @@ -2,10 +2,12 @@ package controller import ( "fmt" + "strings" "testing" "time" "github.com/go-logr/logr" + oadpv1alpha1 "github.com/openshift/oadp-operator/api/v1alpha1" velerov1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" @@ -15,8 +17,6 @@ import ( "k8s.io/client-go/tools/record" "k8s.io/utils/ptr" "sigs.k8s.io/controller-runtime/pkg/client" - - oadpv1alpha1 "github.com/openshift/oadp-operator/api/v1alpha1" ) func TestDPAReconciler_ValidateDataProtectionCR(t *testing.T) { @@ -2839,12 +2839,12 @@ func (t *testLogSink) WithName(name string) logr.LogSink { re func TestVMFileRestoreValidation(t *testing.T) { tests := []struct { - name string - namespace string - dpa *oadpv1alpha1.DataProtectionApplication - clusterWideDPAs []client.Object - wantErr bool - errorContains string + name string + namespace string + dpa *oadpv1alpha1.DataProtectionApplication + clusterWideDPAs []client.Object + wantErr bool + errorContains string }{ { name: "VM file restore disabled - no validation", @@ -3092,7 +3092,7 @@ func TestVMFileRestoreValidation(t *testing.T) { t.Errorf("ValidateDataProtectionCR() expected error but got none") return } - if tt.errorContains != "" && !contains(err.Error(), tt.errorContains) { + if tt.errorContains != "" && !strings.Contains(err.Error(), tt.errorContains) { t.Errorf("ValidateDataProtectionCR() error = %v, want error containing %v", err, tt.errorContains) } if valid { @@ -3110,16 +3110,3 @@ func TestVMFileRestoreValidation(t *testing.T) { }) } } - -func contains(s, substr string) bool { - return len(s) >= len(substr) && (s == substr || len(substr) == 0 || (len(s) > 0 && len(substr) > 0 && containsHelper(s, substr))) -} - -func containsHelper(s, substr string) bool { - for i := 0; i <= len(s)-len(substr); i++ { - if s[i:i+len(substr)] == substr { - return true - } - } - return false -} diff --git a/internal/controller/vmfilerestore_controller.go b/internal/controller/vmfilerestore_controller.go index e6173e3d5d..157dce51ed 100644 --- a/internal/controller/vmfilerestore_controller.go +++ b/internal/controller/vmfilerestore_controller.go @@ -7,27 +7,26 @@ import ( "strconv" "github.com/go-logr/logr" + oadpv1alpha1 "github.com/openshift/oadp-operator/api/v1alpha1" + "github.com/openshift/oadp-operator/pkg/common" "github.com/sirupsen/logrus" "golang.org/x/exp/maps" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/resource" k8serror "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/intstr" "k8s.io/utils/ptr" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" - - oadpv1alpha1 "github.com/openshift/oadp-operator/api/v1alpha1" - "github.com/openshift/oadp-operator/pkg/common" ) const ( - vmFileRestoreObjectName = "oadp-vm-file-restore-controller-manager" - vmFileRestoreControlPlaneKey = "control-plane" - vmFileRestoreControlPlaneValue = "oadp-vm-file-restore-controller" + vmFileRestoreObjectName = "oadp-vm-file-restore-controller-manager" + vmFileRestoreControlPlaneKey = "control-plane" + vmFileRestoreControlPlaneValue = "oadp-vm-file-restore-controller" vmfrDpaResourceVersionAnnotation = oadpv1alpha1.OadpOperatorLabel + "-vmfr-dpa-resource-version" ) @@ -44,7 +43,7 @@ var ( "app.kubernetes.io/part-of": common.OADPOperator, } - vmfrDpaResourceVersion = "" + vmfrDpaResourceVersion = "" previousVMFileRestoreConfiguration *oadpv1alpha1.VMFileRestore = nil ) diff --git a/internal/controller/vmfilerestore_controller_test.go b/internal/controller/vmfilerestore_controller_test.go index 3d94ef6c30..a335bca1b3 100644 --- a/internal/controller/vmfilerestore_controller_test.go +++ b/internal/controller/vmfilerestore_controller_test.go @@ -3,11 +3,13 @@ package controller import ( "context" "os" + "strings" "testing" "github.com/go-logr/logr" "github.com/onsi/ginkgo/v2" "github.com/onsi/gomega" + oadpv1alpha1 "github.com/openshift/oadp-operator/api/v1alpha1" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" @@ -15,8 +17,6 @@ import ( "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/tools/record" "k8s.io/utils/ptr" - - oadpv1alpha1 "github.com/openshift/oadp-operator/api/v1alpha1" ) const ( @@ -27,12 +27,12 @@ const ( ) type ReconcileVMFileRestoreControllerScenario struct { - namespace string - dpa string - errMessage string - eventWords []string + namespace string + dpa string + errMessage string + eventWords []string vmFileRestoreEnabled bool - deployment *appsv1.Deployment + deployment *appsv1.Deployment } func createTestVMFileRestoreDeployment(namespace string) *appsv1.Deployment { @@ -41,8 +41,8 @@ func createTestVMFileRestoreDeployment(namespace string) *appsv1.Deployment { Name: vmFileRestoreObjectName, Namespace: namespace, Labels: map[string]string{ - "test": "test", - "app.kubernetes.io/name": "wrong", + "test": "test", + "app.kubernetes.io/name": "wrong", vmFileRestoreControlPlaneKey: "super-wrong", }, }, @@ -202,28 +202,28 @@ var _ = ginkgo.Describe("Test ReconcileVMFileRestoreController function", func() ) }, ginkgo.Entry("Should create VM file restore deployment", ReconcileVMFileRestoreControllerScenario{ - namespace: "vmfr-test-1", - dpa: "vmfr-test-1-dpa", - eventWords: []string{"Normal", "VMFileRestoreDeploymentReconciled", "created"}, + namespace: "vmfr-test-1", + dpa: "vmfr-test-1-dpa", + eventWords: []string{"Normal", "VMFileRestoreDeploymentReconciled", "created"}, vmFileRestoreEnabled: true, }), ginkgo.Entry("Should update VM file restore deployment", ReconcileVMFileRestoreControllerScenario{ - namespace: "vmfr-test-2", - dpa: "vmfr-test-2-dpa", - eventWords: []string{"Normal", "VMFileRestoreDeploymentReconciled", "updated"}, + namespace: "vmfr-test-2", + dpa: "vmfr-test-2-dpa", + eventWords: []string{"Normal", "VMFileRestoreDeploymentReconciled", "updated"}, vmFileRestoreEnabled: true, - deployment: createTestVMFileRestoreDeployment("vmfr-test-2"), + deployment: createTestVMFileRestoreDeployment("vmfr-test-2"), }), ginkgo.Entry("Should delete VM file restore deployment", ReconcileVMFileRestoreControllerScenario{ - namespace: "vmfr-test-3", - dpa: "vmfr-test-3-dpa", - eventWords: []string{"Normal", "VMFileRestoreDeploymentDeleteSucceed", "deleted"}, + namespace: "vmfr-test-3", + dpa: "vmfr-test-3-dpa", + eventWords: []string{"Normal", "VMFileRestoreDeploymentDeleteSucceed", "deleted"}, vmFileRestoreEnabled: false, - deployment: createTestVMFileRestoreDeployment("vmfr-test-3"), + deployment: createTestVMFileRestoreDeployment("vmfr-test-3"), }), ginkgo.Entry("Should do nothing when disabled", ReconcileVMFileRestoreControllerScenario{ - namespace: "vmfr-test-4", - dpa: "vmfr-test-4-dpa", + namespace: "vmfr-test-4", + dpa: "vmfr-test-4-dpa", vmFileRestoreEnabled: false, }), ) @@ -241,9 +241,9 @@ var _ = ginkgo.Describe("Test ReconcileVMFileRestoreController function", func() ) }, ginkgo.Entry("Should error because manager container was not found in Deployment", ReconcileVMFileRestoreControllerScenario{ - namespace: "vmfr-test-error-1", - dpa: "vmfr-test-error-1-dpa", - errMessage: "could not find VM file restore container in Deployment", + namespace: "vmfr-test-error-1", + dpa: "vmfr-test-error-1-dpa", + errMessage: "could not find VM file restore container in Deployment", vmFileRestoreEnabled: true, deployment: &appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{ @@ -690,15 +690,15 @@ func TestEnsureVMFileRestoreRequiredLabels(t *testing.T) { func TestBuildVMFileRestoreDeployment(t *testing.T) { tests := []struct { - name string - dpa *oadpv1alpha1.DataProtectionApplication - envVars map[string]string - expectedImage string - expectedEnvCount int - expectedReplicas int32 - expectedSAName string - expectError bool - errorContains string + name string + dpa *oadpv1alpha1.DataProtectionApplication + envVars map[string]string + expectedImage string + expectedEnvCount int + expectedReplicas int32 + expectedSAName string + expectError bool + errorContains string }{ { name: "Should build deployment with default resources and images", @@ -811,7 +811,7 @@ func TestBuildVMFileRestoreDeployment(t *testing.T) { if tt.expectError { if err == nil { t.Errorf("expected error but got none") - } else if tt.errorContains != "" && !contains(err.Error(), tt.errorContains) { + } else if tt.errorContains != "" && !strings.Contains(err.Error(), tt.errorContains) { t.Errorf("expected error to contain '%s', got: %v", tt.errorContains, err) } return From 6059e81be7b6ef01b36761b578cec98e905857d3 Mon Sep 17 00:00:00 2001 From: Shubham Pampattiwar Date: Wed, 12 Nov 2025 15:09:29 -0800 Subject: [PATCH 4/6] lint fix --- Makefile | 2 +- internal/controller/validator_test.go | 3 ++- internal/controller/vmfilerestore_controller.go | 5 +++-- internal/controller/vmfilerestore_controller_test.go | 3 ++- 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/Makefile b/Makefile index 1582637572..b05afbaa81 100644 --- a/Makefile +++ b/Makefile @@ -4,7 +4,7 @@ DEFAULT_VERSION := 99.0.0 VERSION ?= $(DEFAULT_VERSION) # the version of the operator OPERATOR_SDK_VERSION ?= v1.35.0 ENVTEST_K8S_VERSION = 1.32 #refers to the version of kubebuilder assets to be downloaded by envtest binary # Kubernetes version from OpenShift 4.19.x -GOLANGCI_LINT_VERSION ?= v2.1.2 +GOLANGCI_LINT_VERSION ?= v2.6.1 KUSTOMIZE_VERSION ?= v5.2.1 CONTROLLER_TOOLS_VERSION ?= v0.16.5 OPM_VERSION ?= v1.23.0 diff --git a/internal/controller/validator_test.go b/internal/controller/validator_test.go index 9a21c07aed..d52812d7ef 100644 --- a/internal/controller/validator_test.go +++ b/internal/controller/validator_test.go @@ -7,7 +7,6 @@ import ( "time" "github.com/go-logr/logr" - oadpv1alpha1 "github.com/openshift/oadp-operator/api/v1alpha1" velerov1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" @@ -17,6 +16,8 @@ import ( "k8s.io/client-go/tools/record" "k8s.io/utils/ptr" "sigs.k8s.io/controller-runtime/pkg/client" + + oadpv1alpha1 "github.com/openshift/oadp-operator/api/v1alpha1" ) func TestDPAReconciler_ValidateDataProtectionCR(t *testing.T) { diff --git a/internal/controller/vmfilerestore_controller.go b/internal/controller/vmfilerestore_controller.go index 157dce51ed..0728765d37 100644 --- a/internal/controller/vmfilerestore_controller.go +++ b/internal/controller/vmfilerestore_controller.go @@ -7,8 +7,6 @@ import ( "strconv" "github.com/go-logr/logr" - oadpv1alpha1 "github.com/openshift/oadp-operator/api/v1alpha1" - "github.com/openshift/oadp-operator/pkg/common" "github.com/sirupsen/logrus" "golang.org/x/exp/maps" appsv1 "k8s.io/api/apps/v1" @@ -21,6 +19,9 @@ import ( "k8s.io/utils/ptr" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + + oadpv1alpha1 "github.com/openshift/oadp-operator/api/v1alpha1" + "github.com/openshift/oadp-operator/pkg/common" ) const ( diff --git a/internal/controller/vmfilerestore_controller_test.go b/internal/controller/vmfilerestore_controller_test.go index a335bca1b3..9a87a04737 100644 --- a/internal/controller/vmfilerestore_controller_test.go +++ b/internal/controller/vmfilerestore_controller_test.go @@ -9,7 +9,6 @@ import ( "github.com/go-logr/logr" "github.com/onsi/ginkgo/v2" "github.com/onsi/gomega" - oadpv1alpha1 "github.com/openshift/oadp-operator/api/v1alpha1" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" @@ -17,6 +16,8 @@ import ( "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/tools/record" "k8s.io/utils/ptr" + + oadpv1alpha1 "github.com/openshift/oadp-operator/api/v1alpha1" ) const ( From 06dd15758f07991ed30678832454290afd5f6dda Mon Sep 17 00:00:00 2001 From: Shubham Pampattiwar Date: Thu, 13 Nov 2025 13:43:54 -0800 Subject: [PATCH 5/6] make bundle --- .../oadp-operator.clusterserviceversion.yaml | 26 +++++++++++++++++++ config/rbac/role.yaml | 22 ++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/bundle/manifests/oadp-operator.clusterserviceversion.yaml b/bundle/manifests/oadp-operator.clusterserviceversion.yaml index efdb3b301f..b8a02fc774 100644 --- a/bundle/manifests/oadp-operator.clusterserviceversion.yaml +++ b/bundle/manifests/oadp-operator.clusterserviceversion.yaml @@ -1016,6 +1016,12 @@ spec: - patch - update - watch + - apiGroups: + - '*' + resources: + - '*' + verbs: + - '*' - apiGroups: - apps resources: @@ -1073,6 +1079,8 @@ spec: - apiGroups: - oadp.openshift.io resources: + - '*' + - buckets - cloudstorages - dataprotectionapplications - dataprotectiontests @@ -1087,6 +1095,7 @@ spec: - apiGroups: - oadp.openshift.io resources: + - buckets/finalizers - cloudstorages/finalizers - dataprotectionapplications/finalizers - dataprotectiontests/finalizers @@ -1095,6 +1104,7 @@ spec: - apiGroups: - oadp.openshift.io resources: + - buckets/status - cloudstorages/status - dataprotectionapplications/status - dataprotectiontests/status @@ -1138,6 +1148,7 @@ spec: - security.openshift.io resourceNames: - privileged + - velero-privileged resources: - securitycontextconstraints verbs: @@ -1167,6 +1178,7 @@ spec: - apiGroups: - velero.io resources: + - '*' - backups - backupstoragelocations - restores @@ -1214,6 +1226,20 @@ spec: - packagemanifests verbs: - '*' + - apiGroups: + - '*' + resources: + - '*' + verbs: + - get + - watch + - list + - update + - patch + - create + - delete + - assign + - deletecollection - nonResourceURLs: - '*' verbs: diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index fbd9cb5985..34509819d7 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -35,6 +35,12 @@ rules: - patch - update - watch +- apiGroups: + - '*' + resources: + - '*' + verbs: + - '*' - apiGroups: - apps resources: @@ -93,6 +99,7 @@ rules: - oadp.openshift.io resources: - '*' + - buckets - cloudstorages - dataprotectionapplications - dataprotectiontests @@ -107,6 +114,7 @@ rules: - apiGroups: - oadp.openshift.io resources: + - buckets/finalizers - cloudstorages/finalizers - dataprotectionapplications/finalizers - dataprotectiontests/finalizers @@ -115,6 +123,7 @@ rules: - apiGroups: - oadp.openshift.io resources: + - buckets/status - cloudstorages/status - dataprotectionapplications/status - dataprotectiontests/status @@ -154,6 +163,15 @@ rules: - securitycontextconstraints verbs: - use +- apiGroups: + - security.openshift.io + resourceNames: + - privileged + - velero-privileged + resources: + - securitycontextconstraints + verbs: + - use - apiGroups: - snapshot.storage.k8s.io resources: @@ -180,6 +198,10 @@ rules: - velero.io resources: - '*' + - backups + - backupstoragelocations + - restores + - volumesnapshotlocations verbs: - create - delete From 7a274ec65817d61426490e7c02cb0df5ac72c98c Mon Sep 17 00:00:00 2001 From: Tiger Kaovilai Date: Tue, 18 Nov 2025 18:47:05 -0500 Subject: [PATCH 6/6] `make bundle` after `gh pr checkout 2003 --recurse-submodules` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ❯ gh pr checkout 2003 --recurse-submodules remote: Enumerating objects: 77, done. remote: Counting objects: 100% (29/29), done. remote: Compressing objects: 100% (14/14), done. remote: Total 77 (delta 15), reused 18 (delta 14), pack-reused 48 (from 1) Unpacking objects: 100% (77/77), 118.21 KiB | 1.01 MiB/s, done. From https://github.com/shubham-pampattiwar/oadp-operator * [new branch] vm-file-restore-integration -> shub/vm-file-restore-integration Previous HEAD position was 24e37584 update prow notes, operator-config (#2029) branch 'vm-file-restore-integration' set up to track 'shub/vm-file-restore-integration'. Switched to a new branch 'vm-file-restore-integration' ~/oadp-operator vm-file-restore-integration ❯ make bundle Using Container Tool: docker [ -f /Users/tkaovila/oadp-operator/bin/oadp-dev/controller-gen ] || { set -e ; mkdir -p /Users/tkaovila/oadp-operator/bin/oadp-dev/ ; TMP_DIR=$(mktemp -d) ; cd $TMP_DIR ; go mod init tmp ; echo "Downloading sigs.k8s.io/controller-tools/cmd/controller-gen@v0.16.5 to branch directory" ; GOBIN=/Users/tkaovila/oadp-operator/bin/oadp-dev/ go install -mod=mod sigs.k8s.io/controller-tools/cmd/controller-gen@v0.16.5 ; rm -rf $TMP_DIR ; } GOFLAGS="-mod=mod" /Users/tkaovila/oadp-operator/bin/oadp-dev/controller-gen rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases [ -f /Users/tkaovila/oadp-operator/bin/oadp-dev/kustomize ] || { set -e ; mkdir -p /Users/tkaovila/oadp-operator/bin/oadp-dev/ ; TMP_DIR=$(mktemp -d) ; cd $TMP_DIR ; go mod init tmp ; echo "Downloading sigs.k8s.io/kustomize/kustomize/v5@v5.2.1 to branch directory" ; GOBIN=/Users/tkaovila/oadp-operator/bin/oadp-dev/ go install -mod=mod sigs.k8s.io/kustomize/kustomize/v5@v5.2.1 ; rm -rf $TMP_DIR ; } GOFLAGS="-mod=mod" /Users/tkaovila/oadp-operator/bin/oadp-dev/operator-sdk generate kustomize manifests -q cd config/manager && GOFLAGS="-mod=mod" /Users/tkaovila/oadp-operator/bin/oadp-dev/kustomize edit set image controller=quay.io/konveyor/oadp-operator:latest GOFLAGS="-mod=mod" /Users/tkaovila/oadp-operator/bin/oadp-dev/kustomize build config/manifests | GOFLAGS="-mod=mod" /Users/tkaovila/oadp-operator/bin/oadp-dev/operator-sdk generate bundle -q --extra-service-accounts "velero,non-admin-controller,oadp-vm-file-restore-controller-manager" --overwrite --version 99.0.0 --channels="dev" --default-channel="dev" WARN[0000] ClusterServiceVersion validation: [OperationFailed] provided API should have an example annotation WARN[0000] ClusterServiceVersion validation: [OperationFailed] provided API should have an example annotation WARN[0000] ClusterServiceVersion validation: [OperationFailed] provided API should have an example annotation WARN[0000] ClusterServiceVersion validation: [OperationFailed] provided API should have an example annotation WARN[0000] ClusterServiceVersion validation: [OperationFailed] provided API should have an example annotation WARN[0000] ClusterServiceVersion validation: [OperationFailed] provided API should have an example annotation WARN[0000] ClusterServiceVersion validation: [CSVFileNotValid] (oadp-operator.v99.0.0) csv.Spec.minKubeVersion is not informed. It is recommended you provide this information. Otherwise, it would mean that your operator project can be distributed and installed in any cluster version available, which is not necessarily the case for all projects. INFO[0000] Creating bundle.Dockerfile INFO[0000] Creating bundle/metadata/annotations.yaml INFO[0000] Bundle metadata generated successfully Using Container Tool: docker Using Container Tool: docker [ -f /Users/tkaovila/oadp-operator/bin/yq ] || { set -e ; TMP_DIR=$(mktemp -d) ; cd $TMP_DIR ; go mod init tmp ; echo "Downloading github.com/mikefarah/yq/v4@v4.28.1" ; GOBIN=/Users/tkaovila/oadp-operator/bin go install -mod=mod github.com/mikefarah/yq/v4@v4.28.1 ; rm -rf $TMP_DIR ; } go: creating new go.mod: module tmp Downloading github.com/mikefarah/yq/v4@v4.28.1 Using Container Tool: docker [ -f /Users/tkaovila/oadp-operator/bin/yq ] || { set -e ; TMP_DIR=$(mktemp -d) ; cd $TMP_DIR ; go mod init tmp ; echo "Downloading github.com/mikefarah/yq/v4@v4.28.1" ; GOBIN=/Users/tkaovila/oadp-operator/bin go install -mod=mod github.com/mikefarah/yq/v4@v4.28.1 ; rm -rf $TMP_DIR ; } cp bundle.Dockerfile build/Dockerfile.bundle GOFLAGS="-mod=mod" /Users/tkaovila/oadp-operator/bin/oadp-dev/operator-sdk bundle validate ./bundle WARN[0000] Warning: Value velero.io/v1, Kind=BackupRepository: provided API should have an example annotation WARN[0000] Warning: Value oadp.openshift.io/v1alpha1, Kind=VirtualMachineBackupsDiscovery: provided API should have an example annotation WARN[0000] Warning: Value oadp.openshift.io/v1alpha1, Kind=CloudStorage: provided API should have an example annotation WARN[0000] Warning: Value velero.io/v2alpha1, Kind=DataUpload: provided API should have an example annotation WARN[0000] Warning: Value velero.io/v2alpha1, Kind=DataDownload: provided API should have an example annotation WARN[0000] Warning: Value oadp.openshift.io/v1alpha1, Kind=VirtualMachineFileRestore: provided API should have an example annotation WARN[0000] Warning: Value : (oadp-operator.v99.0.0) csv.Spec.minKubeVersion is not informed. It is recommended you provide this information. Otherwise, it would mean that your operator project can be distributed and installed in any cluster version available, which is not necessarily the case for all projects. INFO[0000] All validation tests have completed successfully gsed -e 's/ createdAt: .*/ createdAt: "2025-02-28T20:03:54Z"/' bundle/manifests/oadp-operator.clusterserviceversion.yaml > bundle/manifests/oadp-operator.clusterserviceversion.yaml.tmp mv bundle/manifests/oadp-operator.clusterserviceversion.yaml.tmp bundle/manifests/oadp-operator.clusterserviceversion.yaml Signed-off-by: Tiger Kaovilai --- .../oadp-operator.clusterserviceversion.yaml | 22 ------------------- config/rbac/role.yaml | 22 ------------------- 2 files changed, 44 deletions(-) diff --git a/bundle/manifests/oadp-operator.clusterserviceversion.yaml b/bundle/manifests/oadp-operator.clusterserviceversion.yaml index b8a02fc774..cf45ed4573 100644 --- a/bundle/manifests/oadp-operator.clusterserviceversion.yaml +++ b/bundle/manifests/oadp-operator.clusterserviceversion.yaml @@ -1016,12 +1016,6 @@ spec: - patch - update - watch - - apiGroups: - - '*' - resources: - - '*' - verbs: - - '*' - apiGroups: - apps resources: @@ -1080,7 +1074,6 @@ spec: - oadp.openshift.io resources: - '*' - - buckets - cloudstorages - dataprotectionapplications - dataprotectiontests @@ -1095,7 +1088,6 @@ spec: - apiGroups: - oadp.openshift.io resources: - - buckets/finalizers - cloudstorages/finalizers - dataprotectionapplications/finalizers - dataprotectiontests/finalizers @@ -1104,7 +1096,6 @@ spec: - apiGroups: - oadp.openshift.io resources: - - buckets/status - cloudstorages/status - dataprotectionapplications/status - dataprotectiontests/status @@ -1144,15 +1135,6 @@ spec: - securitycontextconstraints verbs: - use - - apiGroups: - - security.openshift.io - resourceNames: - - privileged - - velero-privileged - resources: - - securitycontextconstraints - verbs: - - use - apiGroups: - snapshot.storage.k8s.io resources: @@ -1179,10 +1161,6 @@ spec: - velero.io resources: - '*' - - backups - - backupstoragelocations - - restores - - volumesnapshotlocations verbs: - create - delete diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 34509819d7..fbd9cb5985 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -35,12 +35,6 @@ rules: - patch - update - watch -- apiGroups: - - '*' - resources: - - '*' - verbs: - - '*' - apiGroups: - apps resources: @@ -99,7 +93,6 @@ rules: - oadp.openshift.io resources: - '*' - - buckets - cloudstorages - dataprotectionapplications - dataprotectiontests @@ -114,7 +107,6 @@ rules: - apiGroups: - oadp.openshift.io resources: - - buckets/finalizers - cloudstorages/finalizers - dataprotectionapplications/finalizers - dataprotectiontests/finalizers @@ -123,7 +115,6 @@ rules: - apiGroups: - oadp.openshift.io resources: - - buckets/status - cloudstorages/status - dataprotectionapplications/status - dataprotectiontests/status @@ -163,15 +154,6 @@ rules: - securitycontextconstraints verbs: - use -- apiGroups: - - security.openshift.io - resourceNames: - - privileged - - velero-privileged - resources: - - securitycontextconstraints - verbs: - - use - apiGroups: - snapshot.storage.k8s.io resources: @@ -198,10 +180,6 @@ rules: - velero.io resources: - '*' - - backups - - backupstoragelocations - - restores - - volumesnapshotlocations verbs: - create - delete