diff --git a/pkg/validation-webhook/scheme.go b/pkg/validation-webhook/scheme.go index 6ca79c09..b9819ebb 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/v4/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 ccb1e0a8..3f46ea39 100644 --- a/pkg/validation-webhook/snapshot.go +++ b/pkg/validation-webhook/snapshot.go @@ -20,10 +20,13 @@ import ( "fmt" "reflect" - volumesnapshotv1 "github.com/kubernetes-csi/external-snapshotter/client/v6/apis/volumesnapshot/v1" - volumesnapshotv1beta1 "github.com/kubernetes-csi/external-snapshotter/client/v6/apis/volumesnapshot/v1beta1" + volumesnapshotv1 "github.com/kubernetes-csi/external-snapshotter/client/v4/apis/volumesnapshot/v1" + volumesnapshotv1beta1 "github.com/kubernetes-csi/external-snapshotter/client/v4/apis/volumesnapshot/v1beta1" + storagelisters "github.com/kubernetes-csi/external-snapshotter/client/v4/listers/volumesnapshot/v1" + "github.com/kubernetes-csi/external-snapshotter/v4/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,14 @@ 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"} + // SnapshotContentV1Beta1GVR is GroupVersionResource for v1beta1 VolumeSnapshotContents + SnapshotClassV1Beta1GVR = metav1.GroupVersionResource{Group: volumesnapshotv1beta1.GroupName, Version: "v1beta1", Resource: "volumesnapshotclasses"} + // SnapshotContentV1GVR is GroupVersionResource for v1 VolumeSnapshotContents + SnapshotClassV1GVR = metav1.GroupVersionResource{Group: volumesnapshotv1.GroupName, Version: "v1", Resource: "volumesnapshotclasses"} ) // Add a label {"added-label": "yes"} to the object -func admitSnapshot(ar v1.AdmissionReview) *v1.AdmissionResponse { +func admitSnapshot(ar v1.AdmissionReview, lister storagelisters.VolumeSnapshotClassLister) *v1.AdmissionResponse { klog.V(2).Info("admitting volumesnapshots or volumesnapshotcontents") reviewResponse := &v1.AdmissionResponse{ @@ -106,6 +113,18 @@ func admitSnapshot(ar v1.AdmissionReview) *v1.AdmissionResponse { return toV1AdmissionResponse(err) } return decideSnapshotContentV1(snapcontent, oldSnapcontent, isUpdate) + case SnapshotClassV1Beta1GVR: + snapClass := &volumesnapshotv1beta1.VolumeSnapshotClass{} + if _, _, err := deserializer.Decode(raw, nil, snapClass); err != nil { + klog.Error(err) + return toV1AdmissionResponse(err) + } + oldSnapClass := &volumesnapshotv1beta1.VolumeSnapshotClass{} + if _, _, err := deserializer.Decode(oldRaw, nil, oldSnapClass); err != nil { + klog.Error(err) + return toV1AdmissionResponse(err) + } + return decideSnapshotClassV1beta1(snapClass, oldSnapClass, lister) default: err := fmt.Errorf("expect resource to be %s or %s", SnapshotV1Beta1GVR, SnapshotContentV1Beta1GVR) klog.Error(err) @@ -223,6 +242,50 @@ func decideSnapshotContentV1(snapcontent, oldSnapcontent *volumesnapshotv1.Volum return reviewResponse } +func decideSnapshotClassV1beta1(snapClass, oldSnapClass *volumesnapshotv1beta1.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 oldSnapClass.Annotations[utils.IsDefaultSnapshotClassAnnotation] == "true" { + return reviewResponse + } + + ret, err := lister.List(labels.Everything()) + if err != nil { + reviewResponse.Allowed = false + reviewResponse.Result.Message = err.Error() + return reviewResponse + } + + driverToSnapshotDefautlMap := map[string]*volumesnapshotv1.VolumeSnapshotClass{} + for _, snapshotClass := range ret { + if snapshotClass.Annotations[utils.IsDefaultSnapshotClassAnnotation] != "true" { + continue + } + if _, ok := driverToSnapshotDefautlMap[snapshotClass.Driver]; ok { + // Log an error or something here because this means that old behavior is present. + // We probably should just preserve behavior in this case. + return reviewResponse + } + driverToSnapshotDefautlMap[snapshotClass.Driver] = snapshotClass + } + + if _, ok := driverToSnapshotDefautlMap[snapClass.Driver]; !ok { + reviewResponse.Allowed = false + reviewResponse.Result.Message = fmt.Sprintf("default snapshot class already exits for driver: %v", snapClass.Driver) + return reviewResponse + } + return reviewResponse +} + func strPtrDereference(s *string) string { if s == nil { return "" diff --git a/pkg/validation-webhook/snapshot_test.go b/pkg/validation-webhook/snapshot_test.go index 078d3d8e..d83dc61d 100644 --- a/pkg/validation-webhook/snapshot_test.go +++ b/pkg/validation-webhook/snapshot_test.go @@ -208,7 +208,7 @@ func TestAdmitVolumeSnapshotV1beta1(t *testing.T) { Operation: tc.operation, }, } - response := admitSnapshot(review) + response := admitSnapshot(review, nil) shouldAdmit := response.Allowed msg := response.Result.Message @@ -391,7 +391,7 @@ func TestAdmitVolumeSnapshotV1(t *testing.T) { Operation: tc.operation, }, } - response := admitSnapshot(review) + response := admitSnapshot(review, nil) shouldAdmit := response.Allowed msg := response.Result.Message @@ -541,7 +541,7 @@ func TestAdmitVolumeSnapshotContentV1beta1(t *testing.T) { Operation: tc.operation, }, } - response := admitSnapshot(review) + response := admitSnapshot(review, nil) shouldAdmit := response.Allowed msg := response.Result.Message @@ -685,7 +685,7 @@ func TestAdmitVolumeSnapshotContentV1(t *testing.T) { Operation: tc.operation, }, } - response := admitSnapshot(review) + response := admitSnapshot(review, nil) shouldAdmit := response.Allowed msg := response.Result.Message @@ -701,3 +701,7 @@ func TestAdmitVolumeSnapshotContentV1(t *testing.T) { }) } } + +func TestAdmitVolumeSnapshotClassV1beta1(t *testing.T) { + +} diff --git a/pkg/validation-webhook/webhook.go b/pkg/validation-webhook/webhook.go index 6a568f8a..d5d718db 100644 --- a/pkg/validation-webhook/webhook.go +++ b/pkg/validation-webhook/webhook.go @@ -23,19 +23,27 @@ import ( "fmt" "io/ioutil" "net/http" + "os" + "time" + clientset "github.com/kubernetes-csi/external-snapshotter/client/v4/clientset/versioned" + storagelisters "github.com/kubernetes-csi/external-snapshotter/client/v4/listers/volumesnapshot/v1" "github.com/spf13/cobra" + informers "github.com/kubernetes-csi/external-snapshotter/client/v4/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,13 +66,15 @@ 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 -type admitv1beta1Func func(v1beta1.AdmissionReview) *v1beta1.AdmissionResponse +type admitv1beta1Func func(v1beta1.AdmissionReview, storagelisters.VolumeSnapshotClassLister) *v1beta1.AdmissionResponse // admitv1beta1Func handles a v1 admission -type admitv1Func func(v1.AdmissionReview) *v1.AdmissionResponse +type admitv1Func func(v1.AdmissionReview, storagelisters.VolumeSnapshotClassLister) *v1.AdmissionResponse // admitHandler is a handler, for both validators and mutators, that supports multiple admission review versions type admitHandler struct { @@ -80,16 +90,16 @@ func newDelegateToV1AdmitHandler(f admitv1Func) admitHandler { } func delegateV1beta1AdmitToV1(f admitv1Func) admitv1beta1Func { - return func(review v1beta1.AdmissionReview) *v1beta1.AdmissionResponse { + return func(review v1beta1.AdmissionReview, lister storagelisters.VolumeSnapshotClassLister) *v1beta1.AdmissionResponse { in := v1.AdmissionReview{Request: convertAdmissionRequestToV1(review.Request)} - out := f(in) + out := f(in, lister) return convertAdmissionResponseToV1beta1(out) } } // serve handles the http portion of a request prior to handing to an admit // function -func serve(w http.ResponseWriter, r *http.Request, admit admitHandler) { +func serve(w http.ResponseWriter, r *http.Request, admit admitHandler, lister storagelisters.VolumeSnapshotClassLister) { var body []byte if r.Body == nil { msg := "Expected request body to be non-empty" @@ -137,7 +147,7 @@ func serve(w http.ResponseWriter, r *http.Request, admit admitHandler) { } responseAdmissionReview := &v1beta1.AdmissionReview{} responseAdmissionReview.SetGroupVersionKind(*gvk) - responseAdmissionReview.Response = admit.v1beta1(*requestedAdmissionReview) + responseAdmissionReview.Response = admit.v1beta1(*requestedAdmissionReview, lister) responseAdmissionReview.Response.UID = requestedAdmissionReview.Request.UID responseObj = responseAdmissionReview case v1.SchemeGroupVersion.WithKind("AdmissionReview"): @@ -150,7 +160,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.v1(*requestedAdmissionReview, lister) responseAdmissionReview.Response.UID = requestedAdmissionReview.Request.UID responseObj = responseAdmissionReview default: @@ -173,21 +183,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(admitSnapshot), 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 +233,33 @@ 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) + } + + r := 0 * time.Second + resyncPeriod := &r + + factory := informers.NewSharedInformerFactory(snapClient, *resyncPeriod) + lister := factory.Snapshot().V1().VolumeSnapshotClasses().Lister() + + 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() +} diff --git a/pkg/validation-webhook/webhook_test.go b/pkg/validation-webhook/webhook_test.go index 1c325a5b..9aee1210 100644 --- a/pkg/validation-webhook/webhook_test.go +++ b/pkg/validation-webhook/webhook_test.go @@ -14,8 +14,22 @@ import ( "os" "testing" "time" + + v1 "github.com/kubernetes-csi/external-snapshotter/client/v4/apis/volumesnapshot/v1" + "k8s.io/apimachinery/pkg/labels" ) +type fakeSnapshotLister struct { +} + +func (f *fakeSnapshotLister) List(selector labels.Selector) (ret []*v1.VolumeSnapshotClass, err error) { + return nil, nil +} + +func (f *fakeSnapshotLister) Get(name string) (*v1.VolumeSnapshotClass, error) { + return nil, nil +} + func TestWebhookCertReload(t *testing.T) { // Initialize test space tmpDir := os.TempDir() + "/webhook-cert-tests" @@ -32,6 +46,9 @@ func TestWebhookCertReload(t *testing.T) { t.Errorf("unexpected error occurred while deleting certs: %v", err) } }() + + //Create a fake lister + generateTestCertKeyPair(t, certFile, keyFile) // Start test server @@ -45,7 +62,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) } }()