diff --git a/pkg/apis/core/v1beta1/openapi_generated.go b/pkg/apis/core/v1beta1/openapi_generated.go index 222481f1ce..64c162b7e2 100644 --- a/pkg/apis/core/v1beta1/openapi_generated.go +++ b/pkg/apis/core/v1beta1/openapi_generated.go @@ -376,6 +376,7 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA "kubevirt.io/containerized-data-importer-api/pkg/apis/core/v1beta1.OldTLSProfile": schema_pkg_apis_core_v1beta1_OldTLSProfile(ref), "kubevirt.io/containerized-data-importer-api/pkg/apis/core/v1beta1.PlatformOptions": schema_pkg_apis_core_v1beta1_PlatformOptions(ref), "kubevirt.io/containerized-data-importer-api/pkg/apis/core/v1beta1.StorageProfile": schema_pkg_apis_core_v1beta1_StorageProfile(ref), + "kubevirt.io/containerized-data-importer-api/pkg/apis/core/v1beta1.StorageProfileCondition": schema_pkg_apis_core_v1beta1_StorageProfileCondition(ref), "kubevirt.io/containerized-data-importer-api/pkg/apis/core/v1beta1.StorageProfileList": schema_pkg_apis_core_v1beta1_StorageProfileList(ref), "kubevirt.io/containerized-data-importer-api/pkg/apis/core/v1beta1.StorageProfileSpec": schema_pkg_apis_core_v1beta1_StorageProfileSpec(ref), "kubevirt.io/containerized-data-importer-api/pkg/apis/core/v1beta1.StorageProfileStatus": schema_pkg_apis_core_v1beta1_StorageProfileStatus(ref), @@ -19163,6 +19164,58 @@ func schema_pkg_apis_core_v1beta1_StorageProfile(ref common.ReferenceCallback) c } } +func schema_pkg_apis_core_v1beta1_StorageProfileCondition(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "StorageProfileCondition represents the state of a storage profile condition", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "type": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "status": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "lastTransitionTime": { + SchemaProps: spec.SchemaProps{ + Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.Time"), + }, + }, + "lastHeartbeatTime": { + SchemaProps: spec.SchemaProps{ + Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.Time"), + }, + }, + "reason": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + "message": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"type", "status"}, + }, + }, + Dependencies: []string{ + "k8s.io/apimachinery/pkg/apis/meta/v1.Time"}, + } +} + func schema_pkg_apis_core_v1beta1_StorageProfileList(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ @@ -19319,11 +19372,25 @@ func schema_pkg_apis_core_v1beta1_StorageProfileStatus(ref common.ReferenceCallb Format: "", }, }, + "conditions": { + SchemaProps: spec.SchemaProps{ + Description: "Conditions contains the current conditions observed for the StorageProfile", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("kubevirt.io/containerized-data-importer-api/pkg/apis/core/v1beta1.StorageProfileCondition"), + }, + }, + }, + }, + }, }, }, }, Dependencies: []string{ - "kubevirt.io/containerized-data-importer-api/pkg/apis/core/v1beta1.ClaimPropertySet"}, + "kubevirt.io/containerized-data-importer-api/pkg/apis/core/v1beta1.ClaimPropertySet", "kubevirt.io/containerized-data-importer-api/pkg/apis/core/v1beta1.StorageProfileCondition"}, } } diff --git a/pkg/controller/storageprofile-controller.go b/pkg/controller/storageprofile-controller.go index d9e9f09781..586d05f492 100644 --- a/pkg/controller/storageprofile-controller.go +++ b/pkg/controller/storageprofile-controller.go @@ -52,6 +52,10 @@ const ( counterLabelRWX = "rwx" counterLabelSmartClone = "smartclone" counterLabelDegraded = "degraded" + + recognizedProvisionerMessage = "Provisioner is recognized" + unrecognizedProvisionerMessage = "Provisioner is not recognized" + unrecognizedStorageClassParametersMessage = "Storage class parameters are not recognized" ) // StorageProfileReconciler members @@ -125,6 +129,7 @@ func (r *StorageProfileReconciler) reconcileStorageProfile(sc *storagev1.Storage } storageProfile.Status.ClaimPropertySets = claimPropertySets + r.reconcileConditions(context.TODO(), sc, storageProfile) util.SetRecommendedLabels(storageProfile, r.installerLabels, "cdi-controller") if err := r.updateStorageProfile(prevStorageProfile, storageProfile, log); err != nil { @@ -198,6 +203,32 @@ func (r *StorageProfileReconciler) getStorageProfile(sc *storagev1.StorageClass) return storageProfile, prevStorageProfile, nil } +func (r *StorageProfileReconciler) reconcileConditions(ctx context.Context, sc *storagev1.StorageClass, sp *cdiv1.StorageProfile) { + cond := findStorageProfileConditionByType(sp, cdiv1.StorageProfileRecognized) + if cond == nil { + sp.Status.Conditions = append(sp.Status.Conditions, cdiv1.StorageProfileCondition{Type: cdiv1.StorageProfileRecognized}) + cond = &sp.Status.Conditions[len(sp.Status.Conditions)-1] + } + + switch reason := storagecapabilities.IsRecognized(sc); reason { + case storagecapabilities.RecognizedProvisioner: + updateConditionState(&cond.ConditionState, v1.ConditionTrue, recognizedProvisionerMessage, string(reason)) + case storagecapabilities.UnrecognizedProvisioner: + updateConditionState(&cond.ConditionState, v1.ConditionFalse, unrecognizedProvisionerMessage, string(reason)) + case storagecapabilities.UnrecognizedStorageClassParameters: + updateConditionState(&cond.ConditionState, v1.ConditionFalse, unrecognizedStorageClassParametersMessage, string(reason)) + } +} + +func findStorageProfileConditionByType(sp *cdiv1.StorageProfile, condType cdiv1.StorageProfileConditionType) *cdiv1.StorageProfileCondition { + for i := range sp.Status.Conditions { + if sp.Status.Conditions[i].Type == condType { + return &sp.Status.Conditions[i] + } + } + return nil +} + func (r *StorageProfileReconciler) reconcilePropertySets(sc *storagev1.StorageClass) []cdiv1.ClaimPropertySet { claimPropertySets := []cdiv1.ClaimPropertySet{} capabilities, found := storagecapabilities.GetCapabilities(r.client, sc) diff --git a/pkg/controller/storageprofile-controller_test.go b/pkg/controller/storageprofile-controller_test.go index 84adc1d730..2121ff9119 100644 --- a/pkg/controller/storageprofile-controller_test.go +++ b/pkg/controller/storageprofile-controller_test.go @@ -651,6 +651,35 @@ var _ = Describe("Storage profile controller reconcile loop", func() { Entry("Without RWX, on SNO, not degraded", v1.ReadWriteOnce, true, false), ) + DescribeTable("Should set Recognized condition", func(provisioner string, scParameters map[string]string, expectedStatus v1.ConditionStatus, expectedReason, expectedMessage string) { + storageClass := CreateStorageClassWithProvisioner(storageClassName, nil, nil, provisioner) + storageClass.Parameters = scParameters + reconciler = createStorageProfileReconciler(storageClass) + _, err := reconciler.Reconcile(context.TODO(), reconcile.Request{NamespacedName: types.NamespacedName{Name: storageClassName}}) + Expect(err).ToNot(HaveOccurred()) + + sp := &cdiv1.StorageProfile{} + err = reconciler.client.Get(context.TODO(), types.NamespacedName{Name: storageClassName}, sp, &client.GetOptions{}) + Expect(err).ToNot(HaveOccurred()) + + Expect(sp.Status.Conditions).To(HaveLen(1)) + cond := sp.Status.Conditions[0] + Expect(cond.Type).To(Equal(cdiv1.StorageProfileRecognized)) + Expect(cond.Status).To(Equal(expectedStatus)) + Expect(cond.Reason).To(Equal(expectedReason)) + Expect(cond.Message).To(Equal(expectedMessage)) + }, + Entry("recognized provisioner", + cephProvisioner, nil, + v1.ConditionTrue, string(storagecapabilities.RecognizedProvisioner), recognizedProvisionerMessage), + Entry("unrecognized provisioner", + "unknown-provisioner", nil, + v1.ConditionFalse, string(storagecapabilities.UnrecognizedProvisioner), unrecognizedProvisionerMessage), + Entry("recognized provisioner with unrecognized parameters", + "infinibox-csi-driver", map[string]string{"storage_protocol": "unsupported"}, + v1.ConditionFalse, string(storagecapabilities.UnrecognizedStorageClassParameters), unrecognizedStorageClassParametersMessage), + ) + }) func createStorageProfileReconciler(objects ...runtime.Object) *StorageProfileReconciler { diff --git a/pkg/operator/resources/crds_generated.go b/pkg/operator/resources/crds_generated.go index 4c355fdca0..6d3d74abad 100644 --- a/pkg/operator/resources/crds_generated.go +++ b/pkg/operator/resources/crds_generated.go @@ -7845,6 +7845,34 @@ spec: description: CloneStrategy defines the preferred method for performing a CDI clone type: string + conditions: + description: Conditions contains the current conditions observed for + the StorageProfile + items: + description: StorageProfileCondition represents the state of a storage + profile condition + properties: + lastHeartbeatTime: + format: date-time + type: string + lastTransitionTime: + format: date-time + type: string + message: + type: string + reason: + type: string + status: + type: string + type: + description: StorageProfileConditionType is the string representation + of known condition types + type: string + required: + - status + - type + type: object + type: array dataImportCronSourceFormat: description: DataImportCronSourceFormat defines the format of the DataImportCron-created disk image sources diff --git a/pkg/storagecapabilities/storagecapabilities.go b/pkg/storagecapabilities/storagecapabilities.go index a9fb344c2c..651a19c6b4 100644 --- a/pkg/storagecapabilities/storagecapabilities.go +++ b/pkg/storagecapabilities/storagecapabilities.go @@ -259,6 +259,27 @@ var UnsupportedProvisioners = map[string]struct{}{ storagehelpers.NotSupportedProvisioner: {}, } +// StorageClassRecognizeReason represents the reason for the storage class recognition condition +type StorageClassRecognizeReason string + +const ( + RecognizedProvisioner StorageClassRecognizeReason = "RecognizedProvisioner" + UnrecognizedProvisioner StorageClassRecognizeReason = "UnrecognizedProvisioner" + UnrecognizedStorageClassParameters StorageClassRecognizeReason = "UnrecognizedStorageClassParameters" +) + +// IsRecognized checks if the storage class provisioner and parameters are recognized so capabilities are available +func IsRecognized(sc *storagev1.StorageClass) StorageClassRecognizeReason { + provisionerKey := storageProvisionerKey(sc) + if provisionerKey == "UNKNOWN" { + return UnrecognizedStorageClassParameters + } + if _, found := CapabilitiesByProvisionerKey[provisionerKey]; found { + return RecognizedProvisioner + } + return UnrecognizedProvisioner +} + // GetCapabilities finds and returns a predefined StorageCapabilities for a given StorageClass func GetCapabilities(cl client.Client, sc *storagev1.StorageClass) ([]StorageCapabilities, bool) { provisionerKey := storageProvisionerKey(sc) diff --git a/staging/src/kubevirt.io/containerized-data-importer-api/pkg/apis/core/v1beta1/types.go b/staging/src/kubevirt.io/containerized-data-importer-api/pkg/apis/core/v1beta1/types.go index 3a473dd865..4890a48780 100644 --- a/staging/src/kubevirt.io/containerized-data-importer-api/pkg/apis/core/v1beta1/types.go +++ b/staging/src/kubevirt.io/containerized-data-importer-api/pkg/apis/core/v1beta1/types.go @@ -467,8 +467,24 @@ type StorageProfileStatus struct { DataImportCronSourceFormat *DataImportCronSourceFormat `json:"dataImportCronSourceFormat,omitempty"` // SnapshotClass is optional specific VolumeSnapshotClass for CloneStrategySnapshot. If not set, a VolumeSnapshotClass is chosen according to the provisioner. SnapshotClass *string `json:"snapshotClass,omitempty"` + // Conditions contains the current conditions observed for the StorageProfile + Conditions []StorageProfileCondition `json:"conditions,omitempty" optional:"true"` } +// StorageProfileCondition represents the state of a storage profile condition +type StorageProfileCondition struct { + Type StorageProfileConditionType `json:"type" description:"type of condition ie. Recognized"` + ConditionState `json:",inline"` +} + +// StorageProfileConditionType is the string representation of known condition types +type StorageProfileConditionType string + +const ( + // StorageProfileRecognized is the condition that indicates if the storage class provisioner and parameters are recognized by CDI + StorageProfileRecognized StorageProfileConditionType = "Recognized" +) + // ClaimPropertySet is a set of properties applicable to PVC type ClaimPropertySet struct { // AccessModes contains the desired access modes the volume should have. diff --git a/staging/src/kubevirt.io/containerized-data-importer-api/pkg/apis/core/v1beta1/types_swagger_generated.go b/staging/src/kubevirt.io/containerized-data-importer-api/pkg/apis/core/v1beta1/types_swagger_generated.go index 04c75cd7d9..ca3ecd819c 100644 --- a/staging/src/kubevirt.io/containerized-data-importer-api/pkg/apis/core/v1beta1/types_swagger_generated.go +++ b/staging/src/kubevirt.io/containerized-data-importer-api/pkg/apis/core/v1beta1/types_swagger_generated.go @@ -214,6 +214,13 @@ func (StorageProfileStatus) SwaggerDoc() map[string]string { "claimPropertySets": "ClaimPropertySets computed from the spec and detected in the system\n+kubebuilder:validation:MaxItems=8", "dataImportCronSourceFormat": "DataImportCronSourceFormat defines the format of the DataImportCron-created disk image sources", "snapshotClass": "SnapshotClass is optional specific VolumeSnapshotClass for CloneStrategySnapshot. If not set, a VolumeSnapshotClass is chosen according to the provisioner.", + "conditions": "Conditions contains the current conditions observed for the StorageProfile", + } +} + +func (StorageProfileCondition) SwaggerDoc() map[string]string { + return map[string]string{ + "": "StorageProfileCondition represents the state of a storage profile condition", } } diff --git a/staging/src/kubevirt.io/containerized-data-importer-api/pkg/apis/core/v1beta1/zz_generated.deepcopy.go b/staging/src/kubevirt.io/containerized-data-importer-api/pkg/apis/core/v1beta1/zz_generated.deepcopy.go index 571347ca16..45d26ca258 100644 --- a/staging/src/kubevirt.io/containerized-data-importer-api/pkg/apis/core/v1beta1/zz_generated.deepcopy.go +++ b/staging/src/kubevirt.io/containerized-data-importer-api/pkg/apis/core/v1beta1/zz_generated.deepcopy.go @@ -1694,6 +1694,23 @@ func (in *StorageProfile) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *StorageProfileCondition) DeepCopyInto(out *StorageProfileCondition) { + *out = *in + in.ConditionState.DeepCopyInto(&out.ConditionState) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new StorageProfileCondition. +func (in *StorageProfileCondition) DeepCopy() *StorageProfileCondition { + if in == nil { + return nil + } + out := new(StorageProfileCondition) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *StorageProfileList) DeepCopyInto(out *StorageProfileList) { *out = *in @@ -1800,6 +1817,13 @@ func (in *StorageProfileStatus) DeepCopyInto(out *StorageProfileStatus) { *out = new(string) **out = **in } + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]StorageProfileCondition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } return }