Repository: kubernetes-csi/external-snapshotter (go)

Issue:
Webhook to ensure single default VolumeSnapshotClass
**Is your feature request related to a problem?/Why is this needed**

When users create two or more default VolumeSnapshotClasses, any subsequent VolumeSnapshot they create is then pending with `Failed to set default snapshot class with error 2 default snapshot classes were found` event.

It would be cool to have an admission webhook that would prevent creation / update of the second default VolumeSnapshotClass.

**Describe the solution you'd like in detail**
A simple webhook that makes sure that there is only one default VolumeSnapshotClass for a given CSI driver, returning a nice error to users when they try to make two default snapshot classes.

**Describe alternatives you've considered**

**Additional context**

Suggested workflow for changing the default VolumeSnapshotClass for a CSI driver would be:

1. Delete the defaulting annotation from the "old" default VolumeSnapshotClass.
2. Add the annotation to the "new" default VolumeSnapshotClass.

As you can see, there is a (brief?) moment when the snapshot-controller will not see any default class and it won't be able to process VolumeSnapshots that don't have any class set (yet). I *assume* the controller will re-sync these snapshots after step 2. is completed and file the new default VolumeSnapshotClass in them. Please test this scenario carefully.

Pull request description:
Add default validation for VolumeSnapshotClass
<!--  Thanks for sending a pull request!  Here are some tips for you:

1. If this is your first time, please read our contributor guidelines: https://git.k8s.io/community/contributors/guide#your-first-contribution and developer guide https://git.k8s.io/community/contributors/devel/development.md#development-guide
2. Follow the instructions for writing a release note: https://git.k8s.io/community/contributors/guide/release-notes.md
3. If the PR is unfinished, see how to mark it: https://git.k8s.io/community/contributors/guide/pull-requests.md#marking-unfinished-pull-requests
-->

**What type of PR is this?**
> Uncomment only one ` /kind <>` line, hit enter to put that in a new line, and remove leading whitespaces from that line:
>
> /kind api-change
/kind bug
> /kind cleanup
> /kind design
> /kind documentation
> /kind failing-test
> /kind feature
> /kind flake

**What this PR does / why we need it**:
Adding validation so that a user can not create or update to a situation where they have more than default VolumeSnapshotClass

**Which issue(s) this PR fixes**:
<!--
*Automatically closes linked issue when PR is merged.
Usage: `Fixes #<issue number>`, or `Fixes (paste link of issue)`.
_If PR is about `failing-tests or flakes`, please post the related issues/tests in a comment and do not use `Fixes`_*
-->
Fixes #667 
**Special notes for your reviewer**:

**Does this PR introduce a user-facing change?**:
<!--
If no, just write "NONE" in the release-note block below.
If yes, a release note is required:
Enter your extended release note in the block below. If the PR requires additional action from users switching to the new release, include the string "action required".
-->
```release-note
Adding validation for VolumeSnapshotClass to only have a single default for a particular driver. 
```


Patch:
diff --git a/pkg/validation-webhook/scheme.go b/pkg/validation-webhook/scheme.go
index 6ca79c097..538432a0c 100644
--- a/pkg/validation-webhook/scheme.go
+++ b/pkg/validation-webhook/scheme.go
@@ -17,6 +17,7 @@ limitations under the License.
 package webhook
 
 import (
+	snapshot "github.com/kubernetes-csi/external-snapshotter/client/v6/clientset/versioned/scheme"
 	admissionv1 "k8s.io/api/admission/v1"
 	admissionv1beta1 "k8s.io/api/admission/v1beta1"
 	admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
@@ -40,4 +41,5 @@ func addToScheme(scheme *runtime.Scheme) {
 	utilruntime.Must(admissionregistrationv1beta1.AddToScheme(scheme))
 	utilruntime.Must(admissionv1.AddToScheme(scheme))
 	utilruntime.Must(admissionregistrationv1.AddToScheme(scheme))
+	utilruntime.Must(snapshot.AddToScheme(scheme))
 }
diff --git a/pkg/validation-webhook/snapshot.go b/pkg/validation-webhook/snapshot.go
index ccb1e0a8a..cd4eaa185 100644
--- a/pkg/validation-webhook/snapshot.go
+++ b/pkg/validation-webhook/snapshot.go
@@ -22,8 +22,11 @@ import (
 
 	volumesnapshotv1 "github.com/kubernetes-csi/external-snapshotter/client/v6/apis/volumesnapshot/v1"
 	volumesnapshotv1beta1 "github.com/kubernetes-csi/external-snapshotter/client/v6/apis/volumesnapshot/v1beta1"
+	storagelisters "github.com/kubernetes-csi/external-snapshotter/client/v6/listers/volumesnapshot/v1"
+	"github.com/kubernetes-csi/external-snapshotter/v6/pkg/utils"
 	v1 "k8s.io/api/admission/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/labels"
 	"k8s.io/klog/v2"
 )
 
@@ -36,10 +39,26 @@ var (
 	SnapshotContentV1Beta1GVR = metav1.GroupVersionResource{Group: volumesnapshotv1beta1.GroupName, Version: "v1beta1", Resource: "volumesnapshotcontents"}
 	// SnapshotContentV1GVR is GroupVersionResource for v1 VolumeSnapshotContents
 	SnapshotContentV1GVR = metav1.GroupVersionResource{Group: volumesnapshotv1.GroupName, Version: "v1", Resource: "volumesnapshotcontents"}
+	// SnapshotContentV1GVR is GroupVersionResource for v1 VolumeSnapshotContents
+	SnapshotClassV1GVR = metav1.GroupVersionResource{Group: volumesnapshotv1.GroupName, Version: "v1", Resource: "volumesnapshotclasses"}
 )
 
+type SnapshotAdmitter interface {
+	Admit(v1.AdmissionReview) *v1.AdmissionResponse
+}
+
+type admitter struct {
+	lister storagelisters.VolumeSnapshotClassLister
+}
+
+func NewSnapshotAdmitter(lister storagelisters.VolumeSnapshotClassLister) SnapshotAdmitter {
+	return &admitter{
+		lister: lister,
+	}
+}
+
 // Add a label {"added-label": "yes"} to the object
-func admitSnapshot(ar v1.AdmissionReview) *v1.AdmissionResponse {
+func (a admitter) Admit(ar v1.AdmissionReview) *v1.AdmissionResponse {
 	klog.V(2).Info("admitting volumesnapshots or volumesnapshotcontents")
 
 	reviewResponse := &v1.AdmissionResponse{
@@ -106,6 +125,18 @@ func admitSnapshot(ar v1.AdmissionReview) *v1.AdmissionResponse {
 			return toV1AdmissionResponse(err)
 		}
 		return decideSnapshotContentV1(snapcontent, oldSnapcontent, isUpdate)
+	case SnapshotClassV1GVR:
+		snapClass := &volumesnapshotv1.VolumeSnapshotClass{}
+		if _, _, err := deserializer.Decode(raw, nil, snapClass); err != nil {
+			klog.Error(err)
+			return toV1AdmissionResponse(err)
+		}
+		oldSnapClass := &volumesnapshotv1.VolumeSnapshotClass{}
+		if _, _, err := deserializer.Decode(oldRaw, nil, oldSnapClass); err != nil {
+			klog.Error(err)
+			return toV1AdmissionResponse(err)
+		}
+		return decideSnapshotClassV1(snapClass, oldSnapClass, a.lister)
 	default:
 		err := fmt.Errorf("expect resource to be %s or %s", SnapshotV1Beta1GVR, SnapshotContentV1Beta1GVR)
 		klog.Error(err)
@@ -223,6 +254,43 @@ func decideSnapshotContentV1(snapcontent, oldSnapcontent *volumesnapshotv1.Volum
 	return reviewResponse
 }
 
+func decideSnapshotClassV1(snapClass, oldSnapClass *volumesnapshotv1.VolumeSnapshotClass, lister storagelisters.VolumeSnapshotClassLister) *v1.AdmissionResponse {
+	reviewResponse := &v1.AdmissionResponse{
+		Allowed: true,
+		Result:  &metav1.Status{},
+	}
+
+	// Only Validate when a new snapClass is being set as a default.
+	if snapClass.Annotations[utils.IsDefaultSnapshotClassAnnotation] != "true" {
+		return reviewResponse
+	}
+
+	// If Old snapshot class has this, then we can assume that it was validated if driver is the same.
+	if oldSnapClass.Annotations[utils.IsDefaultSnapshotClassAnnotation] == "true" && oldSnapClass.Driver == snapClass.Driver {
+		return reviewResponse
+	}
+
+	ret, err := lister.List(labels.Everything())
+	if err != nil {
+		reviewResponse.Allowed = false
+		reviewResponse.Result.Message = err.Error()
+		return reviewResponse
+	}
+
+	for _, snapshotClass := range ret {
+		if snapshotClass.Annotations[utils.IsDefaultSnapshotClassAnnotation] != "true" {
+			continue
+		}
+		if snapshotClass.Driver == snapClass.Driver {
+			reviewResponse.Allowed = false
+			reviewResponse.Result.Message = fmt.Sprintf("default snapshot class: %v already exits for driver: %v", snapshotClass.Name, snapClass.Driver)
+			return reviewResponse
+		}
+	}
+
+	return reviewResponse
+}
+
 func strPtrDereference(s *string) string {
 	if s == nil {
 		return "<nil string pointer>"
diff --git a/pkg/validation-webhook/webhook.go b/pkg/validation-webhook/webhook.go
index 6a568f8a1..1e00f692a 100644
--- a/pkg/validation-webhook/webhook.go
+++ b/pkg/validation-webhook/webhook.go
@@ -23,19 +23,26 @@ import (
 	"fmt"
 	"io/ioutil"
 	"net/http"
+	"os"
 
+	clientset "github.com/kubernetes-csi/external-snapshotter/client/v6/clientset/versioned"
+	storagelisters "github.com/kubernetes-csi/external-snapshotter/client/v6/listers/volumesnapshot/v1"
 	"github.com/spf13/cobra"
 
+	informers "github.com/kubernetes-csi/external-snapshotter/client/v6/informers/externalversions"
 	v1 "k8s.io/api/admission/v1"
 	"k8s.io/api/admission/v1beta1"
 	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/client-go/rest"
+	"k8s.io/client-go/tools/clientcmd"
 	"k8s.io/klog/v2"
 )
 
 var (
-	certFile string
-	keyFile  string
-	port     int
+	certFile       string
+	keyFile        string
+	kubeconfigFile string
+	port           int
 )
 
 // CmdWebhook is used by Cobra.
@@ -58,6 +65,8 @@ func init() {
 		"Secure port that the webhook listens on")
 	CmdWebhook.MarkFlagRequired("tls-cert-file")
 	CmdWebhook.MarkFlagRequired("tls-private-key-file")
+	// Add optional flag for kubeconfig
+	CmdWebhook.Flags().StringVar(&kubeconfigFile, "kubeconfig", "", "kubeconfig file to use for volumesnapshotclasses")
 }
 
 // admitv1beta1Func handles a v1beta1 admission
@@ -68,14 +77,12 @@ type admitv1Func func(v1.AdmissionReview) *v1.AdmissionResponse
 
 // admitHandler is a handler, for both validators and mutators, that supports multiple admission review versions
 type admitHandler struct {
-	v1beta1 admitv1beta1Func
-	v1      admitv1Func
+	SnapshotAdmitter
 }
 
-func newDelegateToV1AdmitHandler(f admitv1Func) admitHandler {
+func newDelegateToV1AdmitHandler(sa SnapshotAdmitter) admitHandler {
 	return admitHandler{
-		v1beta1: delegateV1beta1AdmitToV1(f),
-		v1:      f,
+		SnapshotAdmitter: sa,
 	}
 }
 
@@ -137,7 +144,7 @@ func serve(w http.ResponseWriter, r *http.Request, admit admitHandler) {
 		}
 		responseAdmissionReview := &v1beta1.AdmissionReview{}
 		responseAdmissionReview.SetGroupVersionKind(*gvk)
-		responseAdmissionReview.Response = admit.v1beta1(*requestedAdmissionReview)
+		responseAdmissionReview.Response = delegateV1beta1AdmitToV1(admit.Admit)(*requestedAdmissionReview)
 		responseAdmissionReview.Response.UID = requestedAdmissionReview.Request.UID
 		responseObj = responseAdmissionReview
 	case v1.SchemeGroupVersion.WithKind("AdmissionReview"):
@@ -150,7 +157,7 @@ func serve(w http.ResponseWriter, r *http.Request, admit admitHandler) {
 		}
 		responseAdmissionReview := &v1.AdmissionReview{}
 		responseAdmissionReview.SetGroupVersionKind(*gvk)
-		responseAdmissionReview.Response = admit.v1(*requestedAdmissionReview)
+		responseAdmissionReview.Response = admit.Admit(*requestedAdmissionReview)
 		responseAdmissionReview.Response.UID = requestedAdmissionReview.Request.UID
 		responseObj = responseAdmissionReview
 	default:
@@ -173,21 +180,29 @@ func serve(w http.ResponseWriter, r *http.Request, admit admitHandler) {
 	}
 }
 
-func serveSnapshotRequest(w http.ResponseWriter, r *http.Request) {
-	serve(w, r, newDelegateToV1AdmitHandler(admitSnapshot))
+type serveWebhook struct {
+	lister storagelisters.VolumeSnapshotClassLister
 }
 
-func startServer(ctx context.Context, tlsConfig *tls.Config, cw *CertWatcher) error {
+func (s serveWebhook) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	serve(w, r, newDelegateToV1AdmitHandler(NewSnapshotAdmitter(s.lister)))
+}
+
+func startServer(ctx context.Context, tlsConfig *tls.Config, cw *CertWatcher, lister storagelisters.VolumeSnapshotClassLister) error {
 	go func() {
 		klog.Info("Starting certificate watcher")
 		if err := cw.Start(ctx); err != nil {
 			klog.Errorf("certificate watcher error: %v", err)
 		}
 	}()
+	// Pipe through the informer at some point here.
+	s := &serveWebhook{
+		lister: lister,
+	}
 
 	fmt.Println("Starting webhook server")
 	mux := http.NewServeMux()
-	mux.HandleFunc("/volumesnapshot", serveSnapshotRequest)
+	mux.Handle("/volumesnapshot", s)
 	mux.HandleFunc("/readyz", func(w http.ResponseWriter, req *http.Request) { w.Write([]byte("ok")) })
 	srv := &http.Server{
 		Handler:   mux,
@@ -215,7 +230,35 @@ func main(cmd *cobra.Command, args []string) {
 		GetCertificate: cw.GetCertificate,
 	}
 
-	if err := startServer(ctx, tlsConfig, cw); err != nil {
+	// Create an indexer.
+	// Create the client config. Use kubeconfig if given, otherwise assume in-cluster.
+	config, err := buildConfig(kubeconfigFile)
+	if err != nil {
+		klog.Error(err.Error())
+		os.Exit(1)
+	}
+	snapClient, err := clientset.NewForConfig(config)
+	if err != nil {
+		klog.Errorf("Error building snapshot clientset: %s", err.Error())
+		os.Exit(1)
+	}
+
+	factory := informers.NewSharedInformerFactory(snapClient, 0)
+	lister := factory.Snapshot().V1().VolumeSnapshotClasses().Lister()
+
+	//Start the informers
+	factory.Start(ctx.Done())
+	//wait for the caches to sync
+	factory.WaitForCacheSync(ctx.Done())
+
+	if err := startServer(ctx, tlsConfig, cw, lister); err != nil {
 		klog.Fatalf("server stopped: %v", err)
 	}
 }
+
+func buildConfig(kubeconfig string) (*rest.Config, error) {
+	if kubeconfig != "" {
+		return clientcmd.BuildConfigFromFlags("", kubeconfig)
+	}
+	return rest.InClusterConfig()
+}


Test patch:
diff --git a/pkg/validation-webhook/snapshot_test.go b/pkg/validation-webhook/snapshot_test.go
index 078d3d8ea..796a067f2 100644
--- a/pkg/validation-webhook/snapshot_test.go
+++ b/pkg/validation-webhook/snapshot_test.go
@@ -23,8 +23,12 @@ import (
 
 	volumesnapshotv1 "github.com/kubernetes-csi/external-snapshotter/client/v6/apis/volumesnapshot/v1"
 	volumesnapshotv1beta1 "github.com/kubernetes-csi/external-snapshotter/client/v6/apis/volumesnapshot/v1beta1"
+	storagelisters "github.com/kubernetes-csi/external-snapshotter/client/v6/listers/volumesnapshot/v1"
+	"github.com/kubernetes-csi/external-snapshotter/v6/pkg/utils"
 	v1 "k8s.io/api/admission/v1"
 	core_v1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/labels"
 	"k8s.io/apimachinery/pkg/runtime"
 )
 
@@ -208,7 +212,8 @@ func TestAdmitVolumeSnapshotV1beta1(t *testing.T) {
 					Operation: tc.operation,
 				},
 			}
-			response := admitSnapshot(review)
+			sa := NewSnapshotAdmitter(nil)
+			response := sa.Admit(review)
 			shouldAdmit := response.Allowed
 			msg := response.Result.Message
 
@@ -391,7 +396,8 @@ func TestAdmitVolumeSnapshotV1(t *testing.T) {
 					Operation: tc.operation,
 				},
 			}
-			response := admitSnapshot(review)
+			sa := NewSnapshotAdmitter(nil)
+			response := sa.Admit(review)
 			shouldAdmit := response.Allowed
 			msg := response.Result.Message
 
@@ -541,7 +547,8 @@ func TestAdmitVolumeSnapshotContentV1beta1(t *testing.T) {
 					Operation: tc.operation,
 				},
 			}
-			response := admitSnapshot(review)
+			sa := NewSnapshotAdmitter(nil)
+			response := sa.Admit(review)
 			shouldAdmit := response.Allowed
 			msg := response.Result.Message
 
@@ -685,7 +692,293 @@ func TestAdmitVolumeSnapshotContentV1(t *testing.T) {
 					Operation: tc.operation,
 				},
 			}
-			response := admitSnapshot(review)
+			sa := NewSnapshotAdmitter(nil)
+			response := sa.Admit(review)
+			shouldAdmit := response.Allowed
+			msg := response.Result.Message
+
+			expectedResponse := tc.shouldAdmit
+			expectedMsg := tc.msg
+
+			if shouldAdmit != expectedResponse {
+				t.Errorf("expected \"%v\" to equal \"%v\"", shouldAdmit, expectedResponse)
+			}
+			if msg != expectedMsg {
+				t.Errorf("expected \"%v\" to equal \"%v\"", msg, expectedMsg)
+			}
+		})
+	}
+}
+
+type fakeSnapshotLister struct {
+	values []*volumesnapshotv1.VolumeSnapshotClass
+}
+
+func (f *fakeSnapshotLister) List(selector labels.Selector) (ret []*volumesnapshotv1.VolumeSnapshotClass, err error) {
+	return f.values, nil
+}
+
+func (f *fakeSnapshotLister) Get(name string) (*volumesnapshotv1.VolumeSnapshotClass, error) {
+	for _, v := range f.values {
+		if v.Name == name {
+			return v, nil
+		}
+	}
+	return nil, nil
+}
+
+func TestAdmitVolumeSnapshotClassV1(t *testing.T) {
+	testCases := []struct {
+		name                   string
+		volumeSnapshotClass    *volumesnapshotv1.VolumeSnapshotClass
+		oldVolumeSnapshotClass *volumesnapshotv1.VolumeSnapshotClass
+		shouldAdmit            bool
+		msg                    string
+		operation              v1.Operation
+		lister                 storagelisters.VolumeSnapshotClassLister
+	}{
+		{
+			name: "new default for class with no existing classes",
+			volumeSnapshotClass: &volumesnapshotv1.VolumeSnapshotClass{
+				TypeMeta: metav1.TypeMeta{},
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{
+						utils.IsDefaultSnapshotClassAnnotation: "true",
+					},
+				},
+				Driver: "test.csi.io",
+			},
+			oldVolumeSnapshotClass: &volumesnapshotv1.VolumeSnapshotClass{},
+			shouldAdmit:            true,
+			msg:                    "",
+			operation:              v1.Create,
+			lister:                 &fakeSnapshotLister{values: []*volumesnapshotv1.VolumeSnapshotClass{}},
+		},
+		{
+			name: "new default for class for  with existing default class different drivers",
+			volumeSnapshotClass: &volumesnapshotv1.VolumeSnapshotClass{
+				TypeMeta: metav1.TypeMeta{},
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{
+						utils.IsDefaultSnapshotClassAnnotation: "true",
+					},
+				},
+				Driver: "test.csi.io",
+			},
+			oldVolumeSnapshotClass: &volumesnapshotv1.VolumeSnapshotClass{},
+			shouldAdmit:            true,
+			msg:                    "",
+			operation:              v1.Create,
+			lister: &fakeSnapshotLister{values: []*volumesnapshotv1.VolumeSnapshotClass{
+				{
+					TypeMeta: metav1.TypeMeta{},
+					ObjectMeta: metav1.ObjectMeta{
+						Annotations: map[string]string{
+							utils.IsDefaultSnapshotClassAnnotation: "true",
+						},
+					},
+					Driver: "existing.test.csi.io",
+				},
+			}},
+		},
+		{
+			name: "new default for class with existing default class same driver",
+			volumeSnapshotClass: &volumesnapshotv1.VolumeSnapshotClass{
+				TypeMeta: metav1.TypeMeta{},
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{
+						utils.IsDefaultSnapshotClassAnnotation: "true",
+					},
+				},
+				Driver: "test.csi.io",
+			},
+			oldVolumeSnapshotClass: &volumesnapshotv1.VolumeSnapshotClass{},
+			shouldAdmit:            false,
+			msg:                    "default snapshot class: driver-a already exits for driver: test.csi.io",
+			operation:              v1.Create,
+			lister: &fakeSnapshotLister{values: []*volumesnapshotv1.VolumeSnapshotClass{
+				{
+					TypeMeta: metav1.TypeMeta{},
+					ObjectMeta: metav1.ObjectMeta{
+						Name: "driver-a",
+						Annotations: map[string]string{
+							utils.IsDefaultSnapshotClassAnnotation: "true",
+						},
+					},
+					Driver: "test.csi.io",
+				},
+			}},
+		},
+		{
+			name: "default for class with existing default class same driver update",
+			volumeSnapshotClass: &volumesnapshotv1.VolumeSnapshotClass{
+				TypeMeta: metav1.TypeMeta{},
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{
+						utils.IsDefaultSnapshotClassAnnotation: "true",
+					},
+				},
+				Driver: "test.csi.io",
+			},
+			oldVolumeSnapshotClass: &volumesnapshotv1.VolumeSnapshotClass{
+				TypeMeta: metav1.TypeMeta{},
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{
+						utils.IsDefaultSnapshotClassAnnotation: "true",
+					},
+				},
+				Driver: "test.csi.io",
+			},
+			shouldAdmit: true,
+			msg:         "",
+			operation:   v1.Update,
+			lister: &fakeSnapshotLister{values: []*volumesnapshotv1.VolumeSnapshotClass{
+				{
+					TypeMeta: metav1.TypeMeta{},
+					ObjectMeta: metav1.ObjectMeta{
+						Annotations: map[string]string{
+							utils.IsDefaultSnapshotClassAnnotation: "true",
+						},
+					},
+					Driver: "test.csi.io",
+				},
+			}},
+		},
+		{
+			name: "new snapshot for class with existing default class same driver",
+			volumeSnapshotClass: &volumesnapshotv1.VolumeSnapshotClass{
+				TypeMeta:   metav1.TypeMeta{},
+				ObjectMeta: metav1.ObjectMeta{},
+				Driver:     "test.csi.io",
+			},
+			oldVolumeSnapshotClass: &volumesnapshotv1.VolumeSnapshotClass{},
+			shouldAdmit:            true,
+			msg:                    "",
+			operation:              v1.Create,
+			lister: &fakeSnapshotLister{values: []*volumesnapshotv1.VolumeSnapshotClass{
+				{
+					TypeMeta: metav1.TypeMeta{},
+					ObjectMeta: metav1.ObjectMeta{
+						Annotations: map[string]string{
+							utils.IsDefaultSnapshotClassAnnotation: "true",
+						},
+					},
+					Driver: "test.csi.io",
+				},
+			}},
+		},
+		{
+			name: "new snapshot for class with existing default classes",
+			volumeSnapshotClass: &volumesnapshotv1.VolumeSnapshotClass{
+				TypeMeta: metav1.TypeMeta{},
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{
+						utils.IsDefaultSnapshotClassAnnotation: "true",
+					},
+				},
+				Driver: "test.csi.io",
+			},
+			oldVolumeSnapshotClass: &volumesnapshotv1.VolumeSnapshotClass{},
+			shouldAdmit:            false,
+			msg:                    "default snapshot class: driver-is-default already exits for driver: test.csi.io",
+			operation:              v1.Create,
+			lister: &fakeSnapshotLister{values: []*volumesnapshotv1.VolumeSnapshotClass{
+				{
+					TypeMeta: metav1.TypeMeta{},
+					ObjectMeta: metav1.ObjectMeta{
+						Name: "driver-is-default",
+						Annotations: map[string]string{
+							utils.IsDefaultSnapshotClassAnnotation: "true",
+						},
+					},
+					Driver: "test.csi.io",
+				},
+				{
+					TypeMeta: metav1.TypeMeta{},
+					ObjectMeta: metav1.ObjectMeta{
+						Annotations: map[string]string{
+							utils.IsDefaultSnapshotClassAnnotation: "true",
+						},
+					},
+					Driver: "test.csi.io",
+				},
+			}},
+		},
+		{
+			name: "update snapshot class to new driver with existing default classes",
+			volumeSnapshotClass: &volumesnapshotv1.VolumeSnapshotClass{
+				TypeMeta: metav1.TypeMeta{},
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{
+						utils.IsDefaultSnapshotClassAnnotation: "true",
+					},
+				},
+				Driver: "driver.test.csi.io",
+			},
+			oldVolumeSnapshotClass: &volumesnapshotv1.VolumeSnapshotClass{
+				TypeMeta: metav1.TypeMeta{},
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{
+						utils.IsDefaultSnapshotClassAnnotation: "true",
+					},
+				},
+				Driver: "test.csi.io",
+			},
+			shouldAdmit: false,
+			msg:         "default snapshot class: driver-test-default already exits for driver: driver.test.csi.io",
+			operation:   v1.Update,
+			lister: &fakeSnapshotLister{values: []*volumesnapshotv1.VolumeSnapshotClass{
+				{
+					TypeMeta: metav1.TypeMeta{},
+					ObjectMeta: metav1.ObjectMeta{
+						Name: "driver-is-default",
+						Annotations: map[string]string{
+							utils.IsDefaultSnapshotClassAnnotation: "true",
+						},
+					},
+					Driver: "test.csi.io",
+				},
+				{
+					TypeMeta: metav1.TypeMeta{},
+					ObjectMeta: metav1.ObjectMeta{
+						Name: "driver-test-default",
+						Annotations: map[string]string{
+							utils.IsDefaultSnapshotClassAnnotation: "true",
+						},
+					},
+					Driver: "driver.test.csi.io",
+				},
+			}},
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			snapshotContent := tc.volumeSnapshotClass
+			raw, err := json.Marshal(snapshotContent)
+			if err != nil {
+				t.Fatal(err)
+			}
+			oldSnapshotClass := tc.oldVolumeSnapshotClass
+			oldRaw, err := json.Marshal(oldSnapshotClass)
+			if err != nil {
+				t.Fatal(err)
+			}
+			review := v1.AdmissionReview{
+				Request: &v1.AdmissionRequest{
+					Object: runtime.RawExtension{
+						Raw: raw,
+					},
+					OldObject: runtime.RawExtension{
+						Raw: oldRaw,
+					},
+					Resource:  SnapshotClassV1GVR,
+					Operation: tc.operation,
+				},
+			}
+			sa := NewSnapshotAdmitter(tc.lister)
+			response := sa.Admit(review)
+
 			shouldAdmit := response.Allowed
 			msg := response.Result.Message
 
diff --git a/pkg/validation-webhook/webhook_test.go b/pkg/validation-webhook/webhook_test.go
index 1c325a5b4..f1e81be5f 100644
--- a/pkg/validation-webhook/webhook_test.go
+++ b/pkg/validation-webhook/webhook_test.go
@@ -45,7 +45,7 @@ func TestWebhookCertReload(t *testing.T) {
 		GetCertificate: cw.GetCertificate,
 	}
 	go func() {
-		if err := startServer(ctx, tlsConfig, cw); err != nil {
+		if err := startServer(ctx, tlsConfig, cw, &fakeSnapshotLister{}); err != nil {
 			panic(err)
 		}
 	}()