From 7118ad163bb46d5e69a8b396c818289f0667093c Mon Sep 17 00:00:00 2001 From: Vishnu Itta Date: Thu, 5 Mar 2020 15:38:21 +0530 Subject: [PATCH] feat(validation): add webhook validations for CVC replica scale (#1621) Signed-off-by: mittachaitu --- pkg/util/util.go | 14 + pkg/webhook/configuration.go | 53 ++- pkg/webhook/cvc.go | 200 +++++++++++ pkg/webhook/cvc_test.go | 631 +++++++++++++++++++++++++++++++++++ pkg/webhook/webhook.go | 15 + 5 files changed, 899 insertions(+), 14 deletions(-) create mode 100644 pkg/webhook/cvc.go create mode 100644 pkg/webhook/cvc_test.go diff --git a/pkg/util/util.go b/pkg/util/util.go index fa3feb533a..607bc07094 100644 --- a/pkg/util/util.go +++ b/pkg/util/util.go @@ -241,3 +241,17 @@ func IsChangeInLists(listA, listB []string) bool { } return false } + +// IsUniqueList returns true if values in list are not repeated else return +// false +func IsUniqueList(list []string) bool { + listMap := map[string]bool{} + + for _, str := range list { + if _, ok := listMap[str]; ok { + return false + } + listMap[str] = true + } + return true +} diff --git a/pkg/webhook/configuration.go b/pkg/webhook/configuration.go index a375ac5893..ddf259c755 100644 --- a/pkg/webhook/configuration.go +++ b/pkg/webhook/configuration.go @@ -73,6 +73,17 @@ var ( transformSvc = []transformSvcFunc{} transformConfig = []transformConfigFunc{ addCSPCDeleteRule, + addCVCWithUpdateRule, + } + cvcRuleWithOperations = v1beta1.RuleWithOperations{ + Operations: []v1beta1.OperationType{ + v1beta1.Update, + }, + Rule: v1beta1.Rule{ + APIGroups: []string{"*"}, + APIVersions: []string{"*"}, + Resources: []string{"cstorvolumeclaims"}, + }, } ) @@ -133,9 +144,9 @@ func createWebhookService( return err } -// createAdmissionService creates our ValidatingWebhookConfiguration resource +// createValidatingWebhookConfig creates our ValidatingWebhookConfiguration resource // if it does not exist. -func createAdmissionService( +func createValidatingWebhookConfig( ownerReference metav1.OwnerReference, validatorWebhook string, namespace string, @@ -160,17 +171,18 @@ func createAdmissionService( webhookHandler := v1beta1.ValidatingWebhook{ Name: webhookHandlerName, - Rules: []v1beta1.RuleWithOperations{{ - Operations: []v1beta1.OperationType{ - v1beta1.Create, - v1beta1.Delete, - }, - Rule: v1beta1.Rule{ - APIGroups: []string{"*"}, - APIVersions: []string{"*"}, - Resources: []string{"persistentvolumeclaims"}, + Rules: []v1beta1.RuleWithOperations{ + { + Operations: []v1beta1.OperationType{ + v1beta1.Create, + v1beta1.Delete, + }, + Rule: v1beta1.Rule{ + APIGroups: []string{"*"}, + APIVersions: []string{"*"}, + Resources: []string{"persistentvolumeclaims"}, + }, }, - }, { Operations: []v1beta1.OperationType{ v1beta1.Create, @@ -182,7 +194,9 @@ func createAdmissionService( APIVersions: []string{"*"}, Resources: []string{"cstorpoolclusters"}, }, - }}, + }, + cvcRuleWithOperations, + }, ClientConfig: v1beta1.WebhookClientConfig{ Service: &v1beta1.ServiceReference{ Namespace: namespace, @@ -356,7 +370,7 @@ func InitValidationServer( ) } - validatorErr := createAdmissionService( + validatorErr := createValidatingWebhookConfig( ownerReference, validatorWebhook, openebsNamespace, @@ -458,6 +472,17 @@ func addCSPCDeleteRule(config *v1beta1.ValidatingWebhookConfiguration) { } } +// addCVCWithUpdateRule adds the CVC webhook config with UPDATE operation if coming from +// previous versions +func addCVCWithUpdateRule(config *v1beta1.ValidatingWebhookConfiguration) { + if config.Labels[string(apis.OpenEBSVersionKey)] < "1.8.0" { + // Currenly we have only one webhook validation so CVC rule in under + // same webhook. + // https://github.com/openebs/maya/blob/9417d96abdaf41a2dbfcdbfb113fb73c83e6cf42/pkg/webhook/configuration.go#L212 + config.Webhooks[0].Rules = append(config.Webhooks[0].Rules, cvcRuleWithOperations) + } +} + // preUpgrade checks for the required older webhook configs,older // then 1.4.0 if exists delete them. func preUpgrade(openebsNamespace string) error { diff --git a/pkg/webhook/cvc.go b/pkg/webhook/cvc.go new file mode 100644 index 0000000000..cd858c5555 --- /dev/null +++ b/pkg/webhook/cvc.go @@ -0,0 +1,200 @@ +/* +Copyright 2020 The OpenEBS Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package webhook + +import ( + "encoding/json" + "net/http" + + apis "github.com/openebs/maya/pkg/apis/openebs.io/v1alpha1" + clientset "github.com/openebs/maya/pkg/client/generated/clientset/versioned" + cvc "github.com/openebs/maya/pkg/cstorvolumeclaim/v1alpha1" + util "github.com/openebs/maya/pkg/util" + "github.com/pkg/errors" + "k8s.io/api/admission/v1beta1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/klog" +) + +type validateFunc func(cvcOldObj, cvcNewObj *apis.CStorVolumeClaim) error + +type getCVC func(name, namespace string, clientset clientset.Interface) (*apis.CStorVolumeClaim, error) + +func (wh *webhook) validateCVCUpdateRequest(req *v1beta1.AdmissionRequest, getCVC getCVC) *v1beta1.AdmissionResponse { + response := NewAdmissionResponse(). + SetAllowed(). + WithResultAsSuccess(http.StatusAccepted).AR + var cvcNewObj apis.CStorVolumeClaim + err := json.Unmarshal(req.Object.Raw, &cvcNewObj) + if err != nil { + klog.Errorf("Couldn't unmarshal raw object: %v to cvc error: %v", req.Object.Raw, err) + response = BuildForAPIObject(response).UnSetAllowed().WithResultAsFailure(err, http.StatusBadRequest).AR + return response + } + + // Get old CVC object by making call to etcd + cvcOldObj, err := getCVC(cvcNewObj.Name, cvcNewObj.Namespace, wh.clientset) + if err != nil { + klog.Errorf("Failed to get CVC %s in namespace %s from etcd error: %v", cvcNewObj.Name, cvcNewObj.Namespace, err) + response = BuildForAPIObject(response).UnSetAllowed().WithResultAsFailure(err, http.StatusBadRequest).AR + return response + } + err = validateCVCSpecChanges(cvcOldObj, &cvcNewObj) + if err != nil { + klog.Errorf("invalid cvc changes: %s error: %s", cvcOldObj.Name, err.Error()) + response = BuildForAPIObject(response).UnSetAllowed().WithResultAsFailure(err, http.StatusBadRequest).AR + return response + } + return response +} + +func validateCVCSpecChanges(cvcOldObj, cvcNewObj *apis.CStorVolumeClaim) error { + validateFuncList := []validateFunc{validateReplicaCount, + validatePoolListChanges, + validateReplicaScaling, + } + for _, f := range validateFuncList { + err := f(cvcOldObj, cvcNewObj) + if err != nil { + return err + } + } + + // Below validations should be done only with new CVC object + err := validatePoolNames(cvcNewObj) + if err != nil { + return err + } + return nil +} + +// TODO: isScalingInProgress(cvcObj *apis.CStorVolumeClaim) signature need to be +// updated to cvcObj.IsScaleingInProgress() +func isScalingInProgress(cvcObj *apis.CStorVolumeClaim) bool { + return len(cvcObj.Spec.Policy.ReplicaPoolInfo) != len(cvcObj.Status.PoolInfo) +} + +// validateReplicaCount returns error if user modified the replica count after +// provisioning the volume else return nil +func validateReplicaCount(cvcOldObj, cvcNewObj *apis.CStorVolumeClaim) error { + if cvcOldObj.Spec.ReplicaCount != cvcNewObj.Spec.ReplicaCount { + return errors.Errorf( + "cvc %s replicaCount got modified from %d to %d", + cvcOldObj.Name, + cvcOldObj.Spec.ReplicaCount, + cvcNewObj.Spec.ReplicaCount, + ) + } + return nil +} + +// validatePoolListChanges returns error if user modified existing pool names with new +// pool name(s) or if user performed more than one replica scale down at a time +func validatePoolListChanges(cvcOldObj, cvcNewObj *apis.CStorVolumeClaim) error { + // Check the new CVC spec changes with old CVC status(Comparing with status + // is more appropriate than comparing with spec) + oldCurrentPoolNames := cvcOldObj.Status.PoolInfo + newDesiredPoolNames := cvc.GetDesiredReplicaPoolNames(cvcNewObj) + modifiedPoolNames := util.ListDiff(oldCurrentPoolNames, newDesiredPoolNames) + // Reject the request if someone perform scaling when CVC is not in Bound + // state + // NOTE: We should not reject the controller request which Updates status as + // Bound as well as pool info in status and spec + // TODO: Make below check as cvcOldObj.ISBound() + // If CVC Status is not bound then reject + if cvcOldObj.Status.Phase != apis.CStorVolumeClaimPhaseBound { + // If controller is updating pool info then new CVC will be in bound state + if cvcNewObj.Status.Phase != apis.CStorVolumeClaimPhaseBound && + // Performed scaling operation on CVC + len(oldCurrentPoolNames) != len(newDesiredPoolNames) { + return errors.Errorf( + "Can't perform scaling of volume replicas when CVC is not in %s state", + apis.CStorVolumeClaimPhaseBound, + ) + } + } + + // Validing Scaling process + if len(newDesiredPoolNames) >= len(oldCurrentPoolNames) { + // If no.of pools on new spec >= no.of pools in old status(scaleup as well + // as migration case then all the pools in old status must present in new + // spec) + if len(modifiedPoolNames) > 0 { + return errors.Errorf( + "volume replica migration directly by modifying pool names %v is not yet supported", + modifiedPoolNames, + ) + } + } else { + // If no.of pools in new spec < no.of pools in old status(scale down + // volume replica case) then there should at most one change in + // oldSpec.PoolInfo - newSpec.PoolInfo + if len(modifiedPoolNames) > 1 { + return errors.Errorf( + "Can't perform more than one replica scale down requested scale down count %d", + len(modifiedPoolNames), + ) + } + } + return nil +} + +// validateReplicaScaling returns error if user updated pool list when scaling is +// already in progress. +// Note: User can perform scaleup of multiple replicas by adding multiple pool +// names at time but not by updating CVC pool names with multiple edits. +func validateReplicaScaling(cvcOldObj, cvcNewObj *apis.CStorVolumeClaim) error { + if isScalingInProgress(cvcOldObj) { + // if old and new CVC has same count of pools then return true else + // return false + if len(cvcOldObj.Spec.Policy.ReplicaPoolInfo) != len(cvcNewObj.Spec.Policy.ReplicaPoolInfo) { + return errors.Errorf("scaling of CVC %s is already in progress", cvcOldObj.Name) + } + } + return nil +} + +// validatePoolNames returns error if there is repeatition of pool names either +// under spec or status of cvc +func validatePoolNames(cvcObj *apis.CStorVolumeClaim) error { + // TODO: Change cvcObj.GetDesiredReplicaPoolNames() + replicaPoolNames := cvc.GetDesiredReplicaPoolNames(cvcObj) + // Check repeatition of pool names under Spec of CVC Object + if !util.IsUniqueList(replicaPoolNames) { + return errors.Errorf( + "duplicate pool names %v found under spec of cvc %s", + replicaPoolNames, + cvcObj.Name, + ) + } + // Check repeatition of pool names under Status of CVC Object + if !util.IsUniqueList(cvcObj.Status.PoolInfo) { + return errors.Errorf( + "duplicate pool names %v found under status of cvc %s", + cvcObj.Status.PoolInfo, + cvcObj.Name, + ) + } + return nil +} + +func getCVCObject(name, namespace string, + clientset clientset.Interface) (*apis.CStorVolumeClaim, error) { + return clientset.OpenebsV1alpha1(). + CStorVolumeClaims(namespace). + Get(name, metav1.GetOptions{}) +} diff --git a/pkg/webhook/cvc_test.go b/pkg/webhook/cvc_test.go new file mode 100644 index 0000000000..1e3328e643 --- /dev/null +++ b/pkg/webhook/cvc_test.go @@ -0,0 +1,631 @@ +/* +Copyright 2020 The OpenEBS Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package webhook + +import ( + "testing" + + apis "github.com/openebs/maya/pkg/apis/openebs.io/v1alpha1" + clientset "github.com/openebs/maya/pkg/client/generated/clientset/versioned" + openebsFakeClientset "github.com/openebs/maya/pkg/client/generated/clientset/versioned/fake" + "github.com/pkg/errors" + "k8s.io/api/admission/v1beta1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" +) + +type fixture struct { + wh *webhook + openebsObjects []runtime.Object +} + +func newFixture() *fixture { + return &fixture{ + wh: &webhook{}, + } +} + +func (f *fixture) withOpenebsObjects(objects ...runtime.Object) *fixture { + f.openebsObjects = objects + f.wh.clientset = openebsFakeClientset.NewSimpleClientset(objects...) + return f +} + +func fakeGetCVCError(name, namespace string, clientset clientset.Interface) (*apis.CStorVolumeClaim, error) { + return nil, errors.Errorf("fake error") +} + +func TestValidateCVCUpdateRequest(t *testing.T) { + f := newFixture().withOpenebsObjects() + tests := map[string]struct { + // existingObj is object existing in etcd via fake client + existingObj *apis.CStorVolumeClaim + requestedObj *apis.CStorVolumeClaim + expectedRsp bool + getCVCObj getCVC + }{ + "When Failed to Get Object From etcd": { + existingObj: &apis.CStorVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cvc1", + Namespace: "openebs", + }, + }, + requestedObj: &apis.CStorVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cvc1", + Namespace: "openebs", + }, + Status: apis.CStorVolumeClaimStatus{ + Phase: apis.CStorVolumeClaimPhaseBound, + }, + }, + expectedRsp: false, + getCVCObj: fakeGetCVCError, + }, + "When ReplicaCount Updated": { + existingObj: &apis.CStorVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cvc2", + Namespace: "openebs", + }, + Spec: apis.CStorVolumeClaimSpec{ + ReplicaCount: 3, + }, + Status: apis.CStorVolumeClaimStatus{ + Phase: apis.CStorVolumeClaimPhaseBound, + }, + }, + requestedObj: &apis.CStorVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cvc2", + Namespace: "openebs", + }, + Spec: apis.CStorVolumeClaimSpec{ + ReplicaCount: 4, + }, + Status: apis.CStorVolumeClaimStatus{ + Phase: apis.CStorVolumeClaimPhaseBound, + }, + }, + expectedRsp: false, + getCVCObj: getCVCObject, + }, + "When Volume Boud Status Updated With Pool Info": { + existingObj: &apis.CStorVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cvc3", + Namespace: "openebs", + }, + Spec: apis.CStorVolumeClaimSpec{ + ReplicaCount: 3, + }, + }, + requestedObj: &apis.CStorVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cvc3", + Namespace: "openebs", + }, + Spec: apis.CStorVolumeClaimSpec{ + ReplicaCount: 3, + Policy: apis.CStorVolumePolicySpec{ + ReplicaPoolInfo: []apis.ReplicaPoolInfo{ + apis.ReplicaPoolInfo{PoolName: "pool1"}, + apis.ReplicaPoolInfo{PoolName: "pool2"}, + apis.ReplicaPoolInfo{PoolName: "pool3"}, + }, + }, + }, + Status: apis.CStorVolumeClaimStatus{ + Phase: apis.CStorVolumeClaimPhaseBound, + PoolInfo: []string{"pool1", "pool2", "pool3"}, + }, + }, + expectedRsp: true, + getCVCObj: getCVCObject, + }, + "When Volume Replcias were Scaled by modifying exisitng pool names": { + existingObj: &apis.CStorVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cvc4", + Namespace: "openebs", + }, + Spec: apis.CStorVolumeClaimSpec{ + ReplicaCount: 3, + Policy: apis.CStorVolumePolicySpec{ + ReplicaPoolInfo: []apis.ReplicaPoolInfo{ + apis.ReplicaPoolInfo{PoolName: "pool1"}, + apis.ReplicaPoolInfo{PoolName: "pool2"}, + apis.ReplicaPoolInfo{PoolName: "pool3"}, + }, + }, + }, + Status: apis.CStorVolumeClaimStatus{ + PoolInfo: []string{"pool1", "pool2", "pool3"}, + Phase: apis.CStorVolumeClaimPhaseBound, + }, + }, + requestedObj: &apis.CStorVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cvc4", + Namespace: "openebs", + }, + Spec: apis.CStorVolumeClaimSpec{ + ReplicaCount: 3, + Policy: apis.CStorVolumePolicySpec{ + ReplicaPoolInfo: []apis.ReplicaPoolInfo{ + apis.ReplicaPoolInfo{PoolName: "pool1"}, + apis.ReplicaPoolInfo{PoolName: "pool2"}, + apis.ReplicaPoolInfo{PoolName: "pool5"}, + apis.ReplicaPoolInfo{PoolName: "pool4"}, + }, + }, + }, + Status: apis.CStorVolumeClaimStatus{ + PoolInfo: []string{"pool1", "pool2", "pool3"}, + Phase: apis.CStorVolumeClaimPhaseBound, + }, + }, + expectedRsp: false, + getCVCObj: getCVCObject, + }, + "When Volume Replcias were migrated": { + existingObj: &apis.CStorVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cvc5", + Namespace: "openebs", + }, + Spec: apis.CStorVolumeClaimSpec{ + ReplicaCount: 3, + Policy: apis.CStorVolumePolicySpec{ + ReplicaPoolInfo: []apis.ReplicaPoolInfo{ + apis.ReplicaPoolInfo{PoolName: "pool1"}, + apis.ReplicaPoolInfo{PoolName: "pool2"}, + apis.ReplicaPoolInfo{PoolName: "pool3"}, + }, + }, + }, + Status: apis.CStorVolumeClaimStatus{ + PoolInfo: []string{"pool1", "pool2", "pool3"}, + Phase: apis.CStorVolumeClaimPhaseBound, + }, + }, + requestedObj: &apis.CStorVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cvc5", + Namespace: "openebs", + }, + Spec: apis.CStorVolumeClaimSpec{ + ReplicaCount: 3, + Policy: apis.CStorVolumePolicySpec{ + ReplicaPoolInfo: []apis.ReplicaPoolInfo{ + apis.ReplicaPoolInfo{PoolName: "pool0"}, + apis.ReplicaPoolInfo{PoolName: "pool2"}, + apis.ReplicaPoolInfo{PoolName: "pool5"}, + }, + }, + }, + Status: apis.CStorVolumeClaimStatus{ + PoolInfo: []string{"pool1", "pool2", "pool3"}, + Phase: apis.CStorVolumeClaimPhaseBound, + }, + }, + expectedRsp: false, + getCVCObj: getCVCObject, + }, + "When CVC Scaling Up InProgress Performing Scaling Again": { + existingObj: &apis.CStorVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cvc6", + Namespace: "openebs", + }, + Spec: apis.CStorVolumeClaimSpec{ + ReplicaCount: 3, + Policy: apis.CStorVolumePolicySpec{ + ReplicaPoolInfo: []apis.ReplicaPoolInfo{ + apis.ReplicaPoolInfo{PoolName: "pool1"}, + apis.ReplicaPoolInfo{PoolName: "pool2"}, + apis.ReplicaPoolInfo{PoolName: "pool3"}, + }, + }, + }, + Status: apis.CStorVolumeClaimStatus{ + PoolInfo: []string{"pool1", "pool2"}, + Phase: apis.CStorVolumeClaimPhaseBound, + }, + }, + requestedObj: &apis.CStorVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cvc6", + Namespace: "openebs", + }, + Spec: apis.CStorVolumeClaimSpec{ + ReplicaCount: 3, + Policy: apis.CStorVolumePolicySpec{ + ReplicaPoolInfo: []apis.ReplicaPoolInfo{ + apis.ReplicaPoolInfo{PoolName: "pool1"}, + apis.ReplicaPoolInfo{PoolName: "pool2"}, + apis.ReplicaPoolInfo{PoolName: "pool3"}, + apis.ReplicaPoolInfo{PoolName: "pool4"}, + }, + }, + }, + Status: apis.CStorVolumeClaimStatus{ + PoolInfo: []string{"pool1", "pool2"}, + Phase: apis.CStorVolumeClaimPhaseBound, + }, + }, + expectedRsp: false, + getCVCObj: getCVCObject, + }, + "When More Than One Replica Were Scale Down": { + existingObj: &apis.CStorVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cvc7", + Namespace: "openebs", + }, + Spec: apis.CStorVolumeClaimSpec{ + ReplicaCount: 3, + Policy: apis.CStorVolumePolicySpec{ + ReplicaPoolInfo: []apis.ReplicaPoolInfo{ + apis.ReplicaPoolInfo{PoolName: "pool1"}, + apis.ReplicaPoolInfo{PoolName: "pool2"}, + apis.ReplicaPoolInfo{PoolName: "pool3"}, + }, + }, + }, + Status: apis.CStorVolumeClaimStatus{ + PoolInfo: []string{"pool1", "pool2", "pool3"}, + Phase: apis.CStorVolumeClaimPhaseBound, + }, + }, + requestedObj: &apis.CStorVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cvc7", + Namespace: "openebs", + }, + Spec: apis.CStorVolumeClaimSpec{ + ReplicaCount: 3, + Policy: apis.CStorVolumePolicySpec{ + ReplicaPoolInfo: []apis.ReplicaPoolInfo{ + apis.ReplicaPoolInfo{PoolName: "pool1"}, + }, + }, + }, + Status: apis.CStorVolumeClaimStatus{ + PoolInfo: []string{"pool1", "pool2", "pool3"}, + Phase: apis.CStorVolumeClaimPhaseBound, + }, + }, + expectedRsp: false, + getCVCObj: getCVCObject, + }, + "When ScaleUp was Performed Before CVC In Bound State": { + existingObj: &apis.CStorVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cvc8", + Namespace: "openebs", + }, + Spec: apis.CStorVolumeClaimSpec{ + ReplicaCount: 3, + }, + }, + requestedObj: &apis.CStorVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cvc8", + Namespace: "openebs", + }, + Spec: apis.CStorVolumeClaimSpec{ + ReplicaCount: 3, + Policy: apis.CStorVolumePolicySpec{ + ReplicaPoolInfo: []apis.ReplicaPoolInfo{ + apis.ReplicaPoolInfo{PoolName: "pool1"}, + apis.ReplicaPoolInfo{PoolName: "pool2"}, + apis.ReplicaPoolInfo{PoolName: "pool3"}, + }, + }, + }, + }, + expectedRsp: false, + getCVCObj: getCVCObject, + }, + "When Scale Up Alone Performed": { + existingObj: &apis.CStorVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cvc10", + Namespace: "openebs", + }, + Spec: apis.CStorVolumeClaimSpec{ + ReplicaCount: 1, + Policy: apis.CStorVolumePolicySpec{ + ReplicaPoolInfo: []apis.ReplicaPoolInfo{ + apis.ReplicaPoolInfo{PoolName: "pool1"}, + }, + }, + }, + Status: apis.CStorVolumeClaimStatus{ + PoolInfo: []string{"pool1"}, + Phase: apis.CStorVolumeClaimPhaseBound, + }, + }, + requestedObj: &apis.CStorVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cvc10", + Namespace: "openebs", + }, + Spec: apis.CStorVolumeClaimSpec{ + ReplicaCount: 1, + Policy: apis.CStorVolumePolicySpec{ + ReplicaPoolInfo: []apis.ReplicaPoolInfo{ + apis.ReplicaPoolInfo{PoolName: "pool1"}, + apis.ReplicaPoolInfo{PoolName: "pool2"}, + apis.ReplicaPoolInfo{PoolName: "pool3"}, + }, + }, + }, + Status: apis.CStorVolumeClaimStatus{ + PoolInfo: []string{"pool1"}, + Phase: apis.CStorVolumeClaimPhaseBound, + }, + }, + expectedRsp: true, + getCVCObj: getCVCObject, + }, + "When Scale Down Alone Performed": { + existingObj: &apis.CStorVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cvc11", + Namespace: "openebs", + }, + Spec: apis.CStorVolumeClaimSpec{ + ReplicaCount: 1, + Policy: apis.CStorVolumePolicySpec{ + ReplicaPoolInfo: []apis.ReplicaPoolInfo{ + apis.ReplicaPoolInfo{PoolName: "pool1"}, + apis.ReplicaPoolInfo{PoolName: "pool2"}, + }, + }, + }, + Status: apis.CStorVolumeClaimStatus{ + PoolInfo: []string{"pool1", "pool2"}, + Phase: apis.CStorVolumeClaimPhaseBound, + }, + }, + requestedObj: &apis.CStorVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cvc11", + Namespace: "openebs", + }, + Spec: apis.CStorVolumeClaimSpec{ + ReplicaCount: 1, + Policy: apis.CStorVolumePolicySpec{ + ReplicaPoolInfo: []apis.ReplicaPoolInfo{ + apis.ReplicaPoolInfo{PoolName: "pool1"}, + }, + }, + }, + Status: apis.CStorVolumeClaimStatus{ + PoolInfo: []string{"pool1", "pool2"}, + Phase: apis.CStorVolumeClaimPhaseBound, + }, + }, + expectedRsp: true, + getCVCObj: getCVCObject, + }, + "When Scale Up Status Was Updated Success": { + existingObj: &apis.CStorVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cvc12", + Namespace: "openebs", + }, + Spec: apis.CStorVolumeClaimSpec{ + ReplicaCount: 1, + Policy: apis.CStorVolumePolicySpec{ + ReplicaPoolInfo: []apis.ReplicaPoolInfo{ + apis.ReplicaPoolInfo{PoolName: "pool1"}, + apis.ReplicaPoolInfo{PoolName: "pool2"}, + }, + }, + }, + Status: apis.CStorVolumeClaimStatus{ + PoolInfo: []string{"pool1"}, + Phase: apis.CStorVolumeClaimPhaseBound, + }, + }, + requestedObj: &apis.CStorVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cvc12", + Namespace: "openebs", + }, + Spec: apis.CStorVolumeClaimSpec{ + ReplicaCount: 1, + Policy: apis.CStorVolumePolicySpec{ + ReplicaPoolInfo: []apis.ReplicaPoolInfo{ + apis.ReplicaPoolInfo{PoolName: "pool1"}, + apis.ReplicaPoolInfo{PoolName: "pool2"}, + }, + }, + }, + Status: apis.CStorVolumeClaimStatus{ + PoolInfo: []string{"pool1", "pool2"}, + Phase: apis.CStorVolumeClaimPhaseBound, + }, + }, + expectedRsp: true, + getCVCObj: getCVCObject, + }, + "When Scale Down Status Was Updated Success": { + existingObj: &apis.CStorVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cvc13", + Namespace: "openebs", + }, + Spec: apis.CStorVolumeClaimSpec{ + ReplicaCount: 1, + Policy: apis.CStorVolumePolicySpec{ + ReplicaPoolInfo: []apis.ReplicaPoolInfo{ + apis.ReplicaPoolInfo{PoolName: "pool1"}, + }, + }, + }, + Status: apis.CStorVolumeClaimStatus{ + PoolInfo: []string{"pool1", "pool2"}, + Phase: apis.CStorVolumeClaimPhaseBound, + }, + }, + requestedObj: &apis.CStorVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cvc13", + Namespace: "openebs", + }, + Spec: apis.CStorVolumeClaimSpec{ + ReplicaCount: 1, + Policy: apis.CStorVolumePolicySpec{ + ReplicaPoolInfo: []apis.ReplicaPoolInfo{ + apis.ReplicaPoolInfo{PoolName: "pool1"}, + }, + }, + }, + Status: apis.CStorVolumeClaimStatus{ + PoolInfo: []string{"pool1"}, + Phase: apis.CStorVolumeClaimPhaseBound, + }, + }, + expectedRsp: true, + getCVCObj: getCVCObject, + }, + "When CVC Spec Pool Names Were Repeated": { + existingObj: &apis.CStorVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cvc14", + Namespace: "openebs", + }, + Spec: apis.CStorVolumeClaimSpec{ + ReplicaCount: 1, + Policy: apis.CStorVolumePolicySpec{ + ReplicaPoolInfo: []apis.ReplicaPoolInfo{ + apis.ReplicaPoolInfo{PoolName: "pool1"}, + apis.ReplicaPoolInfo{PoolName: "pool2"}, + }, + }, + }, + Status: apis.CStorVolumeClaimStatus{ + PoolInfo: []string{"pool1", "pool2"}, + Phase: apis.CStorVolumeClaimPhaseBound, + }, + }, + requestedObj: &apis.CStorVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cvc14", + Namespace: "openebs", + }, + Spec: apis.CStorVolumeClaimSpec{ + ReplicaCount: 1, + Policy: apis.CStorVolumePolicySpec{ + ReplicaPoolInfo: []apis.ReplicaPoolInfo{ + apis.ReplicaPoolInfo{PoolName: "pool1"}, + apis.ReplicaPoolInfo{PoolName: "pool2"}, + apis.ReplicaPoolInfo{PoolName: "pool1"}, + }, + }, + }, + Status: apis.CStorVolumeClaimStatus{ + PoolInfo: []string{"pool1", "pool2"}, + Phase: apis.CStorVolumeClaimPhaseBound, + }, + }, + expectedRsp: false, + getCVCObj: getCVCObject, + }, + "When CVC Status Pool Names Were Repeated": { + existingObj: &apis.CStorVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cvc15", + Namespace: "openebs", + }, + Spec: apis.CStorVolumeClaimSpec{ + ReplicaCount: 1, + Policy: apis.CStorVolumePolicySpec{ + ReplicaPoolInfo: []apis.ReplicaPoolInfo{ + apis.ReplicaPoolInfo{PoolName: "pool1"}, + apis.ReplicaPoolInfo{PoolName: "pool2"}, + apis.ReplicaPoolInfo{PoolName: "pool3"}, + }, + }, + }, + Status: apis.CStorVolumeClaimStatus{ + PoolInfo: []string{"pool1", "pool2"}, + Phase: apis.CStorVolumeClaimPhaseBound, + }, + }, + requestedObj: &apis.CStorVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cvc15", + Namespace: "openebs", + }, + Spec: apis.CStorVolumeClaimSpec{ + ReplicaCount: 1, + Policy: apis.CStorVolumePolicySpec{ + ReplicaPoolInfo: []apis.ReplicaPoolInfo{ + apis.ReplicaPoolInfo{PoolName: "pool1"}, + apis.ReplicaPoolInfo{PoolName: "pool2"}, + apis.ReplicaPoolInfo{PoolName: "pool3"}, + }, + }, + }, + Status: apis.CStorVolumeClaimStatus{ + PoolInfo: []string{"pool1", "pool2", "pool2"}, + Phase: apis.CStorVolumeClaimPhaseBound, + }, + }, + expectedRsp: false, + getCVCObj: getCVCObject, + }, + } + for name, test := range tests { + name, test := name, test + t.Run(name, func(t *testing.T) { + ar := &v1beta1.AdmissionRequest{ + Operation: v1beta1.Create, + Object: runtime.RawExtension{ + Raw: serialize(test.requestedObj), + }, + } + // Create fake object in etcd + _, err := f.wh.clientset.OpenebsV1alpha1(). + CStorVolumeClaims(test.existingObj.Namespace). + Create(test.existingObj) + if err != nil { + t.Fatalf( + "failed to create fake CVC %s Object in Namespace %s error: %v", + test.existingObj.Name, + test.existingObj.Namespace, + err, + ) + } + resp := f.wh.validateCVCUpdateRequest(ar, test.getCVCObj) + if resp.Allowed != test.expectedRsp { + t.Errorf( + "%s test case failed expected response: %t but got %t error: %s", + name, + test.expectedRsp, + resp.Allowed, + resp.Result.Message, + ) + } + }) + } +} diff --git a/pkg/webhook/webhook.go b/pkg/webhook/webhook.go index 3d799e779c..1554be15ce 100644 --- a/pkg/webhook/webhook.go +++ b/pkg/webhook/webhook.go @@ -388,6 +388,8 @@ func (wh *webhook) validate(ar *v1beta1.AdmissionReview) *v1beta1.AdmissionRespo case "CStorPoolCluster": klog.V(2).Infof("Admission webhook request for type %s", req.Kind.Kind) return wh.validateCSPC(ar) + case "CStorVolumeClaim": + return wh.validateCVC(ar) default: klog.V(2).Infof("Admission webhook not configured for type %s", req.Kind.Kind) return response @@ -475,3 +477,16 @@ func (wh *webhook) Serve(w http.ResponseWriter, r *http.Request) { http.Error(w, fmt.Sprintf("could not write response: %v", err), http.StatusInternalServerError) } } + +func (wh *webhook) validateCVC(ar *v1beta1.AdmissionReview) *v1beta1.AdmissionResponse { + req := ar.Request + response := &v1beta1.AdmissionResponse{} + response.Allowed = true + // validates only if requested operation is UPDATE + if req.Operation == v1beta1.Update { + return wh.validateCVCUpdateRequest(req, getCVCObject) + } + klog.V(4).Info("Admission wehbook for CVC module not " + + "configured for operations other than UPDATE") + return response +}