diff --git a/Makefile b/Makefile index 088620e954..066c377e92 100644 --- a/Makefile +++ b/Makefile @@ -65,8 +65,9 @@ ifeq ($(CLUSTER_TYPE), ibmcloud) VELERO_PLUGIN = aws endif +# Kubernetes version from OpenShift 4.15.x https://openshift-release.apps.ci.l2s4.p1.openshiftapps.com/#4-stable # ENVTEST_K8S_VERSION refers to the version of kubebuilder assets to be downloaded by envtest binary. -ENVTEST_K8S_VERSION = 1.21 +ENVTEST_K8S_VERSION = 1.28 # VERSION defines the project version for the bundle. # Update this value when you upgrade the version of your project. @@ -164,7 +165,7 @@ vet: ## Run go vet against code. go vet -mod=mod ./... ENVTEST := $(shell pwd)/bin/setup-envtest -ENVTESTPATH = $(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) -p path) +ENVTESTPATH = $(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(shell pwd)/bin -p path) ifeq ($(shell $(ENVTEST) list | grep $(ENVTEST_K8S_VERSION)),) ENVTESTPATH = $(shell $(ENVTEST) --arch=amd64 use $(ENVTEST_K8S_VERSION) -p path) endif @@ -310,7 +311,7 @@ operator-sdk: bundle: manifests kustomize operator-sdk ## Generate bundle manifests and metadata, then validate generated files. GOFLAGS="-mod=mod" $(OPERATOR_SDK) generate kustomize manifests -q cd config/manager && GOFLAGS="-mod=mod" $(KUSTOMIZE) edit set image controller=$(IMG) - GOFLAGS="-mod=mod" $(KUSTOMIZE) build config/manifests | GOFLAGS="-mod=mod" $(OPERATOR_SDK) generate bundle -q --extra-service-accounts "velero" --overwrite --version $(VERSION) $(BUNDLE_METADATA_OPTS) + GOFLAGS="-mod=mod" $(KUSTOMIZE) build config/manifests | GOFLAGS="-mod=mod" $(OPERATOR_SDK) generate bundle -q --extra-service-accounts "velero,non-admin-controller" --overwrite --version $(VERSION) $(BUNDLE_METADATA_OPTS) @make nullables # Copy updated bundle.Dockerfile to CI's Dockerfile.bundle # TODO: update CI to use generated one @@ -369,18 +370,18 @@ GIT_REV:=$(shell git rev-parse --short HEAD) deploy-olm: THIS_OPERATOR_IMAGE?=ttl.sh/oadp-operator-$(GIT_REV):1h # Set target specific variable deploy-olm: THIS_BUNDLE_IMAGE?=ttl.sh/oadp-operator-bundle-$(GIT_REV):1h # Set target specific variable deploy-olm: DEPLOY_TMP:=$(shell mktemp -d)/ # Set target specific variable -deploy-olm: operator-sdk undeploy-olm ## Build current branch operator image, bundle image, push and install via OLM +deploy-olm: undeploy-olm ## Build current branch operator image, bundle image, push and install via OLM @echo "DEPLOY_TMP: $(DEPLOY_TMP)" # build and push operator and bundle image # use $(OPERATOR_SDK) to install bundle to authenticated cluster cp -r . $(DEPLOY_TMP) && cd $(DEPLOY_TMP) && \ IMG=$(THIS_OPERATOR_IMAGE) BUNDLE_IMG=$(THIS_BUNDLE_IMAGE) \ make docker-build docker-push bundle bundle-build bundle-push; \ - rm -rf $(DEPLOY_TMP) + chmod -R 777 $(DEPLOY_TMP) && rm -rf $(DEPLOY_TMP) $(OPERATOR_SDK) run bundle $(THIS_BUNDLE_IMAGE) --namespace $(OADP_TEST_NAMESPACE) .PHONY: undeploy-olm -undeploy-olm: login-required ## Uninstall current branch operator via OLM +undeploy-olm: login-required operator-sdk ## Uninstall current branch operator via OLM $(OC_CLI) whoami # Check if logged in $(OC_CLI) create namespace $(OADP_TEST_NAMESPACE) || true $(OPERATOR_SDK) cleanup oadp-operator --namespace $(OADP_TEST_NAMESPACE) @@ -544,3 +545,24 @@ lint: golangci-lint ## Run Go linters checks against all project's Go files. .PHONY: lint-fix lint-fix: golangci-lint ## Fix Go linters issues. $(GOLANGCI_LINT) run --fix + +.PHONY: update-non-admin-manifests +update-non-admin-manifests: NON_ADMIN_CONTROLLER_IMG?=quay.io/konveyor/oadp-non-admin:latest +update-non-admin-manifests: ## Update Non Admin Controller (NAC) manifests shipped with OADP, from NON_ADMIN_CONTROLLER_PATH +ifeq ($(NON_ADMIN_CONTROLLER_PATH),) + $(error You must set NON_ADMIN_CONTROLLER_PATH to run this command) +endif + @for file_name in $(shell ls $(NON_ADMIN_CONTROLLER_PATH)/config/crd/bases);do \ + cp $(NON_ADMIN_CONTROLLER_PATH)/config/crd/bases/$$file_name $(shell pwd)/config/crd/bases/$$file_name && \ + grep -q "\- bases/$$file_name" $(shell pwd)/config/crd/kustomization.yaml || \ + sed -i "s%resources:%resources:\n- bases/$$file_name%" $(shell pwd)/config/crd/kustomization.yaml;done + @sed -i "$(shell grep -Inr 'RELATED_IMAGE_NON_ADMIN_CONTROLLER' $(shell pwd)/config/manager/manager.yaml | awk -F':' '{print $$1+1}')s%.*% value: $(NON_ADMIN_CONTROLLER_IMG)%" $(shell pwd)/config/manager/manager.yaml + @mkdir -p $(shell pwd)/config/non-admin-controller_rbac + @for file_name in $(shell grep -I '^\-' $(NON_ADMIN_CONTROLLER_PATH)/config/rbac/kustomization.yaml | awk -F'- ' '{print $$2}');do \ + cp $(NON_ADMIN_CONTROLLER_PATH)/config/rbac/$$file_name $(shell pwd)/config/non-admin-controller_rbac/$$file_name;done + @cp $(NON_ADMIN_CONTROLLER_PATH)/config/rbac/kustomization.yaml $(shell pwd)/config/non-admin-controller_rbac/kustomization.yaml + @for file_name in $(shell grep -I '^\-' $(NON_ADMIN_CONTROLLER_PATH)/config/samples/kustomization.yaml | awk -F'- ' '{print $$2}');do \ + cp $(NON_ADMIN_CONTROLLER_PATH)/config/samples/$$file_name $(shell pwd)/config/samples/$$file_name && \ + grep -q "\- $$file_name" $(shell pwd)/config/samples/kustomization.yaml || \ + sed -i "s%resources:%resources:\n- $$file_name%" $(shell pwd)/config/samples/kustomization.yaml;done + @make bundle diff --git a/api/v1alpha1/oadp_types.go b/api/v1alpha1/oadp_types.go index 1a982ba639..7b5cb48107 100644 --- a/api/v1alpha1/oadp_types.go +++ b/api/v1alpha1/oadp_types.go @@ -62,6 +62,7 @@ const GCPPluginImageKey UnsupportedImageKey = "gcpPluginImageFqin" const CSIPluginImageKey UnsupportedImageKey = "csiPluginImageFqin" const ResticRestoreImageKey UnsupportedImageKey = "resticRestoreImageFqin" const KubeVirtPluginImageKey UnsupportedImageKey = "kubevirtPluginImageFqin" +const NonAdminControllerImageKey UnsupportedImageKey = "nonAdminControllerImageFqin" const OperatorTypeKey UnsupportedImageKey = "operator-type" const OperatorTypeMTC = "mtc" @@ -232,8 +233,18 @@ type SnapshotLocation struct { Velero *velero.VolumeSnapshotLocationSpec `json:"velero"` } +// NonAdmin defines the configuration for the non admin feature +type NonAdmin struct { + // Enables non admin feature, by default is disabled + // +optional + Enable *bool `json:"enable,omitempty"` +} + // Features defines the configuration for the DPA to enable the tech preview features -type Features struct{} +type Features struct { + // +optional + NonAdmin *NonAdmin `json:"nonAdmin"` +} // DataProtectionApplicationSpec defines the desired state of Velero type DataProtectionApplicationSpec struct { @@ -253,6 +264,7 @@ type DataProtectionApplicationSpec struct { // - csiPluginImageFqin // - resticRestoreImageFqin // - kubevirtPluginImageFqin + // - nonAdminControllerImageFqin // - operator-type // +optional UnsupportedOverrides map[UnsupportedImageKey]string `json:"unsupportedOverrides,omitempty"` diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 1d76421dba..2affcefb89 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -347,7 +347,7 @@ func (in *DataProtectionApplicationSpec) DeepCopyInto(out *DataProtectionApplica if in.Features != nil { in, out := &in.Features, &out.Features *out = new(Features) - **out = **in + (*in).DeepCopyInto(*out) } } @@ -386,6 +386,11 @@ func (in *DataProtectionApplicationStatus) DeepCopy() *DataProtectionApplication // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Features) DeepCopyInto(out *Features) { *out = *in + if in.NonAdmin != nil { + in, out := &in.NonAdmin, &out.NonAdmin + *out = new(NonAdmin) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Features. @@ -444,6 +449,26 @@ func (in *NodeAgentConfig) DeepCopy() *NodeAgentConfig { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NonAdmin) DeepCopyInto(out *NonAdmin) { + *out = *in + if in.Enable != nil { + in, out := &in.Enable, &out.Enable + *out = new(bool) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NonAdmin. +func (in *NonAdmin) DeepCopy() *NonAdmin { + if in == nil { + return nil + } + out := new(NonAdmin) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *PodConfig) DeepCopyInto(out *PodConfig) { *out = *in diff --git a/bundle/manifests/nac.oadp.openshift.io_nonadminbackups.yaml b/bundle/manifests/nac.oadp.openshift.io_nonadminbackups.yaml new file mode 100644 index 0000000000..8fe8599691 --- /dev/null +++ b/bundle/manifests/nac.oadp.openshift.io_nonadminbackups.yaml @@ -0,0 +1,733 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.14.0 + creationTimestamp: null + name: nonadminbackups.nac.oadp.openshift.io +spec: + group: nac.oadp.openshift.io + names: + kind: NonAdminBackup + listKind: NonAdminBackupList + plural: nonadminbackups + singular: nonadminbackup + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: NonAdminBackup is the Schema for the nonadminbackups 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: NonAdminBackupSpec defines the desired state of NonAdminBackup + properties: + backupSpec: + description: BackupSpec defines the specification for a Velero backup. + properties: + csiSnapshotTimeout: + description: |- + CSISnapshotTimeout specifies the time used to wait for CSI VolumeSnapshot status turns to + ReadyToUse during creation, before returning error as timeout. + The default value is 10 minute. + type: string + datamover: + description: |- + DataMover specifies the data mover to be used by the backup. + If DataMover is "" or "velero", the built-in data mover will be used. + type: string + defaultVolumesToFsBackup: + description: |- + DefaultVolumesToFsBackup specifies whether pod volume file system backup should be used + for all volumes by default. + nullable: true + type: boolean + defaultVolumesToRestic: + description: |- + DefaultVolumesToRestic specifies whether restic should be used to take a + backup of all pod volumes by default. + + + Deprecated: this field is no longer used and will be removed entirely in future. Use DefaultVolumesToFsBackup instead. + nullable: true + type: boolean + excludedClusterScopedResources: + description: |- + ExcludedClusterScopedResources is a slice of cluster-scoped + resource type names to exclude from the backup. + If set to "*", all cluster-scoped resource types are excluded. + The default value is empty. + items: + type: string + nullable: true + type: array + excludedNamespaceScopedResources: + description: |- + ExcludedNamespaceScopedResources is a slice of namespace-scoped + resource type names to exclude from the backup. + If set to "*", all namespace-scoped resource types are excluded. + The default value is empty. + items: + type: string + nullable: true + type: array + excludedNamespaces: + description: |- + ExcludedNamespaces contains a list of namespaces that are not + included in the backup. + items: + type: string + nullable: true + type: array + excludedResources: + description: |- + ExcludedResources is a slice of resource names that are not + included in the backup. + items: + type: string + nullable: true + type: array + hooks: + description: Hooks represent custom behaviors that should be executed + at different phases of the backup. + properties: + resources: + description: Resources are hooks that should be executed when + backing up individual instances of a resource. + items: + description: |- + BackupResourceHookSpec defines one or more BackupResourceHooks 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 + post: + description: |- + PostHooks is a list of BackupResourceHooks to execute after storing the item in the backup. + These are executed after all "additional items" from item actions are processed. + items: + description: BackupResourceHook defines a hook for + a resource. + properties: + exec: + description: Exec defines an exec hook. + properties: + command: + description: Command is the command and arguments + to execute. + 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 + onError: + description: OnError specifies how Velero + should behave if it encounters an error + executing this hook. + enum: + - Continue + - Fail + type: string + timeout: + description: |- + Timeout defines the maximum amount of time Velero should wait for the hook to complete before + considering the execution a failure. + type: string + required: + - command + type: object + required: + - exec + type: object + type: array + pre: + description: |- + PreHooks is a list of BackupResourceHooks to execute prior to storing the item in the backup. + These are executed before any "additional items" from item actions are processed. + items: + description: BackupResourceHook defines a hook for + a resource. + properties: + exec: + description: Exec defines an exec hook. + properties: + command: + description: Command is the command and arguments + to execute. + 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 + onError: + description: OnError specifies how Velero + should behave if it encounters an error + executing this hook. + enum: + - Continue + - Fail + type: string + timeout: + description: |- + Timeout defines the maximum amount of time Velero should wait for the hook to complete before + considering the execution a failure. + type: string + required: + - command + type: object + required: + - exec + type: object + type: array + required: + - name + type: object + nullable: true + type: array + type: object + includeClusterResources: + description: |- + IncludeClusterResources specifies whether cluster-scoped resources + should be included for consideration in the backup. + nullable: true + type: boolean + includedClusterScopedResources: + description: |- + IncludedClusterScopedResources is a slice of cluster-scoped + resource type names to include in the backup. + If set to "*", all cluster-scoped resource types are included. + The default value is empty, which means only related + cluster-scoped resources are included. + items: + type: string + nullable: true + type: array + includedNamespaceScopedResources: + description: |- + IncludedNamespaceScopedResources is a slice of namespace-scoped + resource type names to include in the backup. + The default value is "*". + items: + type: string + nullable: true + type: array + 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 backup. If empty, all resources are included. + items: + type: string + nullable: true + type: array + itemOperationTimeout: + description: |- + ItemOperationTimeout specifies the time used to wait for asynchronous BackupItemAction operations + The default value is 1 hour. + type: string + labelSelector: + description: |- + LabelSelector is a metav1.LabelSelector to filter with + when adding individual objects to 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 + metadata: + properties: + labels: + additionalProperties: + type: string + type: object + type: object + orLabelSelectors: + description: |- + OrLabelSelectors is list of metav1.LabelSelector to filter with + when adding individual objects to the backup. If multiple provided + they will be joined by the OR operator. LabelSelector as well as + OrLabelSelectors cannot co-exist in backup 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 + orderedResources: + additionalProperties: + type: string + description: |- + OrderedResources specifies the backup order of resources of specific Kind. + The map key is the resource name and value is a list of object names separated by commas. + Each resource name has format "namespace/objectname". For cluster resources, simply use "objectname". + nullable: true + type: object + resourcePolicy: + description: ResourcePolicy specifies the referenced resource + policies that backup should follow + 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 + snapshotMoveData: + description: SnapshotMoveData specifies whether snapshot data + should be moved + nullable: true + type: boolean + snapshotVolumes: + description: |- + SnapshotVolumes specifies whether to take snapshots + of any PV's referenced in the set of objects included + in the Backup. + nullable: true + type: boolean + storageLocation: + description: StorageLocation is a string containing the name of + a BackupStorageLocation where the backup should be stored. + type: string + ttl: + description: |- + TTL is a time.Duration-parseable string describing how long + the Backup should be retained for. + type: string + volumeSnapshotLocations: + description: VolumeSnapshotLocations is a list containing names + of VolumeSnapshotLocations associated with this backup. + items: + type: string + type: array + type: object + backupStatus: + description: BackupStatus captures the current status of a Velero + backup. + properties: + backupItemOperationsAttempted: + description: |- + BackupItemOperationsAttempted is the total number of attempted + async BackupItemAction operations for this backup. + type: integer + backupItemOperationsCompleted: + description: |- + BackupItemOperationsCompleted is the total number of successfully completed + async BackupItemAction operations for this backup. + type: integer + backupItemOperationsFailed: + description: |- + BackupItemOperationsFailed is the total number of async + BackupItemAction operations for this backup which ended with an error. + type: integer + completionTimestamp: + description: |- + CompletionTimestamp records the time a backup was completed. + Completion time is recorded even on failed backups. + Completion time is recorded before uploading the backup object. + The server's time is used for CompletionTimestamps + format: date-time + nullable: true + type: string + csiVolumeSnapshotsAttempted: + description: |- + CSIVolumeSnapshotsAttempted is the total number of attempted + CSI VolumeSnapshots for this backup. + type: integer + csiVolumeSnapshotsCompleted: + description: |- + CSIVolumeSnapshotsCompleted is the total number of successfully + completed CSI VolumeSnapshots for this backup. + type: integer + errors: + description: |- + Errors is a count of all error messages that were generated during + execution of the backup. The actual errors are in the backup's log + file in object storage. + type: integer + expiration: + description: Expiration is when this Backup is eligible for garbage-collection. + format: date-time + nullable: true + type: string + failureReason: + description: FailureReason is an error that caused the entire + backup to fail. + type: string + formatVersion: + description: FormatVersion is the backup format version, including + major, minor, and patch version. + type: string + phase: + description: Phase is the current state of the Backup. + enum: + - New + - FailedValidation + - InProgress + - WaitingForPluginOperations + - WaitingForPluginOperationsPartiallyFailed + - Finalizing + - FinalizingPartiallyFailed + - Completed + - PartiallyFailed + - Failed + - Deleting + type: string + progress: + description: |- + Progress contains information about the backup's execution progress. Note + that this information is best-effort only -- if Velero fails to update it + during a backup for any reason, it may be inaccurate/stale. + nullable: true + properties: + itemsBackedUp: + description: |- + ItemsBackedUp is the number of items that have actually been written to the + backup tarball so far. + type: integer + totalItems: + description: |- + TotalItems is the total number of items to be backed up. This number may change + throughout the execution of the backup due to plugins that return additional related + items to back up, the velero.io/exclude-from-backup label, and various other + filters that happen as items are processed. + type: integer + type: object + startTimestamp: + description: |- + StartTimestamp records the time a backup was started. + Separate from CreationTimestamp, since that value changes + on restores. + 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 + version: + description: |- + Version is the backup format major version. + Deprecated: Please see FormatVersion + type: integer + volumeSnapshotsAttempted: + description: |- + VolumeSnapshotsAttempted is the total number of attempted + volume snapshots for this backup. + type: integer + volumeSnapshotsCompleted: + description: |- + VolumeSnapshotsCompleted is the total number of successfully + completed volume snapshots for this backup. + type: integer + warnings: + description: |- + Warnings is a count of all warning messages that were generated during + execution of the backup. The actual warnings are in the backup's log + file in object storage. + type: integer + type: object + logLevel: + description: NonAdminBackup log level (use debug for the most logging, + leave unset for default) + enum: + - trace + - debug + - info + - warning + - error + - fatal + - panic + type: string + type: object + status: + description: NonAdminBackupStatus defines the observed state of NonAdminBackup + 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 + type: object + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: null + storedVersions: null diff --git a/bundle/manifests/oadp-operator.clusterserviceversion.yaml b/bundle/manifests/oadp-operator.clusterserviceversion.yaml index f7f33fda50..40c853471a 100644 --- a/bundle/manifests/oadp-operator.clusterserviceversion.yaml +++ b/bundle/manifests/oadp-operator.clusterserviceversion.yaml @@ -4,6 +4,23 @@ metadata: annotations: alm-examples: |- [ + { + "apiVersion": "nac.oadp.openshift.io/v1alpha1", + "kind": "NonAdminBackup", + "metadata": { + "labels": { + "app.kubernetes.io/created-by": "oadp-operator", + "app.kubernetes.io/instance": "nonadminbackup-sample", + "app.kubernetes.io/managed-by": "kustomize", + "app.kubernetes.io/name": "nonadminbackup", + "app.kubernetes.io/part-of": "oadp-operator" + }, + "name": "nonadminbackup-sample" + }, + "spec": { + "backupSpec": {} + } + }, { "apiVersion": "oadp.openshift.io/v1alpha1", "kind": "DataProtectionApplication", @@ -400,6 +417,11 @@ spec: displayName: Expiration path: expiration version: v1 + - description: NonAdminBackup is the Schema for the nonadminbackups API + displayName: Non Admin Backup + kind: NonAdminBackup + name: nonadminbackups.nac.oadp.openshift.io + version: v1alpha1 - description: A velero pod volume backup is a restic backup of persistent volumes attached to a running pod. displayName: PodVolumeBackup @@ -580,6 +602,45 @@ spec: install: spec: clusterPermissions: + - rules: + - apiGroups: + - nac.oadp.openshift.io + resources: + - nonadminbackups + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - nac.oadp.openshift.io + resources: + - nonadminbackups/finalizers + verbs: + - update + - apiGroups: + - nac.oadp.openshift.io + resources: + - nonadminbackups/status + verbs: + - get + - patch + - update + - apiGroups: + - velero.io + resources: + - backups + verbs: + - create + - get + - list + - patch + - update + - watch + serviceAccountName: non-admin-controller - rules: - apiGroups: - config.openshift.io @@ -861,6 +922,8 @@ spec: value: quay.io/konveyor/kubevirt-velero-plugin:v0.2.0 - name: RELATED_IMAGE_MUSTGATHER value: registry.redhat.io/oadp/oadp-mustgather-rhel8:v1.2 + - name: RELATED_IMAGE_NON_ADMIN_CONTROLLER + value: quay.io/konveyor/oadp-non-admin:latest image: quay.io/konveyor/oadp-operator:latest imagePullPolicy: Always livenessProbe: @@ -1012,4 +1075,6 @@ spec: name: kubevirt-velero-plugin - image: registry.redhat.io/oadp/oadp-mustgather-rhel8:v1.2 name: mustgather + - image: quay.io/konveyor/oadp-non-admin:latest + name: non-admin-controller version: 99.0.0 diff --git a/bundle/manifests/oadp.openshift.io_dataprotectionapplications.yaml b/bundle/manifests/oadp.openshift.io_dataprotectionapplications.yaml index 9515484b5d..4038abeee3 100644 --- a/bundle/manifests/oadp.openshift.io_dataprotectionapplications.yaml +++ b/bundle/manifests/oadp.openshift.io_dataprotectionapplications.yaml @@ -831,6 +831,14 @@ spec: type: object features: description: features defines the configuration for the DPA to enable the OADP tech preview features + properties: + nonAdmin: + description: NonAdmin defines the configuration for the non admin feature + properties: + enable: + description: Enables non admin feature, by default is disabled + type: boolean + type: object type: object podAnnotations: additionalProperties: @@ -907,7 +915,7 @@ spec: unsupportedOverrides: additionalProperties: type: string - description: 'unsupportedOverrides can be used to override images used in deployments. Available keys are: - veleroImageFqin - awsPluginImageFqin - openshiftPluginImageFqin - azurePluginImageFqin - gcpPluginImageFqin - csiPluginImageFqin - resticRestoreImageFqin - kubevirtPluginImageFqin - operator-type' + description: 'unsupportedOverrides can be used to override images used in deployments. Available keys are: - veleroImageFqin - awsPluginImageFqin - openshiftPluginImageFqin - azurePluginImageFqin - gcpPluginImageFqin - csiPluginImageFqin - resticRestoreImageFqin - kubevirtPluginImageFqin - nonAdminControllerImageFqin - operator-type' type: object required: - configuration diff --git a/config/crd/bases/nac.oadp.openshift.io_nonadminbackups.yaml b/config/crd/bases/nac.oadp.openshift.io_nonadminbackups.yaml new file mode 100644 index 0000000000..389c25d363 --- /dev/null +++ b/config/crd/bases/nac.oadp.openshift.io_nonadminbackups.yaml @@ -0,0 +1,727 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.14.0 + name: nonadminbackups.nac.oadp.openshift.io +spec: + group: nac.oadp.openshift.io + names: + kind: NonAdminBackup + listKind: NonAdminBackupList + plural: nonadminbackups + singular: nonadminbackup + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: NonAdminBackup is the Schema for the nonadminbackups 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: NonAdminBackupSpec defines the desired state of NonAdminBackup + properties: + backupSpec: + description: BackupSpec defines the specification for a Velero backup. + properties: + csiSnapshotTimeout: + description: |- + CSISnapshotTimeout specifies the time used to wait for CSI VolumeSnapshot status turns to + ReadyToUse during creation, before returning error as timeout. + The default value is 10 minute. + type: string + datamover: + description: |- + DataMover specifies the data mover to be used by the backup. + If DataMover is "" or "velero", the built-in data mover will be used. + type: string + defaultVolumesToFsBackup: + description: |- + DefaultVolumesToFsBackup specifies whether pod volume file system backup should be used + for all volumes by default. + nullable: true + type: boolean + defaultVolumesToRestic: + description: |- + DefaultVolumesToRestic specifies whether restic should be used to take a + backup of all pod volumes by default. + + + Deprecated: this field is no longer used and will be removed entirely in future. Use DefaultVolumesToFsBackup instead. + nullable: true + type: boolean + excludedClusterScopedResources: + description: |- + ExcludedClusterScopedResources is a slice of cluster-scoped + resource type names to exclude from the backup. + If set to "*", all cluster-scoped resource types are excluded. + The default value is empty. + items: + type: string + nullable: true + type: array + excludedNamespaceScopedResources: + description: |- + ExcludedNamespaceScopedResources is a slice of namespace-scoped + resource type names to exclude from the backup. + If set to "*", all namespace-scoped resource types are excluded. + The default value is empty. + items: + type: string + nullable: true + type: array + excludedNamespaces: + description: |- + ExcludedNamespaces contains a list of namespaces that are not + included in the backup. + items: + type: string + nullable: true + type: array + excludedResources: + description: |- + ExcludedResources is a slice of resource names that are not + included in the backup. + items: + type: string + nullable: true + type: array + hooks: + description: Hooks represent custom behaviors that should be executed + at different phases of the backup. + properties: + resources: + description: Resources are hooks that should be executed when + backing up individual instances of a resource. + items: + description: |- + BackupResourceHookSpec defines one or more BackupResourceHooks 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 + post: + description: |- + PostHooks is a list of BackupResourceHooks to execute after storing the item in the backup. + These are executed after all "additional items" from item actions are processed. + items: + description: BackupResourceHook defines a hook for + a resource. + properties: + exec: + description: Exec defines an exec hook. + properties: + command: + description: Command is the command and arguments + to execute. + 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 + onError: + description: OnError specifies how Velero + should behave if it encounters an error + executing this hook. + enum: + - Continue + - Fail + type: string + timeout: + description: |- + Timeout defines the maximum amount of time Velero should wait for the hook to complete before + considering the execution a failure. + type: string + required: + - command + type: object + required: + - exec + type: object + type: array + pre: + description: |- + PreHooks is a list of BackupResourceHooks to execute prior to storing the item in the backup. + These are executed before any "additional items" from item actions are processed. + items: + description: BackupResourceHook defines a hook for + a resource. + properties: + exec: + description: Exec defines an exec hook. + properties: + command: + description: Command is the command and arguments + to execute. + 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 + onError: + description: OnError specifies how Velero + should behave if it encounters an error + executing this hook. + enum: + - Continue + - Fail + type: string + timeout: + description: |- + Timeout defines the maximum amount of time Velero should wait for the hook to complete before + considering the execution a failure. + type: string + required: + - command + type: object + required: + - exec + type: object + type: array + required: + - name + type: object + nullable: true + type: array + type: object + includeClusterResources: + description: |- + IncludeClusterResources specifies whether cluster-scoped resources + should be included for consideration in the backup. + nullable: true + type: boolean + includedClusterScopedResources: + description: |- + IncludedClusterScopedResources is a slice of cluster-scoped + resource type names to include in the backup. + If set to "*", all cluster-scoped resource types are included. + The default value is empty, which means only related + cluster-scoped resources are included. + items: + type: string + nullable: true + type: array + includedNamespaceScopedResources: + description: |- + IncludedNamespaceScopedResources is a slice of namespace-scoped + resource type names to include in the backup. + The default value is "*". + items: + type: string + nullable: true + type: array + 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 backup. If empty, all resources are included. + items: + type: string + nullable: true + type: array + itemOperationTimeout: + description: |- + ItemOperationTimeout specifies the time used to wait for asynchronous BackupItemAction operations + The default value is 1 hour. + type: string + labelSelector: + description: |- + LabelSelector is a metav1.LabelSelector to filter with + when adding individual objects to 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 + metadata: + properties: + labels: + additionalProperties: + type: string + type: object + type: object + orLabelSelectors: + description: |- + OrLabelSelectors is list of metav1.LabelSelector to filter with + when adding individual objects to the backup. If multiple provided + they will be joined by the OR operator. LabelSelector as well as + OrLabelSelectors cannot co-exist in backup 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 + orderedResources: + additionalProperties: + type: string + description: |- + OrderedResources specifies the backup order of resources of specific Kind. + The map key is the resource name and value is a list of object names separated by commas. + Each resource name has format "namespace/objectname". For cluster resources, simply use "objectname". + nullable: true + type: object + resourcePolicy: + description: ResourcePolicy specifies the referenced resource + policies that backup should follow + 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 + snapshotMoveData: + description: SnapshotMoveData specifies whether snapshot data + should be moved + nullable: true + type: boolean + snapshotVolumes: + description: |- + SnapshotVolumes specifies whether to take snapshots + of any PV's referenced in the set of objects included + in the Backup. + nullable: true + type: boolean + storageLocation: + description: StorageLocation is a string containing the name of + a BackupStorageLocation where the backup should be stored. + type: string + ttl: + description: |- + TTL is a time.Duration-parseable string describing how long + the Backup should be retained for. + type: string + volumeSnapshotLocations: + description: VolumeSnapshotLocations is a list containing names + of VolumeSnapshotLocations associated with this backup. + items: + type: string + type: array + type: object + backupStatus: + description: BackupStatus captures the current status of a Velero + backup. + properties: + backupItemOperationsAttempted: + description: |- + BackupItemOperationsAttempted is the total number of attempted + async BackupItemAction operations for this backup. + type: integer + backupItemOperationsCompleted: + description: |- + BackupItemOperationsCompleted is the total number of successfully completed + async BackupItemAction operations for this backup. + type: integer + backupItemOperationsFailed: + description: |- + BackupItemOperationsFailed is the total number of async + BackupItemAction operations for this backup which ended with an error. + type: integer + completionTimestamp: + description: |- + CompletionTimestamp records the time a backup was completed. + Completion time is recorded even on failed backups. + Completion time is recorded before uploading the backup object. + The server's time is used for CompletionTimestamps + format: date-time + nullable: true + type: string + csiVolumeSnapshotsAttempted: + description: |- + CSIVolumeSnapshotsAttempted is the total number of attempted + CSI VolumeSnapshots for this backup. + type: integer + csiVolumeSnapshotsCompleted: + description: |- + CSIVolumeSnapshotsCompleted is the total number of successfully + completed CSI VolumeSnapshots for this backup. + type: integer + errors: + description: |- + Errors is a count of all error messages that were generated during + execution of the backup. The actual errors are in the backup's log + file in object storage. + type: integer + expiration: + description: Expiration is when this Backup is eligible for garbage-collection. + format: date-time + nullable: true + type: string + failureReason: + description: FailureReason is an error that caused the entire + backup to fail. + type: string + formatVersion: + description: FormatVersion is the backup format version, including + major, minor, and patch version. + type: string + phase: + description: Phase is the current state of the Backup. + enum: + - New + - FailedValidation + - InProgress + - WaitingForPluginOperations + - WaitingForPluginOperationsPartiallyFailed + - Finalizing + - FinalizingPartiallyFailed + - Completed + - PartiallyFailed + - Failed + - Deleting + type: string + progress: + description: |- + Progress contains information about the backup's execution progress. Note + that this information is best-effort only -- if Velero fails to update it + during a backup for any reason, it may be inaccurate/stale. + nullable: true + properties: + itemsBackedUp: + description: |- + ItemsBackedUp is the number of items that have actually been written to the + backup tarball so far. + type: integer + totalItems: + description: |- + TotalItems is the total number of items to be backed up. This number may change + throughout the execution of the backup due to plugins that return additional related + items to back up, the velero.io/exclude-from-backup label, and various other + filters that happen as items are processed. + type: integer + type: object + startTimestamp: + description: |- + StartTimestamp records the time a backup was started. + Separate from CreationTimestamp, since that value changes + on restores. + 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 + version: + description: |- + Version is the backup format major version. + Deprecated: Please see FormatVersion + type: integer + volumeSnapshotsAttempted: + description: |- + VolumeSnapshotsAttempted is the total number of attempted + volume snapshots for this backup. + type: integer + volumeSnapshotsCompleted: + description: |- + VolumeSnapshotsCompleted is the total number of successfully + completed volume snapshots for this backup. + type: integer + warnings: + description: |- + Warnings is a count of all warning messages that were generated during + execution of the backup. The actual warnings are in the backup's log + file in object storage. + type: integer + type: object + logLevel: + description: NonAdminBackup log level (use debug for the most logging, + leave unset for default) + enum: + - trace + - debug + - info + - warning + - error + - fatal + - panic + type: string + type: object + status: + description: NonAdminBackupStatus defines the observed state of NonAdminBackup + 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 + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/bases/oadp.openshift.io_dataprotectionapplications.yaml b/config/crd/bases/oadp.openshift.io_dataprotectionapplications.yaml index 9515484b5d..4038abeee3 100644 --- a/config/crd/bases/oadp.openshift.io_dataprotectionapplications.yaml +++ b/config/crd/bases/oadp.openshift.io_dataprotectionapplications.yaml @@ -831,6 +831,14 @@ spec: type: object features: description: features defines the configuration for the DPA to enable the OADP tech preview features + properties: + nonAdmin: + description: NonAdmin defines the configuration for the non admin feature + properties: + enable: + description: Enables non admin feature, by default is disabled + type: boolean + type: object type: object podAnnotations: additionalProperties: @@ -907,7 +915,7 @@ spec: unsupportedOverrides: additionalProperties: type: string - description: 'unsupportedOverrides can be used to override images used in deployments. Available keys are: - veleroImageFqin - awsPluginImageFqin - openshiftPluginImageFqin - azurePluginImageFqin - gcpPluginImageFqin - csiPluginImageFqin - resticRestoreImageFqin - kubevirtPluginImageFqin - operator-type' + description: 'unsupportedOverrides can be used to override images used in deployments. Available keys are: - veleroImageFqin - awsPluginImageFqin - openshiftPluginImageFqin - azurePluginImageFqin - gcpPluginImageFqin - csiPluginImageFqin - resticRestoreImageFqin - kubevirtPluginImageFqin - nonAdminControllerImageFqin - operator-type' type: object required: - configuration diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index 4f18352b28..b0ab8b5fe4 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -17,6 +17,7 @@ resources: - bases/velero.io_schedules.yaml - bases/velero.io_serverstatusrequests.yaml - bases/velero.io_volumesnapshotlocations.yaml +- bases/nac.oadp.openshift.io_nonadminbackups.yaml #+kubebuilder:scaffold:crdkustomizeresource patchesStrategicMerge: diff --git a/config/manager/manager.yaml b/config/manager/manager.yaml index 580535e48e..d95a496800 100644 --- a/config/manager/manager.yaml +++ b/config/manager/manager.yaml @@ -54,6 +54,8 @@ spec: value: quay.io/konveyor/kubevirt-velero-plugin:v0.2.0 - name: RELATED_IMAGE_MUSTGATHER value: registry.redhat.io/oadp/oadp-mustgather-rhel8:v1.2 + - name: RELATED_IMAGE_NON_ADMIN_CONTROLLER + value: quay.io/konveyor/oadp-non-admin:latest args: - --leader-elect image: controller:latest diff --git a/config/manifests/bases/oadp-operator.clusterserviceversion.yaml b/config/manifests/bases/oadp-operator.clusterserviceversion.yaml index 04a7a8b425..cf3a2c8a11 100644 --- a/config/manifests/bases/oadp-operator.clusterserviceversion.yaml +++ b/config/manifests/bases/oadp-operator.clusterserviceversion.yaml @@ -401,6 +401,11 @@ spec: displayName: Name path: name version: v1alpha1 + - description: NonAdminBackup is the Schema for the nonadminbackups API + displayName: Non Admin Backup + kind: NonAdminBackup + name: nonadminbackups.nac.oadp.openshift.io + version: v1alpha1 description: | **OpenShift API for Data Protection (OADP)** operator sets up and installs Velero on the OpenShift platform, allowing users to backup and restore diff --git a/config/manifests/kustomization.yaml b/config/manifests/kustomization.yaml index 0a5c4052a1..788a21845a 100644 --- a/config/manifests/kustomization.yaml +++ b/config/manifests/kustomization.yaml @@ -6,6 +6,7 @@ resources: - ../samples - ../scorecard - ../velero +- ../non-admin-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/non-admin-controller_rbac/kustomization.yaml b/config/non-admin-controller_rbac/kustomization.yaml new file mode 100644 index 0000000000..2f853d77fb --- /dev/null +++ b/config/non-admin-controller_rbac/kustomization.yaml @@ -0,0 +1,18 @@ +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 +# - leader_election_role.yaml +# - leader_election_role_binding.yaml +# Comment the following 4 lines if you want to disable +# the auth proxy (https://github.com/brancz/kube-rbac-proxy) +# which protects your /metrics endpoint. +# - auth_proxy_service.yaml +# - auth_proxy_role.yaml +# - auth_proxy_role_binding.yaml +# - auth_proxy_client_clusterrole.yaml diff --git a/config/non-admin-controller_rbac/role.yaml b/config/non-admin-controller_rbac/role.yaml new file mode 100644 index 0000000000..3b3efada80 --- /dev/null +++ b/config/non-admin-controller_rbac/role.yaml @@ -0,0 +1,43 @@ +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: non-admin-controller-role +rules: +- apiGroups: + - nac.oadp.openshift.io + resources: + - nonadminbackups + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - nac.oadp.openshift.io + resources: + - nonadminbackups/finalizers + verbs: + - update +- apiGroups: + - nac.oadp.openshift.io + resources: + - nonadminbackups/status + verbs: + - get + - patch + - update +- apiGroups: + - velero.io + resources: + - backups + verbs: + - create + - get + - list + - patch + - update + - watch diff --git a/config/non-admin-controller_rbac/role_binding.yaml b/config/non-admin-controller_rbac/role_binding.yaml new file mode 100644 index 0000000000..42f638ae73 --- /dev/null +++ b/config/non-admin-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: non-admin-controller-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: non-admin-controller-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: non-admin-controller-role +subjects: +- kind: ServiceAccount + name: non-admin-controller + namespace: system diff --git a/config/non-admin-controller_rbac/service_account.yaml b/config/non-admin-controller_rbac/service_account.yaml new file mode 100644 index 0000000000..09e0b660cb --- /dev/null +++ b/config/non-admin-controller_rbac/service_account.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + labels: + app.kubernetes.io/name: serviceaccount + app.kubernetes.io/instance: non-admin-controller-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: non-admin-controller + namespace: system diff --git a/config/samples/kustomization.yaml b/config/samples/kustomization.yaml index c18dd5bfdd..e56c745767 100644 --- a/config/samples/kustomization.yaml +++ b/config/samples/kustomization.yaml @@ -12,4 +12,5 @@ resources: - schedule.yaml - serverstatusrequest.yaml - volumesnapshotlocation.yaml +- nac_v1alpha1_nonadminbackup.yaml #+kubebuilder:scaffold:manifestskustomizesamples diff --git a/config/samples/nac_v1alpha1_nonadminbackup.yaml b/config/samples/nac_v1alpha1_nonadminbackup.yaml new file mode 100644 index 0000000000..f97101faff --- /dev/null +++ b/config/samples/nac_v1alpha1_nonadminbackup.yaml @@ -0,0 +1,12 @@ +apiVersion: nac.oadp.openshift.io/v1alpha1 +kind: NonAdminBackup +metadata: + labels: + app.kubernetes.io/name: nonadminbackup + app.kubernetes.io/instance: nonadminbackup-sample + app.kubernetes.io/part-of: oadp-operator + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/created-by: oadp-operator + name: nonadminbackup-sample +spec: + backupSpec: {} diff --git a/controllers/dpa_controller.go b/controllers/dpa_controller.go index 9b9ebdbef4..c2f5ef6b78 100644 --- a/controllers/dpa_controller.go +++ b/controllers/dpa_controller.go @@ -104,6 +104,7 @@ func (r *DPAReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.R r.ReconcileVeleroDeployment, r.ReconcileNodeAgentDaemonset, r.ReconcileVeleroMetricsSVC, + r.ReconcileNonAdminController, ) if err != nil { diff --git a/controllers/nonadmin_controller.go b/controllers/nonadmin_controller.go new file mode 100644 index 0000000000..fee80c05fb --- /dev/null +++ b/controllers/nonadmin_controller.go @@ -0,0 +1,203 @@ +package controllers + +import ( + "fmt" + "os" + + "github.com/go-logr/logr" + "golang.org/x/exp/maps" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + k8serror "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/utils/pointer" + "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 ( + nonAdminObjectName = "non-admin-controller" + controlPlaneKey = "control-plane" +) + +var ( + controlPlaneLabel = map[string]string{ + controlPlaneKey: nonAdminObjectName, + } + deploymentLabels = map[string]string{ + "app.kubernetes.io/component": "manager", + "app.kubernetes.io/created-by": common.OADPOperator, + "app.kubernetes.io/instance": nonAdminObjectName, + "app.kubernetes.io/managed-by": "kustomize", + "app.kubernetes.io/name": "deployment", + "app.kubernetes.io/part-of": common.OADPOperator, + } +) + +func (r *DPAReconciler) ReconcileNonAdminController(log logr.Logger) (bool, error) { + // TODO https://github.com/openshift/oadp-operator/pull/1316 + dpa := oadpv1alpha1.DataProtectionApplication{} + if err := r.Get(r.Context, r.NamespacedName, &dpa); err != nil { + return false, err + } + + nonAdminDeployment := &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: nonAdminObjectName, + Namespace: r.NamespacedName.Namespace, + }, + } + + // Delete (possible) previously deployment + if !r.checkNonAdminEnabled(&dpa) { + if err := r.Get( + r.Context, + types.NamespacedName{ + Name: nonAdminDeployment.Name, + Namespace: nonAdminDeployment.Namespace, + }, + nonAdminDeployment, + ); err != nil { + if k8serror.IsNotFound(err) { + return true, nil + } + return false, err + } + + deleteOptionPropagationForeground := metav1.DeletePropagationForeground + if err := r.Delete( + r.Context, + nonAdminDeployment, + &client.DeleteOptions{PropagationPolicy: &deleteOptionPropagationForeground}, + ); err != nil { + r.EventRecorder.Event( + nonAdminDeployment, + corev1.EventTypeWarning, + "NonAdminDeploymentDeleteFailed", + fmt.Sprintf("Could not delete non admin controller deployment %s/%s: %s", nonAdminDeployment.Namespace, nonAdminDeployment.Name, err), + ) + return false, err + } + r.EventRecorder.Event( + nonAdminDeployment, + corev1.EventTypeNormal, + "NonAdminDeploymentDeleteSucceed", + fmt.Sprintf("Non admin controller deployment %s/%s deleted", nonAdminDeployment.Namespace, nonAdminDeployment.Name), + ) + return true, nil + } + + operation, err := controllerutil.CreateOrUpdate( + r.Context, + r.Client, + nonAdminDeployment, + func() error { + r.buildNonAdminDeployment(nonAdminDeployment, &dpa) + + // Setting controller owner reference on the non admin controller deployment + return controllerutil.SetControllerReference(&dpa, nonAdminDeployment, r.Scheme) + }, + ) + if err != nil { + return false, err + } + + if operation != controllerutil.OperationResultNone { + r.EventRecorder.Event( + nonAdminDeployment, + corev1.EventTypeNormal, + "NonAdminDeploymentReconciled", + fmt.Sprintf("Non admin controller deployment %s/%s %s", nonAdminDeployment.Namespace, nonAdminDeployment.Name, operation), + ) + } + return true, nil +} + +func (r *DPAReconciler) buildNonAdminDeployment(deploymentObject *appsv1.Deployment, dpa *oadpv1alpha1.DataProtectionApplication) { + // TODO https://github.com/openshift/oadp-operator/pull/1316 + nonAdminImage := r.getNonAdminImage(dpa) + ensureRequiredLabels(deploymentObject) + ensureRequiredSpecs(deploymentObject, nonAdminImage) +} + +func ensureRequiredLabels(deploymentObject *appsv1.Deployment) { + maps.Copy(deploymentLabels, controlPlaneLabel) + deploymentObjectLabels := deploymentObject.GetLabels() + if deploymentObjectLabels == nil { + deploymentObject.SetLabels(deploymentLabels) + } else { + for key, value := range deploymentLabels { + deploymentObjectLabels[key] = value + } + deploymentObject.SetLabels(deploymentObjectLabels) + } +} + +func ensureRequiredSpecs(deploymentObject *appsv1.Deployment, image string) { + namespaceEnvVar := corev1.EnvVar{ + Name: "WATCH_NAMESPACE", + Value: deploymentObject.Namespace, + } + + deploymentObject.Spec.Replicas = pointer.Int32(1) + deploymentObject.Spec.Selector = &metav1.LabelSelector{ + MatchLabels: controlPlaneLabel, + } + templateObjectLabels := deploymentObject.Spec.Template.GetLabels() + if templateObjectLabels == nil { + deploymentObject.Spec.Template.SetLabels(controlPlaneLabel) + } else { + templateObjectLabels[controlPlaneKey] = controlPlaneLabel[controlPlaneKey] + deploymentObject.Spec.Template.SetLabels(templateObjectLabels) + } + if len(deploymentObject.Spec.Template.Spec.Containers) == 0 { + deploymentObject.Spec.Template.Spec.Containers = []corev1.Container{{ + Name: nonAdminObjectName, + Image: image, + ImagePullPolicy: corev1.PullAlways, + Env: []corev1.EnvVar{namespaceEnvVar}, + }} + } else { + for _, container := range deploymentObject.Spec.Template.Spec.Containers { + if container.Name == nonAdminObjectName { + container.Image = image + container.ImagePullPolicy = corev1.PullAlways + container.Env = []corev1.EnvVar{namespaceEnvVar} + break + } + } + } + deploymentObject.Spec.Template.Spec.RestartPolicy = corev1.RestartPolicyAlways + deploymentObject.Spec.Template.Spec.ServiceAccountName = nonAdminObjectName +} + +func (r *DPAReconciler) checkNonAdminEnabled(dpa *oadpv1alpha1.DataProtectionApplication) bool { + // TODO https://github.com/openshift/oadp-operator/pull/1316 + if dpa.Spec.Features != nil && + dpa.Spec.Features.NonAdmin != nil && + dpa.Spec.Features.NonAdmin.Enable != nil { + return *dpa.Spec.Features.NonAdmin.Enable + } + + return false +} + +func (r *DPAReconciler) getNonAdminImage(dpa *oadpv1alpha1.DataProtectionApplication) string { + // TODO https://github.com/openshift/oadp-operator/pull/1316 + unsupportedOverride := dpa.Spec.UnsupportedOverrides[oadpv1alpha1.NonAdminControllerImageKey] + if unsupportedOverride != "" { + return unsupportedOverride + } + + environmentVariable := os.Getenv("RELATED_IMAGE_NON_ADMIN_CONTROLLER") + if environmentVariable != "" { + return environmentVariable + } + + // TODO https://github.com/openshift/oadp-operator/issues/1379 + return "quay.io/konveyor/oadp-non-admin:latest" +} diff --git a/controllers/nonadmin_controller_test.go b/controllers/nonadmin_controller_test.go new file mode 100644 index 0000000000..51506aeed8 --- /dev/null +++ b/controllers/nonadmin_controller_test.go @@ -0,0 +1,353 @@ +package controllers + +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" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/tools/record" + "k8s.io/utils/pointer" + + oadpv1alpha1 "github.com/openshift/oadp-operator/api/v1alpha1" +) + +const defaultNonAdminImage = "quay.io/konveyor/oadp-non-admin:latest" + +type ReconcileNonAdminControllerScenario struct { + namespace string + dpa string + errMessage string + eventWords []string + nonAdminEnabled bool + deployment *appsv1.Deployment +} + +func createTestDeployment(namespace string) *appsv1.Deployment { + return &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: nonAdminObjectName, + Namespace: namespace, + Labels: map[string]string{ + "test": "test", + "app.kubernetes.io/name": "wrong", + controlPlaneKey: "super-wrong", + }, + }, + Spec: appsv1.DeploymentSpec{ + Replicas: pointer.Int32(2), + Selector: &metav1.LabelSelector{ + MatchLabels: controlPlaneLabel, + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: controlPlaneLabel, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: nonAdminObjectName, + Image: defaultNonAdminImage, + }, + }, + ServiceAccountName: "wrong-one", + }, + }, + }, + } +} + +func runReconcileNonAdminControllerTest( + scenario ReconcileNonAdminControllerScenario, + updateTestScenario func(scenario ReconcileNonAdminControllerScenario), + ctx context.Context, + envVarValue 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{}, + Features: &oadpv1alpha1.Features{ + NonAdmin: &oadpv1alpha1.NonAdmin{ + Enable: pointer.Bool(scenario.nonAdminEnabled), + }, + }, + }, + } + 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_NON_ADMIN_CONTROLLER", envVarValue) + event := record.NewFakeRecorder(5) + r := &DPAReconciler{ + Client: k8sClient, + Scheme: testEnv.Scheme, + Context: ctx, + NamespacedName: types.NamespacedName{ + Name: scenario.dpa, + Namespace: scenario.namespace, + }, + EventRecorder: event, + } + result, err := r.ReconcileNonAdminController(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 ReconcileNonAdminController function", func() { + var ( + ctx = context.Background() + currentTestScenario ReconcileNonAdminControllerScenario + updateTestScenario = func(scenario ReconcileNonAdminControllerScenario) { + currentTestScenario = scenario + } + ) + + ginkgo.AfterEach(func() { + os.Unsetenv("RELATED_IMAGE_NON_ADMIN_CONTROLLER") + + deployment := &appsv1.Deployment{} + if k8sClient.Get( + ctx, + types.NamespacedName{ + Name: nonAdminObjectName, + 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 ReconcileNonAdminControllerScenario) { + runReconcileNonAdminControllerTest(scenario, updateTestScenario, ctx, defaultNonAdminImage) + }, + ginkgo.Entry("Should create non admin deployment", ReconcileNonAdminControllerScenario{ + namespace: "test-1", + dpa: "test-1-dpa", + eventWords: []string{"Normal", "NonAdminDeploymentReconciled", "created"}, + nonAdminEnabled: true, + }), + ginkgo.Entry("Should update non admin deployment", ReconcileNonAdminControllerScenario{ + namespace: "test-2", + dpa: "test-2-dpa", + eventWords: []string{"Normal", "NonAdminDeploymentReconciled", "updated"}, + nonAdminEnabled: true, + deployment: createTestDeployment("test-2"), + }), + ginkgo.Entry("Should delete non admin deployment", ReconcileNonAdminControllerScenario{ + namespace: "test-3", + dpa: "test-3-dpa", + eventWords: []string{"Normal", "NonAdminDeploymentDeleteSucceed", "deleted"}, + nonAdminEnabled: false, + deployment: createTestDeployment("test-3"), + }), + ginkgo.Entry("Should do nothing", ReconcileNonAdminControllerScenario{ + namespace: "test-4", + dpa: "test-4-dpa", + nonAdminEnabled: false, + }), + ) +}) + +func TestDPAReconcilerBuildNonAdminDeployment(t *testing.T) { + r := &DPAReconciler{} + t.Setenv("RELATED_IMAGE_NON_ADMIN_CONTROLLER", defaultNonAdminImage) + deployment := createTestDeployment("test-build-deployment") + r.buildNonAdminDeployment(deployment, &oadpv1alpha1.DataProtectionApplication{}) + labels := deployment.GetLabels() + if labels["test"] != "test" { + t.Errorf("Deployment label 'test' has wrong value: %v", labels["test"]) + } + if labels["app.kubernetes.io/name"] != "deployment" { + t.Errorf("Deployment label 'app.kubernetes.io/name' has wrong value: %v", labels["app.kubernetes.io/name"]) + } + if labels[controlPlaneKey] != nonAdminObjectName { + t.Errorf("Deployment label '%v' has wrong value: %v", controlPlaneKey, labels[controlPlaneKey]) + } + if *deployment.Spec.Replicas != 1 { + t.Errorf("Deployment has wrong number of replicas: %v", *deployment.Spec.Replicas) + } + if deployment.Spec.Template.Spec.ServiceAccountName != nonAdminObjectName { + t.Errorf("Deployment has wrong ServiceAccount: %v", deployment.Spec.Template.Spec.ServiceAccountName) + } +} + +func TestEnsureRequiredLabels(t *testing.T) { + deployment := createTestDeployment("test-ensure-label") + ensureRequiredLabels(deployment) + labels := deployment.GetLabels() + if labels["test"] != "test" { + t.Errorf("Deployment label 'test' has wrong value: %v", labels["test"]) + } + if labels["app.kubernetes.io/name"] != "deployment" { + t.Errorf("Deployment label 'app.kubernetes.io/name' has wrong value: %v", labels["app.kubernetes.io/name"]) + } + if labels[controlPlaneKey] != nonAdminObjectName { + t.Errorf("Deployment label '%v' has wrong value: %v", controlPlaneKey, labels[controlPlaneKey]) + } +} + +func TestEnsureRequiredSpecs(t *testing.T) { + deployment := createTestDeployment("test-ensure-spec") + ensureRequiredSpecs(deployment, defaultNonAdminImage) + if *deployment.Spec.Replicas != 1 { + t.Errorf("Deployment has wrong number of replicas: %v", *deployment.Spec.Replicas) + } + if deployment.Spec.Template.Spec.ServiceAccountName != nonAdminObjectName { + t.Errorf("Deployment has wrong ServiceAccount: %v", deployment.Spec.Template.Spec.ServiceAccountName) + } +} + +func TestDPAReconcilerCheckNonAdminEnabled(t *testing.T) { + r := &DPAReconciler{} + tests := []struct { + name string + result bool + dpa *oadpv1alpha1.DataProtectionApplication + }{ + { + name: "DPA has non admin feature enable: true", + result: true, + dpa: &oadpv1alpha1.DataProtectionApplication{ + Spec: oadpv1alpha1.DataProtectionApplicationSpec{ + Features: &oadpv1alpha1.Features{ + NonAdmin: &oadpv1alpha1.NonAdmin{ + Enable: pointer.Bool(true), + }, + }, + }, + }, + }, + { + name: "DPA has non admin feature enable: false", + result: false, + dpa: &oadpv1alpha1.DataProtectionApplication{ + Spec: oadpv1alpha1.DataProtectionApplicationSpec{ + Features: &oadpv1alpha1.Features{ + NonAdmin: &oadpv1alpha1.NonAdmin{ + Enable: pointer.Bool(false), + }, + }, + }, + }, + }, + { + name: "DPA has no non admin feature", + result: false, + dpa: &oadpv1alpha1.DataProtectionApplication{}, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + result := r.checkNonAdminEnabled(test.dpa) + if result != test.result { + t.Errorf("Results differ: got '%v' but expected '%v'", result, test.result) + } + }) + } +} + +func TestDPAReconcilerGetNonAdminImage(t *testing.T) { + r := &DPAReconciler{} + tests := []struct { + name string + image string + env string + dpa *oadpv1alpha1.DataProtectionApplication + }{ + { + name: "Get non admin image from environment variable with default value", + image: defaultNonAdminImage, + env: defaultNonAdminImage, + dpa: &oadpv1alpha1.DataProtectionApplication{}, + }, + { + name: "Get non admin image from environment variable with custom value", + image: "quay.io/openshift/oadp-non-admin:latest", + env: "quay.io/openshift/oadp-non-admin:latest", + dpa: &oadpv1alpha1.DataProtectionApplication{}, + }, + { + name: "Get non admin image from unsupported overrides", + image: "quay.io/konveyor/another:latest", + dpa: &oadpv1alpha1.DataProtectionApplication{ + Spec: oadpv1alpha1.DataProtectionApplicationSpec{ + UnsupportedOverrides: map[oadpv1alpha1.UnsupportedImageKey]string{ + "nonAdminControllerImageFqin": "quay.io/konveyor/another:latest", + }, + }, + }, + }, + { + name: "Get non admin image from fallback", + image: defaultNonAdminImage, + dpa: &oadpv1alpha1.DataProtectionApplication{}, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if len(test.env) > 0 { + t.Setenv("RELATED_IMAGE_NON_ADMIN_CONTROLLER", test.env) + } + image := r.getNonAdminImage(test.dpa) + if image != test.image { + t.Errorf("Images differ: got '%v' but expected '%v'", image, test.image) + } + }) + } +} diff --git a/controllers/suite_test.go b/controllers/suite_test.go index 43b313883c..01cb31fcca 100644 --- a/controllers/suite_test.go +++ b/controllers/suite_test.go @@ -17,10 +17,12 @@ limitations under the License. package controllers import ( + "fmt" "path/filepath" + "runtime" "testing" - "github.com/onsi/ginkgo" + "github.com/onsi/ginkgo/v2" "github.com/onsi/gomega" "k8s.io/client-go/kubernetes/scheme" "sigs.k8s.io/controller-runtime/pkg/client" @@ -40,7 +42,7 @@ var testEnv *envtest.Environment func TestAPIs(t *testing.T) { gomega.RegisterFailHandler(ginkgo.Fail) - ginkgo.RunSpecs(t, "Controller Suite") + ginkgo.RunSpecs(t, "Controllers Suite") } var _ = ginkgo.BeforeSuite(func() { @@ -50,6 +52,14 @@ var _ = ginkgo.BeforeSuite(func() { testEnv = &envtest.Environment{ CRDDirectoryPaths: []string{filepath.Join("..", "config", "crd", "bases")}, ErrorIfCRDPathMissing: true, + + // The BinaryAssetsDirectory is only required if you want to run the tests directly + // without call the makefile target test. If not informed it will look for the + // default path defined in controller-runtime which is /usr/local/kubebuilder/. + // 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.28.3-%s-%s", runtime.GOOS, runtime.GOARCH)), } cfg, err := testEnv.Start() @@ -65,7 +75,7 @@ var _ = ginkgo.BeforeSuite(func() { gomega.Expect(err).NotTo(gomega.HaveOccurred()) gomega.Expect(k8sClient).NotTo(gomega.BeNil()) -}, 60) +}) var _ = ginkgo.AfterSuite(func() { ginkgo.By("tearing down the test environment") diff --git a/go.mod b/go.mod index 46032bb35c..27af17864c 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,6 @@ require ( github.com/go-logr/logr v1.2.4 github.com/google/uuid v1.3.0 github.com/kubernetes-csi/external-snapshotter/client/v4 v4.2.0 - github.com/onsi/ginkgo v1.16.5 github.com/onsi/ginkgo/v2 v2.11.0 github.com/onsi/gomega v1.27.8 github.com/openshift/api v0.0.0-20230213134911-7ba313770556 // release-4.12 @@ -27,6 +26,7 @@ require ( github.com/deckarep/golang-set/v2 v2.3.0 github.com/google/go-cmp v0.5.9 github.com/vmware-tanzu/velero v1.12.0 + golang.org/x/exp v0.0.0-20230213192124-5e25df0256eb k8s.io/klog/v2 v2.90.0 sigs.k8s.io/yaml v1.3.0 ) @@ -112,7 +112,6 @@ require ( github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/natefinch/atomic v1.0.1 // indirect - github.com/nxadm/tail v1.4.8 // indirect github.com/oklog/run v1.0.0 // indirect github.com/pierrec/lz4 v2.6.1+incompatible // indirect github.com/pkg/errors v0.9.1 // indirect @@ -133,7 +132,6 @@ require ( go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.24.0 // indirect golang.org/x/crypto v0.14.0 // indirect - golang.org/x/exp v0.0.0-20230213192124-5e25df0256eb // indirect golang.org/x/net v0.17.0 // indirect golang.org/x/oauth2 v0.7.0 // indirect golang.org/x/sync v0.3.0 // indirect @@ -151,7 +149,6 @@ require ( google.golang.org/protobuf v1.33.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/ini.v1 v1.67.0 // indirect - gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/cli-runtime v0.24.0 // indirect diff --git a/go.sum b/go.sum index 8178aa6e44..963e8482e2 100644 --- a/go.sum +++ b/go.sum @@ -681,7 +681,6 @@ github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9k github.com/onsi/ginkgo v1.16.2/go.mod h1:CObGmKUOKaSC0RjmoAK7tKyn4Azo5P2IWuoMnvwxz1E= github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= -github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= github.com/onsi/ginkgo/v2 v2.11.0 h1:WgqUCUt/lT6yXoQ8Wef0fsNn5cAuMK7+KT9UFRz2tcU= github.com/onsi/ginkgo/v2 v2.11.0/go.mod h1:ZhrRA5XmEE3x3rhlzamx/JJvujdZoJ2uvgI7kR0iZvM= github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= diff --git a/main.go b/main.go index 079e33bca7..f38c7fde5a 100644 --- a/main.go +++ b/main.go @@ -326,7 +326,7 @@ func CreateCredRequest(roleARN string, WITP string, secretNS string, kubeconf *r "namespace": secretNS, }, "serviceAccountNames": []interface{}{ - "openshift-adp-controller-manager", + common.OADPOperatorServiceAccount, }, "providerSpec": map[string]interface{}{ "apiVersion": "cloudcredential.openshift.io/v1", diff --git a/pkg/common/common.go b/pkg/common/common.go index 889cdf0242..8268797e94 100644 --- a/pkg/common/common.go +++ b/pkg/common/common.go @@ -12,12 +12,14 @@ import ( ) const ( + // From config/default/kustomization.yaml namePrefix field + OADPOperatorPrefix = "openshift-adp-" Velero = "velero" NodeAgent = "node-agent" VeleroNamespace = "oadp-operator" OADPOperator = "oadp-operator" OADPOperatorVelero = "oadp-operator-velero" - OADPOperatorServiceAccount = "openshift-adp-controller-manager" + OADPOperatorServiceAccount = OADPOperatorPrefix + "controller-manager" ) var DefaultRestoreResourcePriorities = restore.Priorities{ diff --git a/tests/e2e/lib/dpa_helpers.go b/tests/e2e/lib/dpa_helpers.go index d897b56e4f..3aea52d41b 100644 --- a/tests/e2e/lib/dpa_helpers.go +++ b/tests/e2e/lib/dpa_helpers.go @@ -325,7 +325,7 @@ func AreVeleroPodsRunning(c *kubernetes.Clientset, namespace string) wait.Condit } func GetOpenShiftADPLogs(c *kubernetes.Clientset, namespace string) (string, error) { - return GetPodWithPrefixContainerLogs(c, namespace, "openshift-adp-controller-manager-", "manager") + return GetPodWithPrefixContainerLogs(c, namespace, common.OADPOperatorPrefix+"controller-manager-", "manager") } // Returns logs from velero container on velero pod