[] Adding validation to snapshot class defaulting
This commit is contained in:
@@ -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))
|
||||
}
|
||||
|
@@ -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 "<nil string pointer>"
|
||||
|
@@ -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) {
|
||||
|
||||
}
|
||||
|
@@ -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()
|
||||
}
|
||||
|
@@ -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)
|
||||
}
|
||||
}()
|
||||
|
Reference in New Issue
Block a user