...

Source file src/sigs.k8s.io/controller-runtime/pkg/builder/webhook_test.go

Documentation: sigs.k8s.io/controller-runtime/pkg/builder

     1  /*
     2  Copyright 2019 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package builder
    18  
    19  import (
    20  	"context"
    21  	"errors"
    22  	"fmt"
    23  	"io"
    24  	"net/http"
    25  	"net/http/httptest"
    26  	"os"
    27  	"strings"
    28  
    29  	"github.com/go-logr/logr"
    30  	. "github.com/onsi/ginkgo/v2"
    31  	. "github.com/onsi/gomega"
    32  	"github.com/onsi/gomega/gbytes"
    33  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    34  	"k8s.io/apimachinery/pkg/runtime"
    35  	"k8s.io/apimachinery/pkg/runtime/schema"
    36  
    37  	"sigs.k8s.io/controller-runtime/pkg/controller"
    38  	logf "sigs.k8s.io/controller-runtime/pkg/log"
    39  	"sigs.k8s.io/controller-runtime/pkg/log/zap"
    40  	"sigs.k8s.io/controller-runtime/pkg/manager"
    41  	"sigs.k8s.io/controller-runtime/pkg/scheme"
    42  	"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
    43  )
    44  
    45  const (
    46  	admissionReviewGV = `{
    47    "kind":"AdmissionReview",
    48    "apiVersion":"admission.k8s.io/`
    49  
    50  	svcBaseAddr = "http://svc-name.svc-ns.svc"
    51  )
    52  
    53  var _ = Describe("webhook", func() {
    54  	Describe("New", func() {
    55  		Context("v1 AdmissionReview", func() {
    56  			runTests("v1")
    57  		})
    58  		Context("v1beta1 AdmissionReview", func() {
    59  			runTests("v1beta1")
    60  		})
    61  	})
    62  })
    63  
    64  func runTests(admissionReviewVersion string) {
    65  	var (
    66  		stop          chan struct{}
    67  		logBuffer     *gbytes.Buffer
    68  		testingLogger logr.Logger
    69  	)
    70  
    71  	BeforeEach(func() {
    72  		stop = make(chan struct{})
    73  		newController = controller.New
    74  		logBuffer = gbytes.NewBuffer()
    75  		testingLogger = zap.New(zap.JSONEncoder(), zap.WriteTo(io.MultiWriter(logBuffer, GinkgoWriter)))
    76  	})
    77  
    78  	AfterEach(func() {
    79  		close(stop)
    80  	})
    81  
    82  	It("should scaffold a defaulting webhook if the type implements the Defaulter interface", func() {
    83  		By("creating a controller manager")
    84  		m, err := manager.New(cfg, manager.Options{})
    85  		ExpectWithOffset(1, err).NotTo(HaveOccurred())
    86  
    87  		By("registering the type in the Scheme")
    88  		builder := scheme.Builder{GroupVersion: testDefaulterGVK.GroupVersion()}
    89  		builder.Register(&TestDefaulter{}, &TestDefaulterList{})
    90  		err = builder.AddToScheme(m.GetScheme())
    91  		ExpectWithOffset(1, err).NotTo(HaveOccurred())
    92  
    93  		err = WebhookManagedBy(m).
    94  			For(&TestDefaulter{}).
    95  			Complete()
    96  		ExpectWithOffset(1, err).NotTo(HaveOccurred())
    97  		svr := m.GetWebhookServer()
    98  		ExpectWithOffset(1, svr).NotTo(BeNil())
    99  
   100  		reader := strings.NewReader(admissionReviewGV + admissionReviewVersion + `",
   101    "request":{
   102      "uid":"07e52e8d-4513-11e9-a716-42010a800270",
   103      "kind":{
   104        "group":"",
   105        "version":"v1",
   106        "kind":"TestDefaulter"
   107      },
   108      "resource":{
   109        "group":"",
   110        "version":"v1",
   111        "resource":"testdefaulter"
   112      },
   113      "namespace":"default",
   114      "operation":"CREATE",
   115      "object":{
   116        "replica":1
   117      },
   118      "oldObject":null
   119    }
   120  }`)
   121  
   122  		ctx, cancel := context.WithCancel(context.Background())
   123  		cancel()
   124  		err = svr.Start(ctx)
   125  		if err != nil && !os.IsNotExist(err) {
   126  			ExpectWithOffset(1, err).NotTo(HaveOccurred())
   127  		}
   128  
   129  		By("sending a request to a mutating webhook path")
   130  		path := generateMutatePath(testDefaulterGVK)
   131  		req := httptest.NewRequest("POST", svcBaseAddr+path, reader)
   132  		req.Header.Add("Content-Type", "application/json")
   133  		w := httptest.NewRecorder()
   134  		svr.WebhookMux().ServeHTTP(w, req)
   135  		ExpectWithOffset(1, w.Code).To(Equal(http.StatusOK))
   136  		By("sanity checking the response contains reasonable fields")
   137  		ExpectWithOffset(1, w.Body).To(ContainSubstring(`"allowed":true`))
   138  		ExpectWithOffset(1, w.Body).To(ContainSubstring(`"patch":`))
   139  		ExpectWithOffset(1, w.Body).To(ContainSubstring(`"code":200`))
   140  
   141  		By("sending a request to a validating webhook path that doesn't exist")
   142  		path = generateValidatePath(testDefaulterGVK)
   143  		_, err = reader.Seek(0, 0)
   144  		ExpectWithOffset(1, err).NotTo(HaveOccurred())
   145  		req = httptest.NewRequest("POST", svcBaseAddr+path, reader)
   146  		req.Header.Add("Content-Type", "application/json")
   147  		w = httptest.NewRecorder()
   148  		svr.WebhookMux().ServeHTTP(w, req)
   149  		ExpectWithOffset(1, w.Code).To(Equal(http.StatusNotFound))
   150  	})
   151  
   152  	It("should scaffold a defaulting webhook which recovers from panics", func() {
   153  		By("creating a controller manager")
   154  		m, err := manager.New(cfg, manager.Options{})
   155  		ExpectWithOffset(1, err).NotTo(HaveOccurred())
   156  
   157  		By("registering the type in the Scheme")
   158  		builder := scheme.Builder{GroupVersion: testDefaulterGVK.GroupVersion()}
   159  		builder.Register(&TestDefaulter{}, &TestDefaulterList{})
   160  		err = builder.AddToScheme(m.GetScheme())
   161  		ExpectWithOffset(1, err).NotTo(HaveOccurred())
   162  
   163  		err = WebhookManagedBy(m).
   164  			For(&TestDefaulter{Panic: true}).
   165  			RecoverPanic().
   166  			Complete()
   167  		ExpectWithOffset(1, err).NotTo(HaveOccurred())
   168  		svr := m.GetWebhookServer()
   169  		ExpectWithOffset(1, svr).NotTo(BeNil())
   170  
   171  		reader := strings.NewReader(admissionReviewGV + admissionReviewVersion + `",
   172    "request":{
   173      "uid":"07e52e8d-4513-11e9-a716-42010a800270",
   174      "kind":{
   175        "group":"",
   176        "version":"v1",
   177        "kind":"TestDefaulter"
   178      },
   179      "resource":{
   180        "group":"",
   181        "version":"v1",
   182        "resource":"testdefaulter"
   183      },
   184      "namespace":"default",
   185      "operation":"CREATE",
   186      "object":{
   187        "replica":1,
   188        "panic":true
   189      },
   190      "oldObject":null
   191    }
   192  }`)
   193  
   194  		ctx, cancel := context.WithCancel(context.Background())
   195  		cancel()
   196  		err = svr.Start(ctx)
   197  		if err != nil && !os.IsNotExist(err) {
   198  			ExpectWithOffset(1, err).NotTo(HaveOccurred())
   199  		}
   200  
   201  		By("sending a request to a mutating webhook path")
   202  		path := generateMutatePath(testDefaulterGVK)
   203  		req := httptest.NewRequest("POST", svcBaseAddr+path, reader)
   204  		req.Header.Add("Content-Type", "application/json")
   205  		w := httptest.NewRecorder()
   206  		svr.WebhookMux().ServeHTTP(w, req)
   207  		ExpectWithOffset(1, w.Code).To(Equal(http.StatusOK))
   208  		By("sanity checking the response contains reasonable fields")
   209  		ExpectWithOffset(1, w.Body).To(ContainSubstring(`"allowed":false`))
   210  		ExpectWithOffset(1, w.Body).To(ContainSubstring(`"code":500`))
   211  		ExpectWithOffset(1, w.Body).To(ContainSubstring(`"message":"panic: fake panic test [recovered]`))
   212  	})
   213  
   214  	It("should scaffold a defaulting webhook with a custom defaulter", func() {
   215  		By("creating a controller manager")
   216  		m, err := manager.New(cfg, manager.Options{})
   217  		ExpectWithOffset(1, err).NotTo(HaveOccurred())
   218  
   219  		By("registering the type in the Scheme")
   220  		builder := scheme.Builder{GroupVersion: testDefaulterGVK.GroupVersion()}
   221  		builder.Register(&TestDefaulter{}, &TestDefaulterList{})
   222  		err = builder.AddToScheme(m.GetScheme())
   223  		ExpectWithOffset(1, err).NotTo(HaveOccurred())
   224  
   225  		err = WebhookManagedBy(m).
   226  			WithDefaulter(&TestCustomDefaulter{}).
   227  			For(&TestDefaulter{}).
   228  			WithLogConstructor(func(base logr.Logger, req *admission.Request) logr.Logger {
   229  				return admission.DefaultLogConstructor(testingLogger, req)
   230  			}).
   231  			Complete()
   232  		ExpectWithOffset(1, err).NotTo(HaveOccurred())
   233  		svr := m.GetWebhookServer()
   234  		ExpectWithOffset(1, svr).NotTo(BeNil())
   235  
   236  		reader := strings.NewReader(admissionReviewGV + admissionReviewVersion + `",
   237    "request":{
   238      "uid":"07e52e8d-4513-11e9-a716-42010a800270",
   239      "kind":{
   240        "group":"foo.test.org",
   241        "version":"v1",
   242        "kind":"TestDefaulter"
   243      },
   244      "resource":{
   245        "group":"foo.test.org",
   246        "version":"v1",
   247        "resource":"testdefaulter"
   248      },
   249      "namespace":"default",
   250      "name":"foo",
   251      "operation":"CREATE",
   252      "object":{
   253        "replica":1
   254      },
   255      "oldObject":null
   256    }
   257  }`)
   258  
   259  		ctx, cancel := context.WithCancel(context.Background())
   260  		cancel()
   261  		err = svr.Start(ctx)
   262  		if err != nil && !os.IsNotExist(err) {
   263  			ExpectWithOffset(1, err).NotTo(HaveOccurred())
   264  		}
   265  
   266  		By("sending a request to a mutating webhook path")
   267  		path := generateMutatePath(testDefaulterGVK)
   268  		req := httptest.NewRequest("POST", svcBaseAddr+path, reader)
   269  		req.Header.Add("Content-Type", "application/json")
   270  		w := httptest.NewRecorder()
   271  		svr.WebhookMux().ServeHTTP(w, req)
   272  		ExpectWithOffset(1, w.Code).To(Equal(http.StatusOK))
   273  		By("sanity checking the response contains reasonable fields")
   274  		ExpectWithOffset(1, w.Body).To(ContainSubstring(`"allowed":true`))
   275  		ExpectWithOffset(1, w.Body).To(ContainSubstring(`"patch":`))
   276  		ExpectWithOffset(1, w.Body).To(ContainSubstring(`"code":200`))
   277  		EventuallyWithOffset(1, logBuffer).Should(gbytes.Say(`"msg":"Defaulting object","object":{"name":"foo","namespace":"default"},"namespace":"default","name":"foo","resource":{"group":"foo.test.org","version":"v1","resource":"testdefaulter"},"user":"","requestID":"07e52e8d-4513-11e9-a716-42010a800270"`))
   278  
   279  		By("sending a request to a validating webhook path that doesn't exist")
   280  		path = generateValidatePath(testDefaulterGVK)
   281  		_, err = reader.Seek(0, 0)
   282  		ExpectWithOffset(1, err).NotTo(HaveOccurred())
   283  		req = httptest.NewRequest("POST", svcBaseAddr+path, reader)
   284  		req.Header.Add("Content-Type", "application/json")
   285  		w = httptest.NewRecorder()
   286  		svr.WebhookMux().ServeHTTP(w, req)
   287  		ExpectWithOffset(1, w.Code).To(Equal(http.StatusNotFound))
   288  	})
   289  
   290  	It("should scaffold a validating webhook if the type implements the Validator interface", func() {
   291  		By("creating a controller manager")
   292  		m, err := manager.New(cfg, manager.Options{})
   293  		ExpectWithOffset(1, err).NotTo(HaveOccurred())
   294  
   295  		By("registering the type in the Scheme")
   296  		builder := scheme.Builder{GroupVersion: testValidatorGVK.GroupVersion()}
   297  		builder.Register(&TestValidator{}, &TestValidatorList{})
   298  		err = builder.AddToScheme(m.GetScheme())
   299  		ExpectWithOffset(1, err).NotTo(HaveOccurred())
   300  
   301  		err = WebhookManagedBy(m).
   302  			For(&TestValidator{}).
   303  			Complete()
   304  		ExpectWithOffset(1, err).NotTo(HaveOccurred())
   305  		svr := m.GetWebhookServer()
   306  		ExpectWithOffset(1, svr).NotTo(BeNil())
   307  
   308  		reader := strings.NewReader(admissionReviewGV + admissionReviewVersion + `",
   309    "request":{
   310      "uid":"07e52e8d-4513-11e9-a716-42010a800270",
   311      "kind":{
   312        "group":"",
   313        "version":"v1",
   314        "kind":"TestValidator"
   315      },
   316      "resource":{
   317        "group":"",
   318        "version":"v1",
   319        "resource":"testvalidator"
   320      },
   321      "namespace":"default",
   322      "operation":"UPDATE",
   323      "object":{
   324        "replica":1
   325      },
   326      "oldObject":{
   327        "replica":2
   328      }
   329    }
   330  }`)
   331  
   332  		ctx, cancel := context.WithCancel(context.Background())
   333  		cancel()
   334  		err = svr.Start(ctx)
   335  		if err != nil && !os.IsNotExist(err) {
   336  			ExpectWithOffset(1, err).NotTo(HaveOccurred())
   337  		}
   338  
   339  		By("sending a request to a mutating webhook path that doesn't exist")
   340  		path := generateMutatePath(testValidatorGVK)
   341  		req := httptest.NewRequest("POST", svcBaseAddr+path, reader)
   342  		req.Header.Add("Content-Type", "application/json")
   343  		w := httptest.NewRecorder()
   344  		svr.WebhookMux().ServeHTTP(w, req)
   345  		ExpectWithOffset(1, w.Code).To(Equal(http.StatusNotFound))
   346  
   347  		By("sending a request to a validating webhook path")
   348  		path = generateValidatePath(testValidatorGVK)
   349  		_, err = reader.Seek(0, 0)
   350  		ExpectWithOffset(1, err).NotTo(HaveOccurred())
   351  		req = httptest.NewRequest("POST", svcBaseAddr+path, reader)
   352  		req.Header.Add("Content-Type", "application/json")
   353  		w = httptest.NewRecorder()
   354  		svr.WebhookMux().ServeHTTP(w, req)
   355  		ExpectWithOffset(1, w.Code).To(Equal(http.StatusOK))
   356  		By("sanity checking the response contains reasonable field")
   357  		ExpectWithOffset(1, w.Body).To(ContainSubstring(`"allowed":false`))
   358  		ExpectWithOffset(1, w.Body).To(ContainSubstring(`"code":403`))
   359  	})
   360  
   361  	It("should scaffold a validating webhook which recovers from panics", func() {
   362  		By("creating a controller manager")
   363  		m, err := manager.New(cfg, manager.Options{})
   364  		ExpectWithOffset(1, err).NotTo(HaveOccurred())
   365  
   366  		By("registering the type in the Scheme")
   367  		builder := scheme.Builder{GroupVersion: testValidatorGVK.GroupVersion()}
   368  		builder.Register(&TestValidator{}, &TestValidatorList{})
   369  		err = builder.AddToScheme(m.GetScheme())
   370  		ExpectWithOffset(1, err).NotTo(HaveOccurred())
   371  
   372  		err = WebhookManagedBy(m).
   373  			For(&TestValidator{Panic: true}).
   374  			RecoverPanic().
   375  			Complete()
   376  		ExpectWithOffset(1, err).NotTo(HaveOccurred())
   377  		svr := m.GetWebhookServer()
   378  		ExpectWithOffset(1, svr).NotTo(BeNil())
   379  
   380  		reader := strings.NewReader(admissionReviewGV + admissionReviewVersion + `",
   381    "request":{
   382      "uid":"07e52e8d-4513-11e9-a716-42010a800270",
   383      "kind":{
   384        "group":"",
   385        "version":"v1",
   386        "kind":"TestValidator"
   387      },
   388      "resource":{
   389        "group":"",
   390        "version":"v1",
   391        "resource":"testvalidator"
   392      },
   393      "namespace":"default",
   394      "operation":"CREATE",
   395      "object":{
   396        "replica":2,
   397        "panic":true
   398      }
   399    }
   400  }`)
   401  
   402  		ctx, cancel := context.WithCancel(context.Background())
   403  		cancel()
   404  		err = svr.Start(ctx)
   405  		if err != nil && !os.IsNotExist(err) {
   406  			ExpectWithOffset(1, err).NotTo(HaveOccurred())
   407  		}
   408  
   409  		By("sending a request to a validating webhook path")
   410  		path := generateValidatePath(testValidatorGVK)
   411  		_, err = reader.Seek(0, 0)
   412  		ExpectWithOffset(1, err).NotTo(HaveOccurred())
   413  		req := httptest.NewRequest("POST", svcBaseAddr+path, reader)
   414  		req.Header.Add("Content-Type", "application/json")
   415  		w := httptest.NewRecorder()
   416  		svr.WebhookMux().ServeHTTP(w, req)
   417  		ExpectWithOffset(1, w.Code).To(Equal(http.StatusOK))
   418  		By("sanity checking the response contains reasonable field")
   419  		ExpectWithOffset(1, w.Body).To(ContainSubstring(`"allowed":false`))
   420  		ExpectWithOffset(1, w.Body).To(ContainSubstring(`"code":500`))
   421  		ExpectWithOffset(1, w.Body).To(ContainSubstring(`"message":"panic: fake panic test [recovered]`))
   422  	})
   423  
   424  	It("should scaffold a validating webhook with a custom validator", func() {
   425  		By("creating a controller manager")
   426  		m, err := manager.New(cfg, manager.Options{})
   427  		ExpectWithOffset(1, err).NotTo(HaveOccurred())
   428  
   429  		By("registering the type in the Scheme")
   430  		builder := scheme.Builder{GroupVersion: testValidatorGVK.GroupVersion()}
   431  		builder.Register(&TestValidator{}, &TestValidatorList{})
   432  		err = builder.AddToScheme(m.GetScheme())
   433  		ExpectWithOffset(1, err).NotTo(HaveOccurred())
   434  
   435  		err = WebhookManagedBy(m).
   436  			WithValidator(&TestCustomValidator{}).
   437  			For(&TestValidator{}).
   438  			WithLogConstructor(func(base logr.Logger, req *admission.Request) logr.Logger {
   439  				return admission.DefaultLogConstructor(testingLogger, req)
   440  			}).
   441  			Complete()
   442  		ExpectWithOffset(1, err).NotTo(HaveOccurred())
   443  		svr := m.GetWebhookServer()
   444  		ExpectWithOffset(1, svr).NotTo(BeNil())
   445  
   446  		reader := strings.NewReader(admissionReviewGV + admissionReviewVersion + `",
   447    "request":{
   448      "uid":"07e52e8d-4513-11e9-a716-42010a800270",
   449      "kind":{
   450        "group":"foo.test.org",
   451        "version":"v1",
   452        "kind":"TestValidator"
   453      },
   454      "resource":{
   455        "group":"foo.test.org",
   456        "version":"v1",
   457        "resource":"testvalidator"
   458      },
   459      "namespace":"default",
   460      "name":"foo",
   461      "operation":"UPDATE",
   462      "object":{
   463        "replica":1
   464      },
   465      "oldObject":{
   466        "replica":2
   467      }
   468    }
   469  }`)
   470  
   471  		ctx, cancel := context.WithCancel(context.Background())
   472  		cancel()
   473  		err = svr.Start(ctx)
   474  		if err != nil && !os.IsNotExist(err) {
   475  			ExpectWithOffset(1, err).NotTo(HaveOccurred())
   476  		}
   477  
   478  		By("sending a request to a mutating webhook path that doesn't exist")
   479  		path := generateMutatePath(testValidatorGVK)
   480  		req := httptest.NewRequest("POST", svcBaseAddr+path, reader)
   481  		req.Header.Add("Content-Type", "application/json")
   482  		w := httptest.NewRecorder()
   483  		svr.WebhookMux().ServeHTTP(w, req)
   484  		ExpectWithOffset(1, w.Code).To(Equal(http.StatusNotFound))
   485  
   486  		By("sending a request to a validating webhook path")
   487  		path = generateValidatePath(testValidatorGVK)
   488  		_, err = reader.Seek(0, 0)
   489  		ExpectWithOffset(1, err).NotTo(HaveOccurred())
   490  		req = httptest.NewRequest("POST", svcBaseAddr+path, reader)
   491  		req.Header.Add("Content-Type", "application/json")
   492  		w = httptest.NewRecorder()
   493  		svr.WebhookMux().ServeHTTP(w, req)
   494  		ExpectWithOffset(1, w.Code).To(Equal(http.StatusOK))
   495  		By("sanity checking the response contains reasonable field")
   496  		ExpectWithOffset(1, w.Body).To(ContainSubstring(`"allowed":false`))
   497  		ExpectWithOffset(1, w.Body).To(ContainSubstring(`"code":403`))
   498  		EventuallyWithOffset(1, logBuffer).Should(gbytes.Say(`"msg":"Validating object","object":{"name":"foo","namespace":"default"},"namespace":"default","name":"foo","resource":{"group":"foo.test.org","version":"v1","resource":"testvalidator"},"user":"","requestID":"07e52e8d-4513-11e9-a716-42010a800270"`))
   499  	})
   500  
   501  	It("should scaffold defaulting and validating webhooks if the type implements both Defaulter and Validator interfaces", func() {
   502  		By("creating a controller manager")
   503  		m, err := manager.New(cfg, manager.Options{})
   504  		ExpectWithOffset(1, err).NotTo(HaveOccurred())
   505  
   506  		By("registering the type in the Scheme")
   507  		builder := scheme.Builder{GroupVersion: testDefaultValidatorGVK.GroupVersion()}
   508  		builder.Register(&TestDefaultValidator{}, &TestDefaultValidatorList{})
   509  		err = builder.AddToScheme(m.GetScheme())
   510  		ExpectWithOffset(1, err).NotTo(HaveOccurred())
   511  
   512  		err = WebhookManagedBy(m).
   513  			For(&TestDefaultValidator{}).
   514  			Complete()
   515  		ExpectWithOffset(1, err).NotTo(HaveOccurred())
   516  		svr := m.GetWebhookServer()
   517  		ExpectWithOffset(1, svr).NotTo(BeNil())
   518  
   519  		reader := strings.NewReader(admissionReviewGV + admissionReviewVersion + `",
   520    "request":{
   521      "uid":"07e52e8d-4513-11e9-a716-42010a800270",
   522      "kind":{
   523        "group":"",
   524        "version":"v1",
   525        "kind":"TestDefaultValidator"
   526      },
   527      "resource":{
   528        "group":"",
   529        "version":"v1",
   530        "resource":"testdefaultvalidator"
   531      },
   532      "namespace":"default",
   533      "operation":"CREATE",
   534      "object":{
   535        "replica":1
   536      },
   537      "oldObject":null
   538    }
   539  }`)
   540  
   541  		ctx, cancel := context.WithCancel(context.Background())
   542  		cancel()
   543  		err = svr.Start(ctx)
   544  		if err != nil && !os.IsNotExist(err) {
   545  			ExpectWithOffset(1, err).NotTo(HaveOccurred())
   546  		}
   547  
   548  		By("sending a request to a mutating webhook path")
   549  		path := generateMutatePath(testDefaultValidatorGVK)
   550  		req := httptest.NewRequest("POST", svcBaseAddr+path, reader)
   551  		req.Header.Add("Content-Type", "application/json")
   552  		w := httptest.NewRecorder()
   553  		svr.WebhookMux().ServeHTTP(w, req)
   554  		ExpectWithOffset(1, w.Code).To(Equal(http.StatusOK))
   555  		By("sanity checking the response contains reasonable field")
   556  		ExpectWithOffset(1, w.Body).To(ContainSubstring(`"allowed":true`))
   557  		ExpectWithOffset(1, w.Body).To(ContainSubstring(`"patch":`))
   558  		ExpectWithOffset(1, w.Body).To(ContainSubstring(`"code":200`))
   559  
   560  		By("sending a request to a validating webhook path")
   561  		path = generateValidatePath(testDefaultValidatorGVK)
   562  		_, err = reader.Seek(0, 0)
   563  		ExpectWithOffset(1, err).NotTo(HaveOccurred())
   564  		req = httptest.NewRequest("POST", svcBaseAddr+path, reader)
   565  		req.Header.Add("Content-Type", "application/json")
   566  		w = httptest.NewRecorder()
   567  		svr.WebhookMux().ServeHTTP(w, req)
   568  		ExpectWithOffset(1, w.Code).To(Equal(http.StatusOK))
   569  		By("sanity checking the response contains reasonable field")
   570  		ExpectWithOffset(1, w.Body).To(ContainSubstring(`"allowed":true`))
   571  		ExpectWithOffset(1, w.Body).To(ContainSubstring(`"code":200`))
   572  	})
   573  
   574  	It("should scaffold a validating webhook if the type implements the Validator interface to validate deletes", func() {
   575  		By("creating a controller manager")
   576  		ctx, cancel := context.WithCancel(context.Background())
   577  
   578  		m, err := manager.New(cfg, manager.Options{})
   579  		ExpectWithOffset(1, err).NotTo(HaveOccurred())
   580  
   581  		By("registering the type in the Scheme")
   582  		builder := scheme.Builder{GroupVersion: testValidatorGVK.GroupVersion()}
   583  		builder.Register(&TestValidator{}, &TestValidatorList{})
   584  		err = builder.AddToScheme(m.GetScheme())
   585  		ExpectWithOffset(1, err).NotTo(HaveOccurred())
   586  
   587  		err = WebhookManagedBy(m).
   588  			For(&TestValidator{}).
   589  			Complete()
   590  		ExpectWithOffset(1, err).NotTo(HaveOccurred())
   591  		svr := m.GetWebhookServer()
   592  		ExpectWithOffset(1, svr).NotTo(BeNil())
   593  
   594  		reader := strings.NewReader(admissionReviewGV + admissionReviewVersion + `",
   595    "request":{
   596      "uid":"07e52e8d-4513-11e9-a716-42010a800270",
   597      "kind":{
   598        "group":"",
   599        "version":"v1",
   600        "kind":"TestValidator"
   601      },
   602      "resource":{
   603        "group":"",
   604        "version":"v1",
   605        "resource":"testvalidator"
   606      },
   607      "namespace":"default",
   608      "operation":"DELETE",
   609      "object":null,
   610      "oldObject":{
   611        "replica":1
   612      }
   613    }
   614  }`)
   615  
   616  		cancel()
   617  		err = svr.Start(ctx)
   618  		if err != nil && !os.IsNotExist(err) {
   619  			ExpectWithOffset(1, err).NotTo(HaveOccurred())
   620  		}
   621  
   622  		By("sending a request to a validating webhook path to check for failed delete")
   623  		path := generateValidatePath(testValidatorGVK)
   624  		req := httptest.NewRequest("POST", svcBaseAddr+path, reader)
   625  		req.Header.Add("Content-Type", "application/json")
   626  		w := httptest.NewRecorder()
   627  		svr.WebhookMux().ServeHTTP(w, req)
   628  		ExpectWithOffset(1, w.Code).To(Equal(http.StatusOK))
   629  		By("sanity checking the response contains reasonable field")
   630  		ExpectWithOffset(1, w.Body).To(ContainSubstring(`"allowed":false`))
   631  		ExpectWithOffset(1, w.Body).To(ContainSubstring(`"code":403`))
   632  
   633  		reader = strings.NewReader(admissionReviewGV + admissionReviewVersion + `",
   634    "request":{
   635      "uid":"07e52e8d-4513-11e9-a716-42010a800270",
   636      "kind":{
   637        "group":"",
   638        "version":"v1",
   639        "kind":"TestValidator"
   640      },
   641      "resource":{
   642        "group":"",
   643        "version":"v1",
   644        "resource":"testvalidator"
   645      },
   646      "namespace":"default",
   647      "operation":"DELETE",
   648      "object":null,
   649      "oldObject":{
   650        "replica":0
   651      }
   652    }
   653  }`)
   654  		By("sending a request to a validating webhook path with correct request")
   655  		path = generateValidatePath(testValidatorGVK)
   656  		req = httptest.NewRequest("POST", svcBaseAddr+path, reader)
   657  		req.Header.Add("Content-Type", "application/json")
   658  		w = httptest.NewRecorder()
   659  		svr.WebhookMux().ServeHTTP(w, req)
   660  		ExpectWithOffset(1, w.Code).To(Equal(http.StatusOK))
   661  		By("sanity checking the response contains reasonable field")
   662  		ExpectWithOffset(1, w.Body).To(ContainSubstring(`"allowed":true`))
   663  		ExpectWithOffset(1, w.Body).To(ContainSubstring(`"code":200`))
   664  	})
   665  
   666  	It("should send an error when trying to register a webhook with more than one For", func() {
   667  		By("creating a controller manager")
   668  		m, err := manager.New(cfg, manager.Options{})
   669  		ExpectWithOffset(1, err).NotTo(HaveOccurred())
   670  
   671  		By("registering the type in the Scheme")
   672  		builder := scheme.Builder{GroupVersion: testDefaulterGVK.GroupVersion()}
   673  		builder.Register(&TestDefaulter{}, &TestDefaulterList{})
   674  		err = builder.AddToScheme(m.GetScheme())
   675  		ExpectWithOffset(1, err).NotTo(HaveOccurred())
   676  
   677  		err = WebhookManagedBy(m).
   678  			For(&TestDefaulter{}).
   679  			For(&TestDefaulter{}).
   680  			Complete()
   681  		Expect(err).To(HaveOccurred())
   682  	})
   683  }
   684  
   685  // TestDefaulter.
   686  var _ runtime.Object = &TestDefaulter{}
   687  
   688  const testDefaulterKind = "TestDefaulter"
   689  
   690  type TestDefaulter struct {
   691  	Replica int  `json:"replica,omitempty"`
   692  	Panic   bool `json:"panic,omitempty"`
   693  }
   694  
   695  var testDefaulterGVK = schema.GroupVersionKind{Group: "foo.test.org", Version: "v1", Kind: testDefaulterKind}
   696  
   697  func (d *TestDefaulter) GetObjectKind() schema.ObjectKind { return d }
   698  func (d *TestDefaulter) DeepCopyObject() runtime.Object {
   699  	return &TestDefaulter{
   700  		Replica: d.Replica,
   701  	}
   702  }
   703  
   704  func (d *TestDefaulter) GroupVersionKind() schema.GroupVersionKind {
   705  	return testDefaulterGVK
   706  }
   707  
   708  func (d *TestDefaulter) SetGroupVersionKind(gvk schema.GroupVersionKind) {}
   709  
   710  var _ runtime.Object = &TestDefaulterList{}
   711  
   712  type TestDefaulterList struct{}
   713  
   714  func (*TestDefaulterList) GetObjectKind() schema.ObjectKind { return nil }
   715  func (*TestDefaulterList) DeepCopyObject() runtime.Object   { return nil }
   716  
   717  func (d *TestDefaulter) Default() {
   718  	if d.Panic {
   719  		panic("fake panic test")
   720  	}
   721  	if d.Replica < 2 {
   722  		d.Replica = 2
   723  	}
   724  }
   725  
   726  // TestValidator.
   727  var _ runtime.Object = &TestValidator{}
   728  
   729  const testValidatorKind = "TestValidator"
   730  
   731  type TestValidator struct {
   732  	Replica int  `json:"replica,omitempty"`
   733  	Panic   bool `json:"panic,omitempty"`
   734  }
   735  
   736  var testValidatorGVK = schema.GroupVersionKind{Group: "foo.test.org", Version: "v1", Kind: testValidatorKind}
   737  
   738  func (v *TestValidator) GetObjectKind() schema.ObjectKind { return v }
   739  func (v *TestValidator) DeepCopyObject() runtime.Object {
   740  	return &TestValidator{
   741  		Replica: v.Replica,
   742  	}
   743  }
   744  
   745  func (v *TestValidator) GroupVersionKind() schema.GroupVersionKind {
   746  	return testValidatorGVK
   747  }
   748  
   749  func (v *TestValidator) SetGroupVersionKind(gvk schema.GroupVersionKind) {}
   750  
   751  var _ runtime.Object = &TestValidatorList{}
   752  
   753  type TestValidatorList struct{}
   754  
   755  func (*TestValidatorList) GetObjectKind() schema.ObjectKind { return nil }
   756  func (*TestValidatorList) DeepCopyObject() runtime.Object   { return nil }
   757  
   758  var _ admission.Validator = &TestValidator{}
   759  
   760  func (v *TestValidator) ValidateCreate() (admission.Warnings, error) {
   761  	if v.Panic {
   762  		panic("fake panic test")
   763  	}
   764  	if v.Replica < 0 {
   765  		return nil, errors.New("number of replica should be greater than or equal to 0")
   766  	}
   767  	return nil, nil
   768  }
   769  
   770  func (v *TestValidator) ValidateUpdate(old runtime.Object) (admission.Warnings, error) {
   771  	if v.Panic {
   772  		panic("fake panic test")
   773  	}
   774  	if v.Replica < 0 {
   775  		return nil, errors.New("number of replica should be greater than or equal to 0")
   776  	}
   777  	if oldObj, ok := old.(*TestValidator); !ok {
   778  		return nil, fmt.Errorf("the old object is expected to be %T", oldObj)
   779  	} else if v.Replica < oldObj.Replica {
   780  		return nil, fmt.Errorf("new replica %v should not be fewer than old replica %v", v.Replica, oldObj.Replica)
   781  	}
   782  	return nil, nil
   783  }
   784  
   785  func (v *TestValidator) ValidateDelete() (admission.Warnings, error) {
   786  	if v.Panic {
   787  		panic("fake panic test")
   788  	}
   789  	if v.Replica > 0 {
   790  		return nil, errors.New("number of replica should be less than or equal to 0 to delete")
   791  	}
   792  	return nil, nil
   793  }
   794  
   795  // TestDefaultValidator.
   796  var _ runtime.Object = &TestDefaultValidator{}
   797  
   798  type TestDefaultValidator struct {
   799  	metav1.TypeMeta
   800  	metav1.ObjectMeta
   801  
   802  	Replica int `json:"replica,omitempty"`
   803  }
   804  
   805  var testDefaultValidatorGVK = schema.GroupVersionKind{Group: "foo.test.org", Version: "v1", Kind: "TestDefaultValidator"}
   806  
   807  func (dv *TestDefaultValidator) GetObjectKind() schema.ObjectKind { return dv }
   808  func (dv *TestDefaultValidator) DeepCopyObject() runtime.Object {
   809  	return &TestDefaultValidator{
   810  		Replica: dv.Replica,
   811  	}
   812  }
   813  
   814  func (dv *TestDefaultValidator) GroupVersionKind() schema.GroupVersionKind {
   815  	return testDefaultValidatorGVK
   816  }
   817  
   818  func (dv *TestDefaultValidator) SetGroupVersionKind(gvk schema.GroupVersionKind) {}
   819  
   820  var _ runtime.Object = &TestDefaultValidatorList{}
   821  
   822  type TestDefaultValidatorList struct{}
   823  
   824  func (*TestDefaultValidatorList) GetObjectKind() schema.ObjectKind { return nil }
   825  func (*TestDefaultValidatorList) DeepCopyObject() runtime.Object   { return nil }
   826  
   827  func (dv *TestDefaultValidator) Default() {
   828  	if dv.Replica < 2 {
   829  		dv.Replica = 2
   830  	}
   831  }
   832  
   833  var _ admission.Validator = &TestDefaultValidator{}
   834  
   835  func (dv *TestDefaultValidator) ValidateCreate() (admission.Warnings, error) {
   836  	if dv.Replica < 0 {
   837  		return nil, errors.New("number of replica should be greater than or equal to 0")
   838  	}
   839  	return nil, nil
   840  }
   841  
   842  func (dv *TestDefaultValidator) ValidateUpdate(old runtime.Object) (admission.Warnings, error) {
   843  	if dv.Replica < 0 {
   844  		return nil, errors.New("number of replica should be greater than or equal to 0")
   845  	}
   846  	return nil, nil
   847  }
   848  
   849  func (dv *TestDefaultValidator) ValidateDelete() (admission.Warnings, error) {
   850  	if dv.Replica > 0 {
   851  		return nil, errors.New("number of replica should be less than or equal to 0 to delete")
   852  	}
   853  	return nil, nil
   854  }
   855  
   856  // TestCustomDefaulter.
   857  
   858  type TestCustomDefaulter struct{}
   859  
   860  func (*TestCustomDefaulter) Default(ctx context.Context, obj runtime.Object) error {
   861  	logf.FromContext(ctx).Info("Defaulting object")
   862  	req, err := admission.RequestFromContext(ctx)
   863  	if err != nil {
   864  		return fmt.Errorf("expected admission.Request in ctx: %w", err)
   865  	}
   866  	if req.Kind.Kind != testDefaulterKind {
   867  		return fmt.Errorf("expected Kind TestDefaulter got %q", req.Kind.Kind)
   868  	}
   869  
   870  	d := obj.(*TestDefaulter) //nolint:ifshort
   871  	if d.Replica < 2 {
   872  		d.Replica = 2
   873  	}
   874  	return nil
   875  }
   876  
   877  var _ admission.CustomDefaulter = &TestCustomDefaulter{}
   878  
   879  // TestCustomValidator.
   880  
   881  type TestCustomValidator struct{}
   882  
   883  func (*TestCustomValidator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) {
   884  	logf.FromContext(ctx).Info("Validating object")
   885  	req, err := admission.RequestFromContext(ctx)
   886  	if err != nil {
   887  		return nil, fmt.Errorf("expected admission.Request in ctx: %w", err)
   888  	}
   889  	if req.Kind.Kind != testValidatorKind {
   890  		return nil, fmt.Errorf("expected Kind TestValidator got %q", req.Kind.Kind)
   891  	}
   892  
   893  	v := obj.(*TestValidator) //nolint:ifshort
   894  	if v.Replica < 0 {
   895  		return nil, errors.New("number of replica should be greater than or equal to 0")
   896  	}
   897  	return nil, nil
   898  }
   899  
   900  func (*TestCustomValidator) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) {
   901  	logf.FromContext(ctx).Info("Validating object")
   902  	req, err := admission.RequestFromContext(ctx)
   903  	if err != nil {
   904  		return nil, fmt.Errorf("expected admission.Request in ctx: %w", err)
   905  	}
   906  	if req.Kind.Kind != testValidatorKind {
   907  		return nil, fmt.Errorf("expected Kind TestValidator got %q", req.Kind.Kind)
   908  	}
   909  
   910  	v := newObj.(*TestValidator)
   911  	old := oldObj.(*TestValidator)
   912  	if v.Replica < 0 {
   913  		return nil, errors.New("number of replica should be greater than or equal to 0")
   914  	}
   915  	if v.Replica < old.Replica {
   916  		return nil, fmt.Errorf("new replica %v should not be fewer than old replica %v", v.Replica, old.Replica)
   917  	}
   918  	return nil, nil
   919  }
   920  
   921  func (*TestCustomValidator) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) {
   922  	logf.FromContext(ctx).Info("Validating object")
   923  	req, err := admission.RequestFromContext(ctx)
   924  	if err != nil {
   925  		return nil, fmt.Errorf("expected admission.Request in ctx: %w", err)
   926  	}
   927  	if req.Kind.Kind != testValidatorKind {
   928  		return nil, fmt.Errorf("expected Kind TestValidator got %q", req.Kind.Kind)
   929  	}
   930  
   931  	v := obj.(*TestValidator) //nolint:ifshort
   932  	if v.Replica > 0 {
   933  		return nil, errors.New("number of replica should be less than or equal to 0 to delete")
   934  	}
   935  	return nil, nil
   936  }
   937  
   938  var _ admission.CustomValidator = &TestCustomValidator{}
   939  

View as plain text