diff --git a/Makefile b/Makefile index 443fa00..7b51ccc 100644 --- a/Makefile +++ b/Makefile @@ -48,6 +48,7 @@ help: ## Display this help. .PHONY: manifests manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects. $(CONTROLLER_GEN) rbac:roleName=non-admin-controller-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases + sed -i 's/Velero backup/NonAdminBackup/' ./config/crd/bases/nac.oadp.openshift.io_nonadminrestores.yaml .PHONY: generate generate: controller-gen ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations. diff --git a/PROJECT b/PROJECT index b10e3ba..3f6ef52 100644 --- a/PROJECT +++ b/PROJECT @@ -17,4 +17,13 @@ resources: kind: NonAdminBackup path: github.com/migtools/oadp-non-admin/api/v1alpha1 version: v1alpha1 +- api: + crdVersion: v1 + namespaced: true + controller: true + domain: oadp.openshift.io + group: nac + kind: NonAdminRestore + path: github.com/migtools/oadp-non-admin/api/v1alpha1 + version: v1alpha1 version: "3" diff --git a/README.md b/README.md index e98f34d..5c5fe0d 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,8 @@ To use NAC functionality: ``` Check the application was successful deployed by accessing its route. + + Create and update items in application UI, to later check if application was successfully restored. - create NonAdminBackup For example, use one of the sample NonAdminBackup available in `hack/samples/backups/` folder, by running @@ -47,7 +49,28 @@ To use NAC functionality: | oc create -f - ``` - - TODO NonAdminRestore + - delete sample application + + For example, delete one of the sample applications available in `hack/samples/apps/` folder, by running + ```sh + oc process -f ./hack/samples/apps/ \ + -p NAMESPACE= \ + | oc delete -f - + ``` + + Check that application was successful deleted by accessing its route. + - create NonAdminRestore + + For example, use one of the sample NonAdminRestore available in `hack/samples/restores/` folder, by running + ```sh + oc process -f ./hack/samples/restores/ \ + -p NAMESPACE= \ + -p NAME= \ + | oc create -f - + ``` + + + After NonAdminRestore completes, check if the application was successful restored by accessing its route and seeing its items in application UI. ## Contributing diff --git a/api/v1alpha1/nonadminbackup_types.go b/api/v1alpha1/nonadminbackup_types.go index ea4657a..73b6966 100644 --- a/api/v1alpha1/nonadminbackup_types.go +++ b/api/v1alpha1/nonadminbackup_types.go @@ -21,17 +21,17 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -// NonAdminBackupPhase is a simple one high-level summary of the lifecycle of an NonAdminBackup. +// NonAdminPhase is a simple one high-level summary of the lifecycle of a non admin object. // +kubebuilder:validation:Enum=New;BackingOff;Created -type NonAdminBackupPhase string +type NonAdminPhase string const ( - // NonAdminBackupPhaseNew - NonAdminBackup resource was accepted by the OpenShift cluster, but it has not yet been processed by the NonAdminController - NonAdminBackupPhaseNew NonAdminBackupPhase = "New" - // NonAdminBackupPhaseBackingOff - Velero Backup object was not created due to NonAdminBackup error (configuration or similar) - NonAdminBackupPhaseBackingOff NonAdminBackupPhase = "BackingOff" - // NonAdminBackupPhaseCreated - Velero Backup was created. The Phase will not have additional informations about the Backup. - NonAdminBackupPhaseCreated NonAdminBackupPhase = "Created" + // NonAdminPhaseNew - non admin resource was accepted by the OpenShift cluster, but it has not yet been processed by the NonAdminController + NonAdminPhaseNew NonAdminPhase = "New" + // NonAdminPhaseBackingOff - Velero object was not created due to error in non admin object (configuration or similar) + NonAdminPhaseBackingOff NonAdminPhase = "BackingOff" + // NonAdminPhaseCreated - Velero object was created. The Phase will not have additional information about the Velero object. + NonAdminPhaseCreated NonAdminPhase = "Created" ) // NonAdminBackupSpec defines the desired state of NonAdminBackup @@ -60,8 +60,8 @@ type NonAdminBackupStatus struct { // +optional VeleroBackupStatus *velerov1api.BackupStatus `json:"veleroBackupStatus,omitempty"` - Phase NonAdminBackupPhase `json:"phase,omitempty"` - Conditions []metav1.Condition `json:"conditions,omitempty"` + Phase NonAdminPhase `json:"phase,omitempty"` + Conditions []metav1.Condition `json:"conditions,omitempty"` } // +kubebuilder:object:root=true diff --git a/api/v1alpha1/nonadminrestore_types.go b/api/v1alpha1/nonadminrestore_types.go new file mode 100644 index 0000000..4a50149 --- /dev/null +++ b/api/v1alpha1/nonadminrestore_types.go @@ -0,0 +1,75 @@ +/* +Copyright 2024. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha1 + +import ( + velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// NonAdminRestoreSpec defines the desired state of NonAdminRestore +type NonAdminRestoreSpec struct { + // Specification for a Velero restore. + RestoreSpec *velerov1api.RestoreSpec `json:"restoreSpec"` + // TODO need to investigate restoreSpec.namespaceMapping, depends on how NAC tracks the namespace access per user + + // TODO NonAdminRestore log level, by default TODO. + // +optional + // +kubebuilder:validation:Enum=trace;debug;info;warning;error;fatal;panic + LogLevel string `json:"logLevel,omitempty"` + // TODO ALSO ADD TEST FOR DIFFERENT LOG LEVELS +} + +// NonAdminRestoreStatus defines the observed state of NonAdminRestore +type NonAdminRestoreStatus struct { + // Related Velero Restore name. + // +optional + VeleroRestoreName string `json:"veleroRestoreName,omitempty"` + + // Related Velero Restore status. + // +optional + VeleroRestoreStatus *velerov1api.RestoreStatus `json:"veleroRestoreStatus,omitempty"` + + Phase NonAdminPhase `json:"phase,omitempty"` + Conditions []metav1.Condition `json:"conditions,omitempty"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:resource:path=nonadminrestores,shortName=nar + +// NonAdminRestore is the Schema for the nonadminrestores API +type NonAdminRestore struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec NonAdminRestoreSpec `json:"spec,omitempty"` + Status NonAdminRestoreStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// NonAdminRestoreList contains a list of NonAdminRestore +type NonAdminRestoreList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []NonAdminRestore `json:"items"` +} + +func init() { + SchemeBuilder.Register(&NonAdminRestore{}, &NonAdminRestoreList{}) +} diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 9ac916b..0d2f898 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -131,3 +131,109 @@ func (in *NonAdminBackupStatus) DeepCopy() *NonAdminBackupStatus { in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NonAdminRestore) DeepCopyInto(out *NonAdminRestore) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NonAdminRestore. +func (in *NonAdminRestore) DeepCopy() *NonAdminRestore { + if in == nil { + return nil + } + out := new(NonAdminRestore) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *NonAdminRestore) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NonAdminRestoreList) DeepCopyInto(out *NonAdminRestoreList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]NonAdminRestore, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NonAdminRestoreList. +func (in *NonAdminRestoreList) DeepCopy() *NonAdminRestoreList { + if in == nil { + return nil + } + out := new(NonAdminRestoreList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *NonAdminRestoreList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NonAdminRestoreSpec) DeepCopyInto(out *NonAdminRestoreSpec) { + *out = *in + if in.RestoreSpec != nil { + in, out := &in.RestoreSpec, &out.RestoreSpec + *out = new(v1.RestoreSpec) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NonAdminRestoreSpec. +func (in *NonAdminRestoreSpec) DeepCopy() *NonAdminRestoreSpec { + if in == nil { + return nil + } + out := new(NonAdminRestoreSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NonAdminRestoreStatus) DeepCopyInto(out *NonAdminRestoreStatus) { + *out = *in + if in.VeleroRestoreStatus != nil { + in, out := &in.VeleroRestoreStatus, &out.VeleroRestoreStatus + *out = new(v1.RestoreStatus) + (*in).DeepCopyInto(*out) + } + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]metav1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NonAdminRestoreStatus. +func (in *NonAdminRestoreStatus) DeepCopy() *NonAdminRestoreStatus { + if in == nil { + return nil + } + out := new(NonAdminRestoreStatus) + in.DeepCopyInto(out) + return out +} diff --git a/cmd/main.go b/cmd/main.go index 8d6be27..d047639 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -98,7 +98,8 @@ func main() { TLSOpts: tlsOpts, }) - if len(constant.OadpNamespace) == 0 { + // TODO create get function in common :question: + if len(os.Getenv(constant.NamespaceEnvVar)) == 0 { setupLog.Error(fmt.Errorf("%v environment variable is empty", constant.NamespaceEnvVar), "environment variable must be set") os.Exit(1) } @@ -138,6 +139,13 @@ func main() { setupLog.Error(err, "unable to create controller", "controller", "NonAdminBackup") os.Exit(1) } + if err = (&controller.NonAdminRestoreReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "NonAdminRestore") + os.Exit(1) + } // +kubebuilder:scaffold:builder if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { diff --git a/config/crd/bases/nac.oadp.openshift.io_nonadminbackups.yaml b/config/crd/bases/nac.oadp.openshift.io_nonadminbackups.yaml index 41dac9e..53e5b39 100644 --- a/config/crd/bases/nac.oadp.openshift.io_nonadminbackups.yaml +++ b/config/crd/bases/nac.oadp.openshift.io_nonadminbackups.yaml @@ -591,8 +591,8 @@ spec: type: object type: array phase: - description: NonAdminBackupPhase is a simple one high-level summary - of the lifecycle of an NonAdminBackup. + description: NonAdminPhase is a simple one high-level summary of the + lifecycle of a non admin object. enum: - New - BackingOff diff --git a/config/crd/bases/nac.oadp.openshift.io_nonadminrestores.yaml b/config/crd/bases/nac.oadp.openshift.io_nonadminrestores.yaml new file mode 100644 index 0000000..fa420e3 --- /dev/null +++ b/config/crd/bases/nac.oadp.openshift.io_nonadminrestores.yaml @@ -0,0 +1,625 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.14.0 + name: nonadminrestores.nac.oadp.openshift.io +spec: + group: nac.oadp.openshift.io + names: + kind: NonAdminRestore + listKind: NonAdminRestoreList + plural: nonadminrestores + shortNames: + - nar + singular: nonadminrestore + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: NonAdminRestore is the Schema for the nonadminrestores 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: NonAdminRestoreSpec defines the desired state of NonAdminRestore + properties: + logLevel: + description: TODO NonAdminRestore log level, by default TODO. + enum: + - trace + - debug + - info + - warning + - error + - fatal + - panic + type: string + restoreSpec: + description: Specification for a Velero restore. + properties: + backupName: + description: |- + BackupName is the unique name of the NonAdminBackup to restore + from. + type: string + excludedNamespaces: + description: |- + ExcludedNamespaces contains a list of namespaces that are not + included in the restore. + items: + type: string + nullable: true + type: array + excludedResources: + description: |- + ExcludedResources is a slice of resource names that are not + included in the restore. + items: + type: string + nullable: true + type: array + existingResourcePolicy: + description: ExistingResourcePolicy specifies the restore behavior + for the Kubernetes resource to be restored + nullable: true + type: string + hooks: + description: Hooks represent custom behaviors that should be executed + during or post restore. + properties: + resources: + items: + description: |- + RestoreResourceHookSpec defines one or more RestoreResrouceHooks that should be executed based on + the rules defined for namespaces, resources, and label selector. + properties: + excludedNamespaces: + description: ExcludedNamespaces specifies the namespaces + to which this hook spec does not apply. + items: + type: string + nullable: true + type: array + excludedResources: + description: ExcludedResources specifies the resources + to which this hook spec does not apply. + items: + type: string + nullable: true + type: array + includedNamespaces: + description: |- + IncludedNamespaces specifies the namespaces to which this hook spec applies. If empty, it applies + to all namespaces. + items: + type: string + nullable: true + type: array + includedResources: + description: |- + IncludedResources specifies the resources to which this hook spec applies. If empty, it applies + to all resources. + items: + type: string + nullable: true + type: array + labelSelector: + description: LabelSelector, if specified, filters the + resources to which this hook spec applies. + nullable: true + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the + selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + name: + description: Name is the name of this hook. + type: string + postHooks: + description: PostHooks is a list of RestoreResourceHooks + to execute during and after restoring a resource. + items: + description: RestoreResourceHook defines a restore + hook for a resource. + properties: + exec: + description: Exec defines an exec restore hook. + properties: + command: + description: Command is the command and arguments + to execute from within a container after + a pod has been restored. + items: + type: string + minItems: 1 + type: array + container: + description: |- + Container is the container in the pod where the command should be executed. If not specified, + the pod's first container is used. + type: string + execTimeout: + description: |- + ExecTimeout defines the maximum amount of time Velero should wait for the hook to complete before + considering the execution a failure. + type: string + onError: + description: OnError specifies how Velero + should behave if it encounters an error + executing this hook. + enum: + - Continue + - Fail + type: string + waitTimeout: + description: |- + WaitTimeout defines the maximum amount of time Velero should wait for the container to be Ready + before attempting to run the command. + type: string + required: + - command + type: object + init: + description: Init defines an init restore hook. + properties: + initContainers: + description: InitContainers is list of init + containers to be added to a pod during its + restore. + items: + type: object + x-kubernetes-preserve-unknown-fields: true + type: array + x-kubernetes-preserve-unknown-fields: true + timeout: + description: Timeout defines the maximum amount + of time Velero should wait for the initContainers + to complete. + type: string + type: object + type: object + type: array + required: + - name + type: object + type: array + type: object + includeClusterResources: + description: |- + IncludeClusterResources specifies whether cluster-scoped resources + should be included for consideration in the restore. If null, defaults + to true. + nullable: true + type: boolean + includedNamespaces: + description: |- + IncludedNamespaces is a slice of namespace names to include objects + from. If empty, all namespaces are included. + items: + type: string + nullable: true + type: array + includedResources: + description: |- + IncludedResources is a slice of resource names to include + in the restore. If empty, all resources in the backup are included. + items: + type: string + nullable: true + type: array + itemOperationTimeout: + description: |- + ItemOperationTimeout specifies the time used to wait for RestoreItemAction operations + The default value is 1 hour. + type: string + labelSelector: + description: |- + LabelSelector is a metav1.LabelSelector to filter with + when restoring individual objects from the backup. If empty + or nil, all objects are included. Optional. + nullable: true + properties: + matchExpressions: + description: matchExpressions is a list of label selector + requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector + applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + namespaceMapping: + additionalProperties: + type: string + description: |- + NamespaceMapping is a map of source namespace names + to target namespace names to restore into. Any source + namespaces not included in the map will be restored into + namespaces of the same name. + type: object + orLabelSelectors: + description: |- + OrLabelSelectors is list of metav1.LabelSelector to filter with + when restoring individual objects from the backup. If multiple provided + they will be joined by the OR operator. LabelSelector as well as + OrLabelSelectors cannot co-exist in restore request, only one of them + can be used + items: + description: |- + A label selector is a label query over a set of resources. The result of matchLabels and + matchExpressions are ANDed. An empty label selector matches all objects. A null + label selector matches no objects. + properties: + matchExpressions: + description: matchExpressions is a list of label selector + requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector + applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + nullable: true + type: array + preserveNodePorts: + description: PreserveNodePorts specifies whether to restore old + nodePorts from backup. + nullable: true + type: boolean + resourceModifier: + description: ResourceModifier specifies the reference to JSON + resource patches that should be applied to resources before + restoration. + nullable: true + properties: + apiGroup: + description: |- + APIGroup is the group for the resource being referenced. + If APIGroup is not specified, the specified Kind must be in the core API group. + For any other third-party types, APIGroup is required. + type: string + kind: + description: Kind is the type of resource being referenced + type: string + name: + description: Name is the name of resource being referenced + type: string + required: + - kind + - name + type: object + x-kubernetes-map-type: atomic + restorePVs: + description: |- + RestorePVs specifies whether to restore all included + PVs from snapshot + nullable: true + type: boolean + restoreStatus: + description: |- + RestoreStatus specifies which resources we should restore the status + field. If nil, no objects are included. Optional. + nullable: true + properties: + excludedResources: + description: ExcludedResources specifies the resources to + which will not restore the status. + items: + type: string + nullable: true + type: array + includedResources: + description: |- + IncludedResources specifies the resources to which will restore the status. + If empty, it applies to all resources. + items: + type: string + nullable: true + type: array + type: object + scheduleName: + description: |- + ScheduleName is the unique name of the Velero schedule to restore + from. If specified, and BackupName is empty, Velero will restore + from the most recent successful backup created from this schedule. + type: string + required: + - backupName + type: object + required: + - restoreSpec + type: object + status: + description: NonAdminRestoreStatus defines the observed state of NonAdminRestore + properties: + conditions: + items: + description: "Condition contains details for one aspect of the current + state of this API Resource.\n---\nThis struct is intended for + direct use as an array at the field path .status.conditions. For + example,\n\n\n\ttype FooStatus struct{\n\t // Represents the + observations of a foo's current state.\n\t // Known .status.conditions.type + are: \"Available\", \"Progressing\", and \"Degraded\"\n\t // + +patchMergeKey=type\n\t // +patchStrategy=merge\n\t // +listType=map\n\t + \ // +listMapKey=type\n\t Conditions []metav1.Condition `json:\"conditions,omitempty\" + patchStrategy:\"merge\" patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"`\n\n\n\t + \ // other fields\n\t}" + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: |- + type of condition in CamelCase or in foo.example.com/CamelCase. + --- + Many .condition.type values are consistent across resources like Available, but because arbitrary conditions can be + useful (see .node.status.conditions), the ability to deconflict is important. + The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + phase: + description: NonAdminPhase is a simple one high-level summary of the + lifecycle of a non admin object. + enum: + - New + - BackingOff + - Created + type: string + veleroRestoreName: + description: Related Velero Restore name. + type: string + veleroRestoreStatus: + description: Related Velero Restore status. + properties: + completionTimestamp: + description: |- + CompletionTimestamp records the time the restore operation was completed. + Completion time is recorded even on failed restore. + The server's time is used for StartTimestamps + format: date-time + nullable: true + type: string + errors: + description: |- + Errors is a count of all error messages that were generated during + execution of the restore. The actual errors are stored in object storage. + type: integer + failureReason: + description: FailureReason is an error that caused the entire + restore to fail. + type: string + phase: + description: Phase is the current state of the Restore + enum: + - New + - FailedValidation + - InProgress + - WaitingForPluginOperations + - WaitingForPluginOperationsPartiallyFailed + - Completed + - PartiallyFailed + - Failed + type: string + progress: + description: |- + Progress contains information about the restore's execution progress. Note + that this information is best-effort only -- if Velero fails to update it + during a restore for any reason, it may be inaccurate/stale. + nullable: true + properties: + itemsRestored: + description: ItemsRestored is the number of items that have + actually been restored so far + type: integer + totalItems: + description: |- + TotalItems is the total number of items to be restored. This number may change + throughout the execution of the restore due to plugins that return additional related + items to restore + type: integer + type: object + restoreItemOperationsAttempted: + description: |- + RestoreItemOperationsAttempted is the total number of attempted + async RestoreItemAction operations for this restore. + type: integer + restoreItemOperationsCompleted: + description: |- + RestoreItemOperationsCompleted is the total number of successfully completed + async RestoreItemAction operations for this restore. + type: integer + restoreItemOperationsFailed: + description: |- + RestoreItemOperationsFailed is the total number of async + RestoreItemAction operations for this restore which ended with an error. + type: integer + startTimestamp: + description: |- + StartTimestamp records the time the restore operation was started. + The server's time is used for StartTimestamps + format: date-time + nullable: true + type: string + validationErrors: + description: |- + ValidationErrors is a slice of all validation errors (if + applicable) + items: + type: string + nullable: true + type: array + warnings: + description: |- + Warnings is a count of all warning messages that were generated during + execution of the restore. The actual warnings are stored in object storage. + type: integer + type: object + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index 2665502..a0ae6c7 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -3,17 +3,20 @@ # It should be run by config/default resources: - bases/nac.oadp.openshift.io_nonadminbackups.yaml +- bases/nac.oadp.openshift.io_nonadminrestores.yaml #+kubebuilder:scaffold:crdkustomizeresource patches: # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix. # patches here are for enabling the conversion webhook for each CRD #- path: patches/webhook_in_nonadminbackups.yaml +#- path: patches/webhook_in_nonadminrestores.yaml #+kubebuilder:scaffold:crdkustomizewebhookpatch # [CERTMANAGER] To enable cert-manager, uncomment all the sections with [CERTMANAGER] prefix. # patches here are for enabling the CA injection for each CRD #- path: patches/cainjection_in_nonadminbackups.yaml +#- path: patches/cainjection_in_nonadminrestores.yaml #+kubebuilder:scaffold:crdkustomizecainjectionpatch # [WEBHOOK] To enable webhook, uncomment the following section diff --git a/config/rbac/nonadminrestore_editor_role.yaml b/config/rbac/nonadminrestore_editor_role.yaml new file mode 100644 index 0000000..9390a97 --- /dev/null +++ b/config/rbac/nonadminrestore_editor_role.yaml @@ -0,0 +1,31 @@ +# permissions for end users to edit nonadminrestores. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: nonadminrestore-editor-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: oadp-nac + app.kubernetes.io/part-of: oadp-nac + app.kubernetes.io/managed-by: kustomize + name: nonadminrestore-editor-role +rules: +- apiGroups: + - nac.oadp.openshift.io + resources: + - nonadminrestores + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - nac.oadp.openshift.io + resources: + - nonadminrestores/status + verbs: + - get diff --git a/config/rbac/nonadminrestore_viewer_role.yaml b/config/rbac/nonadminrestore_viewer_role.yaml new file mode 100644 index 0000000..55b2b6c --- /dev/null +++ b/config/rbac/nonadminrestore_viewer_role.yaml @@ -0,0 +1,27 @@ +# permissions for end users to view nonadminrestores. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: nonadminrestore-viewer-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: oadp-nac + app.kubernetes.io/part-of: oadp-nac + app.kubernetes.io/managed-by: kustomize + name: nonadminrestore-viewer-role +rules: +- apiGroups: + - nac.oadp.openshift.io + resources: + - nonadminrestores + verbs: + - get + - list + - watch +- apiGroups: + - nac.oadp.openshift.io + resources: + - nonadminrestores/status + verbs: + - get diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 3b3efad..58ffd37 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -30,6 +30,32 @@ rules: - get - patch - update +- apiGroups: + - nac.oadp.openshift.io + resources: + - nonadminrestores + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - nac.oadp.openshift.io + resources: + - nonadminrestores/finalizers + verbs: + - update +- apiGroups: + - nac.oadp.openshift.io + resources: + - nonadminrestores/status + verbs: + - get + - patch + - update - apiGroups: - velero.io resources: @@ -41,3 +67,14 @@ rules: - patch - update - watch +- apiGroups: + - velero.io + resources: + - restores + verbs: + - create + - get + - list + - patch + - update + - watch diff --git a/config/samples/kustomization.yaml b/config/samples/kustomization.yaml index 7b31120..f5148bf 100644 --- a/config/samples/kustomization.yaml +++ b/config/samples/kustomization.yaml @@ -1,4 +1,5 @@ ## Append samples of your project ## resources: - nac_v1alpha1_nonadminbackup.yaml +- nac_v1alpha1_nonadminrestore.yaml #+kubebuilder:scaffold:manifestskustomizesamples diff --git a/config/samples/nac_v1alpha1_nonadminrestore.yaml b/config/samples/nac_v1alpha1_nonadminrestore.yaml new file mode 100644 index 0000000..c8e2e2f --- /dev/null +++ b/config/samples/nac_v1alpha1_nonadminrestore.yaml @@ -0,0 +1,13 @@ +apiVersion: nac.oadp.openshift.io/v1alpha1 +kind: NonAdminRestore +metadata: + labels: + app.kubernetes.io/name: nonadminrestore + app.kubernetes.io/instance: nonadminrestore-sample + app.kubernetes.io/part-of: oadp-operator + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/created-by: oadp-operator + name: nonadminrestore-sample +spec: + restoreSpec: + backupName: nonadminbackup-sample diff --git a/docs/architecture.md b/docs/architecture.md index 2a8bfc3..b4a7fb1 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -33,6 +33,12 @@ kubebuilder create api \ --version v1alpha1 \ --kind NonAdminBackup \ --resource --controller +kubebuilder create api \ + --plugins go.kubebuilder.io/v4 \ + --group nac \ + --version v1alpha1 \ + --kind NonAdminRestore \ + --resource --controller make manifests ``` > **NOTE:** The information about plugin and project version, as well as project name, repo and domain, is stored in [PROJECT](../PROJECT) file diff --git a/docs/non_admin_user.md b/docs/non_admin_user.md index 4902541..182b5c0 100644 --- a/docs/non_admin_user.md +++ b/docs/non_admin_user.md @@ -40,28 +40,48 @@ Choose one of the authentication method sections to follow. ``` - Ensure non admin user have appropriate permissions in its namespace, i.e., non admin user have editor roles for the following objects - `nonadminbackups.nac.oadp.openshift.io` + - `nonadminrestores.nac.oadp.openshift.io` For example ```yaml - # config/rbac/nonadminbackup_editor_role.yaml - - apiGroups: - - nac.oadp.openshift.io - resources: - - nonadminbackups - verbs: - - create - - delete - - get - - list - - patch - - update - - watch - - apiGroups: - - nac.oadp.openshift.io - resources: - - nonadminbackups/status - verbs: - - get + # config/rbac/nonadminbackup_editor_role.yaml + - apiGroups: + - nac.oadp.openshift.io + resources: + - nonadminbackups + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - nac.oadp.openshift.io + resources: + - nonadminbackups/status + verbs: + - get + # config/rbac/nonadminrestore_editor_role.yaml + - apiGroups: + - nac.oadp.openshift.io + resources: + - nonadminrestores + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - nac.oadp.openshift.io + resources: + - nonadminrestores/status + verbs: + - get ``` For example, make non admin user have `admin` ClusterRole permissions on its namespace ```sh diff --git a/go.mod b/go.mod index 00d9950..7eb4612 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/onsi/gomega v1.30.0 github.com/stretchr/testify v1.8.4 github.com/vmware-tanzu/velero v1.12.0 + k8s.io/api v0.29.0 k8s.io/apimachinery v0.29.0 k8s.io/client-go v0.29.0 sigs.k8s.io/controller-runtime v0.17.0 @@ -65,7 +66,6 @@ require ( gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/api v0.29.0 // indirect k8s.io/apiextensions-apiserver v0.29.0 // indirect k8s.io/component-base v0.29.0 // indirect k8s.io/klog/v2 v2.110.1 // indirect diff --git a/hack/samples/restores/common.yaml b/hack/samples/restores/common.yaml new file mode 100644 index 0000000..7b47631 --- /dev/null +++ b/hack/samples/restores/common.yaml @@ -0,0 +1,23 @@ +apiVersion: template.openshift.io/v1 +kind: Template +metadata: + name: sample-nonadminrestore +objects: + - apiVersion: nac.oadp.openshift.io/v1alpha1 + kind: NonAdminRestore + metadata: + name: nonadminrestore-sample-${SUFFIX} + namespace: ${NAMESPACE} + spec: + restoreSpec: + backupName: ${NAME} +parameters: + - description: NonAdminRestore suffix + from: '[a-z0-9]{8}' + generate: expression + name: SUFFIX + - description: NonAdminRestore namespace + name: NAMESPACE + value: mysql-persistent + - description: NonAdminBackup name + name: NAME diff --git a/internal/common/constant/constant.go b/internal/common/constant/constant.go index c3748d7..72351f8 100644 --- a/internal/common/constant/constant.go +++ b/internal/common/constant/constant.go @@ -17,7 +17,11 @@ limitations under the License. // Package constant contains all common constants used in the project package constant -import "os" +import ( + "os" + + "github.com/migtools/oadp-non-admin/internal/common/types" +) // Common labels for objects manipulated by the Non Admin Controller // Labels should be used to identify the NAC object @@ -37,6 +41,15 @@ const ( NamespaceEnvVar = "WATCH_NAMESPACE" ) +// Predefined conditions for NonAdminBackup. +// One NonAdminBackup object may have multiple conditions. +// It is more granular knowledge of the NonAdminBackup object and represents the +// array of the conditions through which the NonAdminBackup has or has not passed +const ( + NonAdminConditionAccepted types.NonAdminCondition = "Accepted" + NonAdminConditionQueued types.NonAdminCondition = "Queued" +) + // OadpNamespace is the namespace OADP operator is installed var OadpNamespace = os.Getenv(NamespaceEnvVar) diff --git a/internal/common/function/function.go b/internal/common/function/function.go index 250e58b..1a195c0 100644 --- a/internal/common/function/function.go +++ b/internal/common/function/function.go @@ -29,12 +29,13 @@ import ( velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" apimeta "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/types" + apitypes "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/log" nacv1alpha1 "github.com/migtools/oadp-non-admin/api/v1alpha1" "github.com/migtools/oadp-non-admin/internal/common/constant" + "github.com/migtools/oadp-non-admin/internal/common/types" ) const requiredAnnotationError = "backup does not have the required annotation '%s'" @@ -145,7 +146,7 @@ func GenerateVeleroBackupName(namespace, nabName string) string { } // UpdateNonAdminPhase updates the phase of a NonAdminBackup object with the provided phase. -func UpdateNonAdminPhase(ctx context.Context, r client.Client, logger logr.Logger, nab *nacv1alpha1.NonAdminBackup, phase nacv1alpha1.NonAdminBackupPhase) (bool, error) { +func UpdateNonAdminPhase(ctx context.Context, r client.Client, logger logr.Logger, nab *nacv1alpha1.NonAdminBackup, phase nacv1alpha1.NonAdminPhase) (bool, error) { if nab == nil { return false, errors.New("NonAdminBackup object is nil") } @@ -177,7 +178,7 @@ func UpdateNonAdminPhase(ctx context.Context, r client.Client, logger logr.Logge // based on the provided parameters. It validates the input parameters and ensures // that the condition is set to the desired status only if it differs from the current status. // If the condition is already set to the desired status, no update is performed. -func UpdateNonAdminBackupCondition(ctx context.Context, r client.Client, logger logr.Logger, nab *nacv1alpha1.NonAdminBackup, condition nacv1alpha1.NonAdminCondition, conditionStatus metav1.ConditionStatus, reason string, message string) (bool, error) { +func UpdateNonAdminBackupCondition(ctx context.Context, r client.Client, logger logr.Logger, nab *nacv1alpha1.NonAdminBackup, condition types.NonAdminCondition, conditionStatus metav1.ConditionStatus, reason string, message string) (bool, error) { if nab == nil { return false, errors.New("NonAdminBackup object is nil") } @@ -306,7 +307,7 @@ func GetNonAdminBackupFromVeleroBackup(ctx context.Context, clientInstance clien return nil, fmt.Errorf(requiredAnnotationError, constant.NabOriginNameAnnotation) } - nonAdminBackupKey := types.NamespacedName{ + nonAdminBackupKey := apitypes.NamespacedName{ Namespace: nabOriginNamespace, Name: nabOriginName, } @@ -322,7 +323,7 @@ func GetNonAdminBackupFromVeleroBackup(ctx context.Context, clientInstance clien return nil, fmt.Errorf(requiredAnnotationError, constant.NabOriginUUIDAnnotation) } // Ensure UID matches - if nonAdminBackup.ObjectMeta.UID != types.UID(nabOriginUUID) { + if nonAdminBackup.ObjectMeta.UID != apitypes.UID(nabOriginUUID) { return nil, fmt.Errorf("UID from annotation does not match UID of fetched NonAdminBackup object") } diff --git a/api/v1alpha1/nonadmincontroller_types.go b/internal/common/types/types.go similarity index 56% rename from api/v1alpha1/nonadmincontroller_types.go rename to internal/common/types/types.go index 61c5608..bfdbd43 100644 --- a/api/v1alpha1/nonadmincontroller_types.go +++ b/internal/common/types/types.go @@ -14,17 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ -package v1alpha1 +// Package types contains all common types used in the project +package types -// NonAdminCondition are used for more detailed information supporing NonAdminBackupPhase state. -// +kubebuilder:validation:Enum=Accepted;Queued +// NonAdminCondition are used for more detailed information supporting NonAdminBackupPhase state. type NonAdminCondition string - -// Predefined conditions for NonAdminBackup. -// One NonAdminBackup object may have multiple conditions. -// It is more granular knowledge of the NonAdminBackup object and represents the -// array of the conditions through which the NonAdminBackup has or has not passed -const ( - NonAdminConditionAccepted NonAdminCondition = "Accepted" - NonAdminConditionQueued NonAdminCondition = "Queued" -) diff --git a/internal/controller/nonadminbackup_controller.go b/internal/controller/nonadminbackup_controller.go index d8ac1a9..520fe2c 100644 --- a/internal/controller/nonadminbackup_controller.go +++ b/internal/controller/nonadminbackup_controller.go @@ -128,7 +128,7 @@ func (r *NonAdminBackupReconciler) InitNonAdminBackup(ctx context.Context, logrL // Set initial Phase if nab.Status.Phase == constant.EmptyString { // Phase: New - updatedStatus, errUpdate := function.UpdateNonAdminPhase(ctx, r.Client, logger, nab, nacv1alpha1.NonAdminBackupPhaseNew) + updatedStatus, errUpdate := function.UpdateNonAdminPhase(ctx, r.Client, logger, nab, nacv1alpha1.NonAdminPhaseNew) if errUpdate != nil { logger.Error(errUpdate, "Unable to set NonAdminBackup Phase: New", nameField, nab.Name, constant.NameSpaceString, nab.Namespace) @@ -171,7 +171,7 @@ func (r *NonAdminBackupReconciler) ValidateVeleroBackupSpec(ctx context.Context, } logger.Error(err, errMsg) - updatedStatus, errUpdateStatus := function.UpdateNonAdminPhase(ctx, r.Client, logger, nab, nacv1alpha1.NonAdminBackupPhaseBackingOff) + updatedStatus, errUpdateStatus := function.UpdateNonAdminPhase(ctx, r.Client, logger, nab, nacv1alpha1.NonAdminPhaseBackingOff) if errUpdateStatus != nil { logger.Error(errUpdateStatus, "Unable to set NonAdminBackup Phase: BackingOff", nameField, nab.Name, constant.NameSpaceString, nab.Namespace) return true, false, errUpdateStatus @@ -181,7 +181,7 @@ func (r *NonAdminBackupReconciler) ValidateVeleroBackupSpec(ctx context.Context, } // Continue. VeleroBackup looks fine, setting Accepted condition - updatedCondition, errUpdateCondition := function.UpdateNonAdminBackupCondition(ctx, r.Client, logger, nab, nacv1alpha1.NonAdminConditionAccepted, metav1.ConditionFalse, "InvalidBackupSpec", errMsg) + updatedCondition, errUpdateCondition := function.UpdateNonAdminBackupCondition(ctx, r.Client, logger, nab, constant.NonAdminConditionAccepted, metav1.ConditionFalse, "InvalidBackupSpec", errMsg) if errUpdateCondition != nil { logger.Error(errUpdateCondition, "Unable to set BackupAccepted Condition: False", nameField, nab.Name, constant.NameSpaceString, nab.Namespace) @@ -194,7 +194,7 @@ func (r *NonAdminBackupReconciler) ValidateVeleroBackupSpec(ctx context.Context, return true, false, err } - updatedStatus, errUpdateStatus := function.UpdateNonAdminBackupCondition(ctx, r.Client, logger, nab, nacv1alpha1.NonAdminConditionAccepted, metav1.ConditionTrue, "BackupAccepted", "backup accepted") + updatedStatus, errUpdateStatus := function.UpdateNonAdminBackupCondition(ctx, r.Client, logger, nab, constant.NonAdminConditionAccepted, metav1.ConditionTrue, "BackupAccepted", "backup accepted") if errUpdateStatus != nil { logger.Error(errUpdateStatus, "Unable to set BackupAccepted Condition: True", nameField, nab.Name, constant.NameSpaceString, nab.Namespace) return true, false, errUpdateStatus @@ -291,17 +291,17 @@ func (r *NonAdminBackupReconciler) CreateVeleroBackupSpec(ctx context.Context, l } logger.Info("VeleroBackup successfully created", nameField, veleroBackupName) - _, errUpdate := function.UpdateNonAdminPhase(ctx, r.Client, logger, nab, nacv1alpha1.NonAdminBackupPhaseCreated) + _, errUpdate := function.UpdateNonAdminPhase(ctx, r.Client, logger, nab, nacv1alpha1.NonAdminPhaseCreated) if errUpdate != nil { logger.Error(errUpdate, "Unable to set NonAdminBackup Phase: Created", nameField, nab.Name, constant.NameSpaceString, nab.Namespace) return true, false, errUpdate } - _, errUpdate = function.UpdateNonAdminBackupCondition(ctx, r.Client, logger, nab, nacv1alpha1.NonAdminConditionAccepted, metav1.ConditionTrue, "Validated", "Valid Backup config") + _, errUpdate = function.UpdateNonAdminBackupCondition(ctx, r.Client, logger, nab, constant.NonAdminConditionAccepted, metav1.ConditionTrue, "Validated", "Valid Backup config") if errUpdate != nil { logger.Error(errUpdate, "Unable to set BackupAccepted Condition: True", nameField, nab.Name, constant.NameSpaceString, nab.Namespace) return true, false, errUpdate } - _, errUpdate = function.UpdateNonAdminBackupCondition(ctx, r.Client, logger, nab, nacv1alpha1.NonAdminConditionQueued, metav1.ConditionTrue, "BackupScheduled", "Created Velero Backup object") + _, errUpdate = function.UpdateNonAdminBackupCondition(ctx, r.Client, logger, nab, constant.NonAdminConditionQueued, metav1.ConditionTrue, "BackupScheduled", "Created Velero Backup object") if errUpdate != nil { logger.Error(errUpdate, "Unable to set BackupQueued Condition: True", nameField, nab.Name, constant.NameSpaceString, nab.Namespace) return true, false, errUpdate diff --git a/internal/controller/nonadminrestore_controller.go b/internal/controller/nonadminrestore_controller.go new file mode 100644 index 0000000..b254c5b --- /dev/null +++ b/internal/controller/nonadminrestore_controller.go @@ -0,0 +1,120 @@ +/* +Copyright 2024. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controller + +import ( + "context" + "fmt" + "os" + + "github.com/go-logr/logr" + velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" + + nacv1alpha1 "github.com/migtools/oadp-non-admin/api/v1alpha1" + "github.com/migtools/oadp-non-admin/internal/common/constant" +) + +// NonAdminRestoreReconciler reconciles a NonAdminRestore object +type NonAdminRestoreReconciler struct { + client.Client + Scheme *runtime.Scheme + Logger logr.Logger +} + +// +kubebuilder:rbac:groups=nac.oadp.openshift.io,resources=nonadminrestores,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=nac.oadp.openshift.io,resources=nonadminrestores/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=nac.oadp.openshift.io,resources=nonadminrestores/finalizers,verbs=update + +// +kubebuilder:rbac:groups=velero.io,resources=restores,verbs=get;list;watch;create;update;patch + +// Reconcile is part of the main kubernetes reconciliation loop which aims to +// move the current state of the NonAdminRestore to the desired state. +func (r *NonAdminRestoreReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + r.Logger = log.FromContext(ctx) + logger := r.Logger.WithValues("NonAdminRestore", req.NamespacedName) + + logger.Info("TODO") + + nonAdminRestore := nacv1alpha1.NonAdminRestore{} + err := r.Get(ctx, req.NamespacedName, &nonAdminRestore) + if err != nil { + return ctrl.Result{}, err + } + + err = r.validateSpec(ctx, req, nonAdminRestore.Spec) + if err != nil { + return ctrl.Result{}, err + } + + // TODO try to create Velero Restore + + // TODO update status of NonAdminRestore as Velero Restore progresses + + return ctrl.Result{}, nil +} + +// TODO remove functions params +func (r *NonAdminRestoreReconciler) validateSpec(ctx context.Context, req ctrl.Request, objectSpec nacv1alpha1.NonAdminRestoreSpec) error { + if len(objectSpec.RestoreSpec.ScheduleName) > 0 { + return fmt.Errorf("spec.restoreSpec.scheduleName field is not allowed in NonAdminRestore") + } + + // TODO nonAdminRestore respect restricted fields + + nonAdminBackupName := objectSpec.RestoreSpec.BackupName + nonAdminBackup := &nacv1alpha1.NonAdminBackup{} + err := r.Get(ctx, types.NamespacedName{Namespace: req.Namespace, Name: nonAdminBackupName}, nonAdminBackup) + if err != nil { + if errors.IsNotFound(err) { + // TODO add this error message to NonAdminRestore status + return fmt.Errorf("invalid spec.restoreSpec.backupName: NonAdminBackup '%s' does not exist in namespace %s", nonAdminBackupName, req.Namespace) + } + return err + } + + // TODO move this following to another function, it does not check spec + // TODO create get function in common :question: + oadpNamespace := os.Getenv(constant.NamespaceEnvVar) + + veleroBackupName := nonAdminBackup.Status.VeleroBackupName + if len(veleroBackupName) == 0 { + return fmt.Errorf("NonAdminBackup '%s' does not reference Velero Backup name", nonAdminBackupName) + } + veleroBackup := &velerov1api.Backup{} + err = r.Get(ctx, types.NamespacedName{Namespace: oadpNamespace, Name: veleroBackupName}, veleroBackup) + if err != nil { + if errors.IsNotFound(err) { + // TODO add this error message to NonAdminRestore status + return fmt.Errorf("related Velero backup '%s' for NonAdminBackup '%s' does not exist in OADP namespace %s", veleroBackupName, nonAdminBackupName, oadpNamespace) + } + } + + return nil +} + +// SetupWithManager sets up the NonAdminRestore controller with the Manager. +func (r *NonAdminRestoreReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&nacv1alpha1.NonAdminRestore{}). + Complete(r) +} diff --git a/internal/controller/nonadminrestore_controller_test.go b/internal/controller/nonadminrestore_controller_test.go new file mode 100644 index 0000000..0f7474a --- /dev/null +++ b/internal/controller/nonadminrestore_controller_test.go @@ -0,0 +1,140 @@ +/* +Copyright 2024. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controller + +import ( + "context" + "os" + + "github.com/onsi/ginkgo/v2" + "github.com/onsi/gomega" + "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + nacv1alpha1 "github.com/migtools/oadp-non-admin/api/v1alpha1" + "github.com/migtools/oadp-non-admin/internal/common/constant" +) + +type nonAdminRestoreReconcileScenario struct { + restoreSpec *v1.RestoreSpec + namespace string + nonAdminRestore string + errMessage string +} + +func createTestNonAdminRestore(name string, namespace string, restoreSpec v1.RestoreSpec) *nacv1alpha1.NonAdminRestore { + return &nacv1alpha1.NonAdminRestore{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + Spec: nacv1alpha1.NonAdminRestoreSpec{ + RestoreSpec: &restoreSpec, + }, + } +} + +var _ = ginkgo.Describe("Test NonAdminRestore Reconcile function", func() { + var ( + ctx = context.Background() + currentTestScenario nonAdminRestoreReconcileScenario + updateTestScenario = func(scenario nonAdminRestoreReconcileScenario) { + currentTestScenario = scenario + } + ) + + ginkgo.AfterEach(func() { + gomega.Expect(os.Unsetenv(constant.NamespaceEnvVar)).To(gomega.Succeed()) + + nonAdminRestore := &nacv1alpha1.NonAdminRestore{} + if k8sClient.Get( + ctx, + types.NamespacedName{ + Name: currentTestScenario.nonAdminRestore, + Namespace: currentTestScenario.namespace, + }, + nonAdminRestore, + ) == nil { + gomega.Expect(k8sClient.Delete(ctx, nonAdminRestore)).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 false", + func(scenario nonAdminRestoreReconcileScenario) { + updateTestScenario(scenario) + + namespace := &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: scenario.namespace, + }, + } + gomega.Expect(k8sClient.Create(ctx, namespace)).To(gomega.Succeed()) + + nonAdminRestore := createTestNonAdminRestore(scenario.nonAdminRestore, scenario.namespace, *scenario.restoreSpec) + gomega.Expect(k8sClient.Create(ctx, nonAdminRestore)).To(gomega.Succeed()) + + gomega.Expect(os.Setenv(constant.NamespaceEnvVar, "envVarValue")).To(gomega.Succeed()) + r := &NonAdminRestoreReconciler{ + Client: k8sClient, + Scheme: testEnv.Scheme, + } + result, err := r.Reconcile( + context.Background(), + reconcile.Request{NamespacedName: types.NamespacedName{ + Namespace: scenario.namespace, + Name: scenario.nonAdminRestore, + }}, + ) + + if len(scenario.errMessage) == 0 { + gomega.Expect(result).To(gomega.Equal(reconcile.Result{Requeue: false, RequeueAfter: 0})) + gomega.Expect(err).To(gomega.Not(gomega.HaveOccurred())) + } else { + gomega.Expect(result).To(gomega.Equal(reconcile.Result{Requeue: false, RequeueAfter: 0})) + gomega.Expect(err).To(gomega.HaveOccurred()) + gomega.Expect(err.Error()).To(gomega.ContainSubstring(scenario.errMessage)) + } + }, + ginkgo.Entry("Should NOT accept scheduleName", nonAdminRestoreReconcileScenario{ + namespace: "test-nonadminrestore-reconcile-1", + nonAdminRestore: "test-nonadminrestore-reconcile-1-cr", + errMessage: "scheduleName", + restoreSpec: &v1.RestoreSpec{ + ScheduleName: "wrong", + }, + }), + ginkgo.Entry("Should NOT accept non existing NonAdminBackup", nonAdminRestoreReconcileScenario{ + namespace: "test-nonadminrestore-reconcile-2", + nonAdminRestore: "test-nonadminrestore-reconcile-2-cr", + errMessage: "backupName", + restoreSpec: &v1.RestoreSpec{ + BackupName: "do-not-exist", + }, + }), + // TODO Should NOT accept non existing related Velero Backup + ) +}) diff --git a/internal/controller/suite_test.go b/internal/controller/suite_test.go index e1510ba..54bfa6b 100644 --- a/internal/controller/suite_test.go +++ b/internal/controller/suite_test.go @@ -61,7 +61,7 @@ var _ = ginkgov2.BeforeSuite(func() { // Note that you must have the required binaries setup under the bin directory to perform // the tests directly. When we run make test it will be setup and used automatically. BinaryAssetsDirectory: filepath.Join("..", "..", "bin", "k8s", - fmt.Sprintf("1.29.0-%s-%s", runtime.GOOS, runtime.GOARCH)), + fmt.Sprintf("1.28.3-%s-%s", runtime.GOOS, runtime.GOARCH)), } var err error @@ -73,6 +73,9 @@ var _ = ginkgov2.BeforeSuite(func() { err = nacv1alpha1.AddToScheme(scheme.Scheme) gomega.Expect(err).NotTo(gomega.HaveOccurred()) + err = nacv1alpha1.AddToScheme(scheme.Scheme) + gomega.Expect(err).NotTo(gomega.HaveOccurred()) + // +kubebuilder:scaffold:scheme k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) diff --git a/internal/predicate/nonadminbackup_predicate.go b/internal/predicate/nonadminbackup_predicate.go index d3309a3..0c5fd57 100644 --- a/internal/predicate/nonadminbackup_predicate.go +++ b/internal/predicate/nonadminbackup_predicate.go @@ -44,7 +44,7 @@ func (NonAdminBackupPredicate) Create(ctx context.Context, evt event.CreateEvent logger := getNonAdminBackupPredicateLogger(ctx, name, nameSpace) logger.V(1).Info("NonAdminBackupPredicate: Received Create event") if nonAdminBackup, ok := evt.Object.(*nacv1alpha1.NonAdminBackup); ok { - if nonAdminBackup.Status.Phase == constant.EmptyString || nonAdminBackup.Status.Phase == nacv1alpha1.NonAdminBackupPhaseNew { + if nonAdminBackup.Status.Phase == constant.EmptyString || nonAdminBackup.Status.Phase == nacv1alpha1.NonAdminPhaseNew { logger.V(1).Info("NonAdminBackupPredicate: Accepted Create event") return true } @@ -75,7 +75,7 @@ func (NonAdminBackupPredicate) Update(ctx context.Context, evt event.UpdateEvent if oldPhase == constant.EmptyString && newPhase != constant.EmptyString { logger.V(1).Info("NonAdminBsackupPredicate: Accepted Update event - phase change") return true - } else if oldPhase == nacv1alpha1.NonAdminBackupPhaseNew && newPhase == nacv1alpha1.NonAdminBackupPhaseCreated { + } else if oldPhase == nacv1alpha1.NonAdminPhaseNew && newPhase == nacv1alpha1.NonAdminPhaseCreated { logger.V(1).Info("NonAdminBackupPredicate: Accepted Update event - phase created") return true }