diff --git a/go.mod b/go.mod index c072cc837fd..94b69156dd2 100644 --- a/go.mod +++ b/go.mod @@ -38,30 +38,30 @@ require ( ) replace ( - k8s.io/api => github.com/kcp-dev/kubernetes/staging/src/k8s.io/api v0.0.0-20220412170948-9b23827cdd1b - k8s.io/apiextensions-apiserver => github.com/kcp-dev/kubernetes/staging/src/k8s.io/apiextensions-apiserver v0.0.0-20220412170948-9b23827cdd1b - k8s.io/apimachinery => github.com/kcp-dev/kubernetes/staging/src/k8s.io/apimachinery v0.0.0-20220412170948-9b23827cdd1b - k8s.io/apiserver => github.com/kcp-dev/kubernetes/staging/src/k8s.io/apiserver v0.0.0-20220412170948-9b23827cdd1b - k8s.io/cli-runtime => github.com/kcp-dev/kubernetes/staging/src/k8s.io/cli-runtime v0.0.0-20220412170948-9b23827cdd1b - k8s.io/client-go => github.com/kcp-dev/kubernetes/staging/src/k8s.io/client-go v0.0.0-20220412170948-9b23827cdd1b - k8s.io/cloud-provider => github.com/kcp-dev/kubernetes/staging/src/k8s.io/cloud-provider v0.0.0-20220412170948-9b23827cdd1b - k8s.io/cluster-bootstrap => github.com/kcp-dev/kubernetes/staging/src/k8s.io/cluster-bootstrap v0.0.0-20220412170948-9b23827cdd1b - k8s.io/code-generator => github.com/kcp-dev/kubernetes/staging/src/k8s.io/code-generator v0.0.0-20220412170948-9b23827cdd1b - k8s.io/component-base => github.com/kcp-dev/kubernetes/staging/src/k8s.io/component-base v0.0.0-20220412170948-9b23827cdd1b - k8s.io/component-helpers => github.com/kcp-dev/kubernetes/staging/src/k8s.io/component-helpers v0.0.0-20220412170948-9b23827cdd1b - k8s.io/controller-manager => github.com/kcp-dev/kubernetes/staging/src/k8s.io/controller-manager v0.0.0-20220412170948-9b23827cdd1b - k8s.io/cri-api => github.com/kcp-dev/kubernetes/staging/src/k8s.io/cri-api v0.0.0-20220412170948-9b23827cdd1b - k8s.io/csi-translation-lib => github.com/kcp-dev/kubernetes/staging/src/k8s.io/csi-translation-lib v0.0.0-20220412170948-9b23827cdd1b - k8s.io/kube-aggregator => github.com/kcp-dev/kubernetes/staging/src/k8s.io/kube-aggregator v0.0.0-20220412170948-9b23827cdd1b - k8s.io/kube-controller-manager => github.com/kcp-dev/kubernetes/staging/src/k8s.io/kube-controller-manager v0.0.0-20220412170948-9b23827cdd1b - k8s.io/kube-proxy => github.com/kcp-dev/kubernetes/staging/src/k8s.io/kube-proxy v0.0.0-20220412170948-9b23827cdd1b - k8s.io/kube-scheduler => github.com/kcp-dev/kubernetes/staging/src/k8s.io/kube-scheduler v0.0.0-20220412170948-9b23827cdd1b - k8s.io/kubectl => github.com/kcp-dev/kubernetes/staging/src/k8s.io/kubectl v0.0.0-20220412170948-9b23827cdd1b - k8s.io/kubelet => github.com/kcp-dev/kubernetes/staging/src/k8s.io/kubelet v0.0.0-20220412170948-9b23827cdd1b - k8s.io/kubernetes => github.com/kcp-dev/kubernetes v0.0.0-20220412170948-9b23827cdd1b - k8s.io/legacy-cloud-providers => github.com/kcp-dev/kubernetes/staging/src/k8s.io/legacy-cloud-providers v0.0.0-20220412170948-9b23827cdd1b - k8s.io/metrics => github.com/kcp-dev/kubernetes/staging/src/k8s.io/metrics v0.0.0-20220412170948-9b23827cdd1b - k8s.io/mount-utils => github.com/kcp-dev/kubernetes/staging/src/k8s.io/mount-utils v0.0.0-20220412170948-9b23827cdd1b - k8s.io/pod-security-admission => github.com/kcp-dev/kubernetes/staging/src/k8s.io/pod-security-admission v0.0.0-20220412170948-9b23827cdd1b - k8s.io/sample-apiserver => github.com/kcp-dev/kubernetes/staging/src/k8s.io/sample-apiserver v0.0.0-20220412170948-9b23827cdd1b + k8s.io/api => github.com/sttts/kubernetes/staging/src/k8s.io/api v0.0.0-20220427164015-329d68c4e92d + k8s.io/apiextensions-apiserver => github.com/sttts/kubernetes/staging/src/k8s.io/apiextensions-apiserver v0.0.0-20220427164015-329d68c4e92d + k8s.io/apimachinery => github.com/sttts/kubernetes/staging/src/k8s.io/apimachinery v0.0.0-20220427164015-329d68c4e92d + k8s.io/apiserver => github.com/sttts/kubernetes/staging/src/k8s.io/apiserver v0.0.0-20220427164015-329d68c4e92d + k8s.io/cli-runtime => github.com/sttts/kubernetes/staging/src/k8s.io/cli-runtime v0.0.0-20220427164015-329d68c4e92d + k8s.io/client-go => github.com/sttts/kubernetes/staging/src/k8s.io/client-go v0.0.0-20220427164015-329d68c4e92d + k8s.io/cloud-provider => github.com/sttts/kubernetes/staging/src/k8s.io/cloud-provider v0.0.0-20220427164015-329d68c4e92d + k8s.io/cluster-bootstrap => github.com/sttts/kubernetes/staging/src/k8s.io/cluster-bootstrap v0.0.0-20220427164015-329d68c4e92d + k8s.io/code-generator => github.com/sttts/kubernetes/staging/src/k8s.io/code-generator v0.0.0-20220427164015-329d68c4e92d + k8s.io/component-base => github.com/sttts/kubernetes/staging/src/k8s.io/component-base v0.0.0-20220427164015-329d68c4e92d + k8s.io/component-helpers => github.com/sttts/kubernetes/staging/src/k8s.io/component-helpers v0.0.0-20220427164015-329d68c4e92d + k8s.io/controller-manager => github.com/sttts/kubernetes/staging/src/k8s.io/controller-manager v0.0.0-20220427164015-329d68c4e92d + k8s.io/cri-api => github.com/sttts/kubernetes/staging/src/k8s.io/cri-api v0.0.0-20220427164015-329d68c4e92d + k8s.io/csi-translation-lib => github.com/sttts/kubernetes/staging/src/k8s.io/csi-translation-lib v0.0.0-20220427164015-329d68c4e92d + k8s.io/kube-aggregator => github.com/sttts/kubernetes/staging/src/k8s.io/kube-aggregator v0.0.0-20220427164015-329d68c4e92d + k8s.io/kube-controller-manager => github.com/sttts/kubernetes/staging/src/k8s.io/kube-controller-manager v0.0.0-20220427164015-329d68c4e92d + k8s.io/kube-proxy => github.com/sttts/kubernetes/staging/src/k8s.io/kube-proxy v0.0.0-20220427164015-329d68c4e92d + k8s.io/kube-scheduler => github.com/sttts/kubernetes/staging/src/k8s.io/kube-scheduler v0.0.0-20220427164015-329d68c4e92d + k8s.io/kubectl => github.com/sttts/kubernetes/staging/src/k8s.io/kubectl v0.0.0-20220427164015-329d68c4e92d + k8s.io/kubelet => github.com/sttts/kubernetes/staging/src/k8s.io/kubelet v0.0.0-20220427164015-329d68c4e92d + k8s.io/kubernetes => github.com/sttts/kubernetes v0.0.0-20220427164015-329d68c4e92d + k8s.io/legacy-cloud-providers => github.com/sttts/kubernetes/staging/src/k8s.io/legacy-cloud-providers v0.0.0-20220427164015-329d68c4e92d + k8s.io/metrics => github.com/sttts/kubernetes/staging/src/k8s.io/metrics v0.0.0-20220427164015-329d68c4e92d + k8s.io/mount-utils => github.com/sttts/kubernetes/staging/src/k8s.io/mount-utils v0.0.0-20220427164015-329d68c4e92d + k8s.io/pod-security-admission => github.com/sttts/kubernetes/staging/src/k8s.io/pod-security-admission v0.0.0-20220427164015-329d68c4e92d + k8s.io/sample-apiserver => github.com/sttts/kubernetes/staging/src/k8s.io/sample-apiserver v0.0.0-20220427164015-329d68c4e92d ) diff --git a/go.sum b/go.sum index 82be620acba..763cafa61ea 100644 --- a/go.sum +++ b/go.sum @@ -419,50 +419,6 @@ github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E github.com/karrick/godirwalk v1.16.1/go.mod h1:j4mkqPuvaLI8mp1DroR3P6ad7cyYd4c1qeJ3RV7ULlk= github.com/kcp-dev/apimachinery v0.0.0-20220401165523-9ea2c4e584ad h1:9GW0+0E/gJIsEynR2Ej333DRQ/HOG7R79hdmtSMjono= github.com/kcp-dev/apimachinery v0.0.0-20220401165523-9ea2c4e584ad/go.mod h1:kwfwvtcO3VGCK5hyzlPQCi0s2fYGlQAmKgkx5GKS5Uw= -github.com/kcp-dev/kubernetes v0.0.0-20220412170948-9b23827cdd1b h1:5P5/GOSi4y4ny/ZEtgF4v9QHxJxp8CZnNx7EEYc9YTU= -github.com/kcp-dev/kubernetes v0.0.0-20220412170948-9b23827cdd1b/go.mod h1:JrHA7M1VZSUmOvc0fK/0NskIpVbqlZAOXq7QlRnaT98= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/api v0.0.0-20220412170948-9b23827cdd1b h1:PAN9qSVvYU9gZxKi0ih5WbpJAnPfHbRpJ1QI7c9BWqo= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/api v0.0.0-20220412170948-9b23827cdd1b/go.mod h1:YaTOAfRXJ+yMgQtYx2JxXdxGGoAonb1tg1rTLU0IT54= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/apiextensions-apiserver v0.0.0-20220412170948-9b23827cdd1b h1:MnPLPHyrPDQ4hKu1Gx3IYO0S8FbBPSMfDEuo280zGFc= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/apiextensions-apiserver v0.0.0-20220412170948-9b23827cdd1b/go.mod h1:dDu+cZQ+9z3yQrFM5PeFOE3AOwPgTByRAkuO09owvCs= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/apimachinery v0.0.0-20220412170948-9b23827cdd1b h1:jzOJ33Squd3v6GXFfbDJ4Ew7V2hLWu+n+iRvkt8ouxA= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/apimachinery v0.0.0-20220412170948-9b23827cdd1b/go.mod h1:O3MicknNAEIDH6QnfbYhov76VLccl2SLhbR1odSgttE= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/apiserver v0.0.0-20220412170948-9b23827cdd1b h1:onG+WjstdikIcJiwBsW+31Lu+elZAf62Qm5SwCllbAI= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/apiserver v0.0.0-20220412170948-9b23827cdd1b/go.mod h1:8IjbfoMgd65FoVd8VpCf1ozjLLKLv89eGcdVGJ562QU= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/cli-runtime v0.0.0-20220412170948-9b23827cdd1b h1:iaGz/xAuBCLDoK347Yif3EOg6d0P1CjsFcchpATLpRM= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/cli-runtime v0.0.0-20220412170948-9b23827cdd1b/go.mod h1:orQ0u6YFGILUtVi0Udh8ooQXvlLQnm0+tKbmt7ynloQ= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/client-go v0.0.0-20220412170948-9b23827cdd1b h1:+cCyikOkw2YcwhFi2ED+9HDk25DQPyhx3Wb62akQB9I= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/client-go v0.0.0-20220412170948-9b23827cdd1b/go.mod h1:pyR2F8FLApmfHTXoAynDJ5NesIjkpt5X0L1Sl2bbEHQ= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/cloud-provider v0.0.0-20220412170948-9b23827cdd1b h1:W1xjgiXgWotCb/DJTpOdQdX49JZvL8bGhx2l+9PimPM= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/cloud-provider v0.0.0-20220412170948-9b23827cdd1b/go.mod h1:eot0EK8avXDFURVMYfiQSyACC0ZLJQnsmS6pUhK2XuY= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/cluster-bootstrap v0.0.0-20220412170948-9b23827cdd1b h1:XhEWlx9tUP2j9A7r8FW2O3zrZ4PZ0QP5zOJ4PIkyCAk= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/cluster-bootstrap v0.0.0-20220412170948-9b23827cdd1b/go.mod h1:J3nPGbI3hjNEG/PzGqn8u/6u1xZ01RcakjgKLSRJSSU= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/code-generator v0.0.0-20220412170948-9b23827cdd1b h1:/0TrC8OXAJ3uMzRUcOoPJz4ovgaN14wmW0jyGKY470g= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/code-generator v0.0.0-20220412170948-9b23827cdd1b/go.mod h1:9DrMgqpSLqBZxi7nUouy/RtSauB1VpaHBaLjJ7G56VU= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/component-base v0.0.0-20220412170948-9b23827cdd1b h1:Cm+M6E1HVt50R7MQJwl/44RJHXekUJ3R+hZYzp6ph0g= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/component-base v0.0.0-20220412170948-9b23827cdd1b/go.mod h1:AAzffBuvEhUyfWAdxIV3uNmwRiEoRkJcpHdsxupLoqk= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/component-helpers v0.0.0-20220412170948-9b23827cdd1b h1:8PzC2NSB+uI7XNvkNs91o1N+5yVPDS+Ae6uUGYO6LAc= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/component-helpers v0.0.0-20220412170948-9b23827cdd1b/go.mod h1:4n0wN/LrqxbT8q8KYboJKOHDezDQmqBP8JLi7PyAc6k= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/controller-manager v0.0.0-20220412170948-9b23827cdd1b h1:+gAL1viVvAC4M3Gcq1xasKWoHHJdgyKB3CwN9iCSiQ8= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/controller-manager v0.0.0-20220412170948-9b23827cdd1b/go.mod h1:qD4sCXo/+wo05162TFseC/ieRmKSuI87xfd8Jh3KRgo= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/cri-api v0.0.0-20220412170948-9b23827cdd1b/go.mod h1:VEW+/Dcsea218hAxo+ql7vu4KofSiUN+tKpAI40ash8= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/csi-translation-lib v0.0.0-20220412170948-9b23827cdd1b/go.mod h1:JmoRzun30iLuAdeJvRVYC88aE1JxLGG7zr6o8KOHf+k= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/kube-aggregator v0.0.0-20220412170948-9b23827cdd1b h1:Eu1/AOM8tsgzQclZWlgAJeVlnjgou2TzC1BmH1wTgU0= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/kube-aggregator v0.0.0-20220412170948-9b23827cdd1b/go.mod h1:noJonxdKjALujzfCa+BSuune4i+K6AE7yuKRKPRJ4Hk= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/kube-controller-manager v0.0.0-20220412170948-9b23827cdd1b h1:HRKh0M+I7XVPpak4Zgs297h/DVf1cEdl6YiLLME0YQg= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/kube-controller-manager v0.0.0-20220412170948-9b23827cdd1b/go.mod h1:X+jCm9taSAFLDStCk9jdg5oUyghcYIBL1kTKZ3L8r2U= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/kube-proxy v0.0.0-20220412170948-9b23827cdd1b/go.mod h1:BAErEeTBUJ+9mqW2EZa3UC4rzY8575Jscooq57CVrrA= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/kube-scheduler v0.0.0-20220412170948-9b23827cdd1b/go.mod h1:XWqMgcamrn6LFtdb91dstGlCVFyHqZEK4AkXTU+4/Dw= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/kubectl v0.0.0-20220412170948-9b23827cdd1b/go.mod h1:4y0AXnfqR55ujLnJ9wp5HrizaSxAgLL8O4+QbZ7BWKM= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/kubelet v0.0.0-20220412170948-9b23827cdd1b h1:XlzowqYdE2bccqe1L3YHV5qW2dtag9E4BMeMFDLa47k= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/kubelet v0.0.0-20220412170948-9b23827cdd1b/go.mod h1:4C4GRv5M3KggS3kOszIZ4xpK/lpzfcmKKK9EaSu2r/I= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/legacy-cloud-providers v0.0.0-20220412170948-9b23827cdd1b/go.mod h1:/EdP2IinGcoX2N4R3NLnerTVQdUdIM5oZdWfZj9t0pk= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/metrics v0.0.0-20220412170948-9b23827cdd1b/go.mod h1:QJDvxTOe23WUSu62yzSod+AAtkULwVA8kqCX/qMFgj0= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/mount-utils v0.0.0-20220412170948-9b23827cdd1b h1:aUopo4oU4ZTqkx3mF1PmNDYeUuQgU3hZ2N6RA7A9ov0= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/mount-utils v0.0.0-20220412170948-9b23827cdd1b/go.mod h1:4TleGLIXJAUXyxYMKm84WqIE0TgGbRSJXVFD5ESP44Q= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/pod-security-admission v0.0.0-20220412170948-9b23827cdd1b h1:R02hrVSFjUnlWqqVxY86i8/ekLkuGxwG5pNUYIsLMco= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/pod-security-admission v0.0.0-20220412170948-9b23827cdd1b/go.mod h1:YSfevLOikB6fttjc9y/dmcQj0OJEZ/isjLZeDyOVYTs= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/sample-apiserver v0.0.0-20220412170948-9b23827cdd1b/go.mod h1:V0s75URYbm3vxoPEpjk1nO6c9WEoEIr5pGxS792IBfA= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= @@ -666,6 +622,50 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/sttts/kubernetes v0.0.0-20220427164015-329d68c4e92d h1:/MnJHseu11YYiBxaxkFHOleUmcnBvAVBznOIMFSAS2A= +github.com/sttts/kubernetes v0.0.0-20220427164015-329d68c4e92d/go.mod h1:JrHA7M1VZSUmOvc0fK/0NskIpVbqlZAOXq7QlRnaT98= +github.com/sttts/kubernetes/staging/src/k8s.io/api v0.0.0-20220427164015-329d68c4e92d h1:oYPKLnmuw+FzCjTyJmhGG1CcHB1z06KRb8cVETZIOn4= +github.com/sttts/kubernetes/staging/src/k8s.io/api v0.0.0-20220427164015-329d68c4e92d/go.mod h1:YaTOAfRXJ+yMgQtYx2JxXdxGGoAonb1tg1rTLU0IT54= +github.com/sttts/kubernetes/staging/src/k8s.io/apiextensions-apiserver v0.0.0-20220427164015-329d68c4e92d h1:FKJxOkcaI0OHk51Wk3373mISLEiks7ZfmlGe6rAgAq0= +github.com/sttts/kubernetes/staging/src/k8s.io/apiextensions-apiserver v0.0.0-20220427164015-329d68c4e92d/go.mod h1:dDu+cZQ+9z3yQrFM5PeFOE3AOwPgTByRAkuO09owvCs= +github.com/sttts/kubernetes/staging/src/k8s.io/apimachinery v0.0.0-20220427164015-329d68c4e92d h1:kBe3FOrOF2pqjkFSxMxyl9qBJH1uZ4tkP5AviqDv1ow= +github.com/sttts/kubernetes/staging/src/k8s.io/apimachinery v0.0.0-20220427164015-329d68c4e92d/go.mod h1:O3MicknNAEIDH6QnfbYhov76VLccl2SLhbR1odSgttE= +github.com/sttts/kubernetes/staging/src/k8s.io/apiserver v0.0.0-20220427164015-329d68c4e92d h1:LlAcvD5IP0uI9I15YOAZK11+6cDtnU8zgaPCLHilRxo= +github.com/sttts/kubernetes/staging/src/k8s.io/apiserver v0.0.0-20220427164015-329d68c4e92d/go.mod h1:8IjbfoMgd65FoVd8VpCf1ozjLLKLv89eGcdVGJ562QU= +github.com/sttts/kubernetes/staging/src/k8s.io/cli-runtime v0.0.0-20220427164015-329d68c4e92d h1:/nytsSkw7jjUF0ar2vOwYKUORHWegAfyuYq5R2UQHHo= +github.com/sttts/kubernetes/staging/src/k8s.io/cli-runtime v0.0.0-20220427164015-329d68c4e92d/go.mod h1:orQ0u6YFGILUtVi0Udh8ooQXvlLQnm0+tKbmt7ynloQ= +github.com/sttts/kubernetes/staging/src/k8s.io/client-go v0.0.0-20220427164015-329d68c4e92d h1:CQ92zdLSZ+X2EHvl3PrP82GJW5mzRLY2QPmAe1rAvc0= +github.com/sttts/kubernetes/staging/src/k8s.io/client-go v0.0.0-20220427164015-329d68c4e92d/go.mod h1:pyR2F8FLApmfHTXoAynDJ5NesIjkpt5X0L1Sl2bbEHQ= +github.com/sttts/kubernetes/staging/src/k8s.io/cloud-provider v0.0.0-20220427164015-329d68c4e92d h1:9g2TYUOnQmy3OoyRdYmWJrEL9q7BNrZUOmQpH2yOTio= +github.com/sttts/kubernetes/staging/src/k8s.io/cloud-provider v0.0.0-20220427164015-329d68c4e92d/go.mod h1:eot0EK8avXDFURVMYfiQSyACC0ZLJQnsmS6pUhK2XuY= +github.com/sttts/kubernetes/staging/src/k8s.io/cluster-bootstrap v0.0.0-20220427164015-329d68c4e92d h1:0Krl2S7JPdvx2SDjzFuBxs0fUWEB9D7wPiQGpWcbHHQ= +github.com/sttts/kubernetes/staging/src/k8s.io/cluster-bootstrap v0.0.0-20220427164015-329d68c4e92d/go.mod h1:J3nPGbI3hjNEG/PzGqn8u/6u1xZ01RcakjgKLSRJSSU= +github.com/sttts/kubernetes/staging/src/k8s.io/code-generator v0.0.0-20220427164015-329d68c4e92d h1:QGGV6g1nVTglvbbVIcHTdrdkghPbdGhYEeSmfp4/u1c= +github.com/sttts/kubernetes/staging/src/k8s.io/code-generator v0.0.0-20220427164015-329d68c4e92d/go.mod h1:9DrMgqpSLqBZxi7nUouy/RtSauB1VpaHBaLjJ7G56VU= +github.com/sttts/kubernetes/staging/src/k8s.io/component-base v0.0.0-20220427164015-329d68c4e92d h1:b9hm/z94o8xiMK1gO9/63RsVJ4BZuEsGSbP0825dYyI= +github.com/sttts/kubernetes/staging/src/k8s.io/component-base v0.0.0-20220427164015-329d68c4e92d/go.mod h1:AAzffBuvEhUyfWAdxIV3uNmwRiEoRkJcpHdsxupLoqk= +github.com/sttts/kubernetes/staging/src/k8s.io/component-helpers v0.0.0-20220427164015-329d68c4e92d h1:SebEtQPyIG6jqb7PkQLuKw+YU3xYgrbKYEZmIOtAltg= +github.com/sttts/kubernetes/staging/src/k8s.io/component-helpers v0.0.0-20220427164015-329d68c4e92d/go.mod h1:4n0wN/LrqxbT8q8KYboJKOHDezDQmqBP8JLi7PyAc6k= +github.com/sttts/kubernetes/staging/src/k8s.io/controller-manager v0.0.0-20220427164015-329d68c4e92d h1:0y4uHNWX51iRa3oJ/u0VYdiExl/6zIpYKpimn1jyArI= +github.com/sttts/kubernetes/staging/src/k8s.io/controller-manager v0.0.0-20220427164015-329d68c4e92d/go.mod h1:qD4sCXo/+wo05162TFseC/ieRmKSuI87xfd8Jh3KRgo= +github.com/sttts/kubernetes/staging/src/k8s.io/cri-api v0.0.0-20220427164015-329d68c4e92d/go.mod h1:VEW+/Dcsea218hAxo+ql7vu4KofSiUN+tKpAI40ash8= +github.com/sttts/kubernetes/staging/src/k8s.io/csi-translation-lib v0.0.0-20220427164015-329d68c4e92d/go.mod h1:JmoRzun30iLuAdeJvRVYC88aE1JxLGG7zr6o8KOHf+k= +github.com/sttts/kubernetes/staging/src/k8s.io/kube-aggregator v0.0.0-20220427164015-329d68c4e92d h1:1+tZL0QUnFOzkbKjnOvCr5PsCzmJUXneiMRpJUnWZwA= +github.com/sttts/kubernetes/staging/src/k8s.io/kube-aggregator v0.0.0-20220427164015-329d68c4e92d/go.mod h1:noJonxdKjALujzfCa+BSuune4i+K6AE7yuKRKPRJ4Hk= +github.com/sttts/kubernetes/staging/src/k8s.io/kube-controller-manager v0.0.0-20220427164015-329d68c4e92d h1:6wb2DSG4yZJA/2YHZkjENnlCGSp+ND/VK1JFwDP+6JE= +github.com/sttts/kubernetes/staging/src/k8s.io/kube-controller-manager v0.0.0-20220427164015-329d68c4e92d/go.mod h1:X+jCm9taSAFLDStCk9jdg5oUyghcYIBL1kTKZ3L8r2U= +github.com/sttts/kubernetes/staging/src/k8s.io/kube-proxy v0.0.0-20220427164015-329d68c4e92d/go.mod h1:BAErEeTBUJ+9mqW2EZa3UC4rzY8575Jscooq57CVrrA= +github.com/sttts/kubernetes/staging/src/k8s.io/kube-scheduler v0.0.0-20220427164015-329d68c4e92d/go.mod h1:XWqMgcamrn6LFtdb91dstGlCVFyHqZEK4AkXTU+4/Dw= +github.com/sttts/kubernetes/staging/src/k8s.io/kubectl v0.0.0-20220427164015-329d68c4e92d/go.mod h1:4y0AXnfqR55ujLnJ9wp5HrizaSxAgLL8O4+QbZ7BWKM= +github.com/sttts/kubernetes/staging/src/k8s.io/kubelet v0.0.0-20220427164015-329d68c4e92d h1:Ztk2zG1fyMN/qrlu48a15tR527mGKNdPelBfJhH7Uho= +github.com/sttts/kubernetes/staging/src/k8s.io/kubelet v0.0.0-20220427164015-329d68c4e92d/go.mod h1:4C4GRv5M3KggS3kOszIZ4xpK/lpzfcmKKK9EaSu2r/I= +github.com/sttts/kubernetes/staging/src/k8s.io/legacy-cloud-providers v0.0.0-20220427164015-329d68c4e92d/go.mod h1:/EdP2IinGcoX2N4R3NLnerTVQdUdIM5oZdWfZj9t0pk= +github.com/sttts/kubernetes/staging/src/k8s.io/metrics v0.0.0-20220427164015-329d68c4e92d/go.mod h1:QJDvxTOe23WUSu62yzSod+AAtkULwVA8kqCX/qMFgj0= +github.com/sttts/kubernetes/staging/src/k8s.io/mount-utils v0.0.0-20220427164015-329d68c4e92d h1:sIzSWXFl+L3FUo2Qe4vpdAx+fiu/7qIRpoMWRj6iOnI= +github.com/sttts/kubernetes/staging/src/k8s.io/mount-utils v0.0.0-20220427164015-329d68c4e92d/go.mod h1:4TleGLIXJAUXyxYMKm84WqIE0TgGbRSJXVFD5ESP44Q= +github.com/sttts/kubernetes/staging/src/k8s.io/pod-security-admission v0.0.0-20220427164015-329d68c4e92d h1:6YG6F2xGN1M58QvFyPD50xnO3NgpwUCseesC0NvaAg0= +github.com/sttts/kubernetes/staging/src/k8s.io/pod-security-admission v0.0.0-20220427164015-329d68c4e92d/go.mod h1:YSfevLOikB6fttjc9y/dmcQj0OJEZ/isjLZeDyOVYTs= +github.com/sttts/kubernetes/staging/src/k8s.io/sample-apiserver v0.0.0-20220427164015-329d68c4e92d/go.mod h1:V0s75URYbm3vxoPEpjk1nO6c9WEoEIr5pGxS792IBfA= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= diff --git a/pkg/admission/mutatingwebhook/plugin.go b/pkg/admission/mutatingwebhook/plugin.go new file mode 100644 index 00000000000..29688eb3ac8 --- /dev/null +++ b/pkg/admission/mutatingwebhook/plugin.go @@ -0,0 +1,112 @@ +/* +Copyright 2022 The KCP Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package mutatingwebhook + +import ( + "context" + "io" + + admissionv1 "k8s.io/api/admission/v1" + admissionv1beta1 "k8s.io/api/admission/v1beta1" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apiserver/pkg/admission" + "k8s.io/apiserver/pkg/admission/configuration" + "k8s.io/apiserver/pkg/admission/plugin/webhook/config" + "k8s.io/apiserver/pkg/admission/plugin/webhook/generic" + "k8s.io/apiserver/pkg/admission/plugin/webhook/mutating" + webhookutil "k8s.io/apiserver/pkg/util/webhook" + "k8s.io/client-go/informers" + + "github.com/kcp-dev/kcp/pkg/admission/webhook" +) + +const ( + PluginName = "apis.kcp.dev/MutatingWebhook" +) + +type Plugin struct { + // Using validating plugin, for the dispatcher to use. + // This plugins admit function will never be called. + mutating.Plugin + webhook.WebhookDispatcher +} + +var _ admission.MutationInterface = &Plugin{} + +func NewValidatingAdmissionWebhook(configfile io.Reader) (*Plugin, error) { + p := &Plugin{Plugin: mutating.Plugin{Webhook: &generic.Webhook{}}} + p.Handler = admission.NewHandler(admission.Connect, admission.Create, admission.Delete, admission.Update) + + dispatcherFactory := mutating.NewMutatingDispatcher(&p.Plugin) + + // Making our own dispatcher so that we can control the webhook accessors. + kubeconfigFile, err := config.LoadConfig(configfile) + if err != nil { + return nil, err + } + cm, err := webhookutil.NewClientManager( + []schema.GroupVersion{ + admissionv1beta1.SchemeGroupVersion, + admissionv1.SchemeGroupVersion, + }, + admissionv1beta1.AddToScheme, + admissionv1.AddToScheme, + ) + if err != nil { + return nil, err + } + authInfoResolver, err := webhookutil.NewDefaultAuthenticationInfoResolver(kubeconfigFile) + if err != nil { + return nil, err + } + // Set defaults which may be overridden later. + cm.SetAuthenticationInfoResolver(authInfoResolver) + cm.SetServiceResolver(webhookutil.NewDefaultServiceResolver()) + + p.WebhookDispatcher.SetDispatcher(dispatcherFactory(&cm)) + // Need to do this, to make sure that the underlying objects for the call to ShouldCallHook have the right values + p.Plugin.Webhook, err = generic.NewWebhook(p.Handler, configfile, configuration.NewMutatingWebhookConfigurationManager, dispatcherFactory) + if err != nil { + return nil, err + } + + //Override the ready func + + p.SetReadyFunc(func() bool { + if p.WebhookDispatcher.HasSynced() && p.Plugin.WaitForReady() { + return true + } + return false + }) + return p, nil +} + +func Register(plugins *admission.Plugins) { + plugins.Register(PluginName, func(configFile io.Reader) (admission.Interface, error) { + return NewValidatingAdmissionWebhook(configFile) + }) +} + +func (a *Plugin) Admit(ctx context.Context, attr admission.Attributes, o admission.ObjectInterfaces) error { + return a.WebhookDispatcher.Dispatch(ctx, attr, o) +} + +// SetExternalKubeInformerFactory implements the WantsExternalKubeInformerFactory interface. +func (p *Plugin) SetExternalKubeInformerFactory(f informers.SharedInformerFactory) { + p.WebhookDispatcher.SetHookSource(configuration.NewMutatingWebhookConfigurationManager(f)) + p.Plugin.SetExternalKubeInformerFactory(f) +} diff --git a/pkg/admission/plugins.go b/pkg/admission/plugins.go index 1672f8dcf24..03d5a32112a 100644 --- a/pkg/admission/plugins.go +++ b/pkg/admission/plugins.go @@ -45,7 +45,9 @@ import ( "github.com/kcp-dev/kcp/pkg/admission/clusterworkspaceshard" "github.com/kcp-dev/kcp/pkg/admission/clusterworkspacetype" "github.com/kcp-dev/kcp/pkg/admission/clusterworkspacetypeexists" + kcpmutatingwebhook "github.com/kcp-dev/kcp/pkg/admission/mutatingwebhook" workspacenamespacelifecycle "github.com/kcp-dev/kcp/pkg/admission/namespacelifecycle" + kcpvalidatingwebhook "github.com/kcp-dev/kcp/pkg/admission/validatingwebhook" ) // AllOrderedPlugins is the list of all the plugins in order. @@ -57,6 +59,8 @@ var AllOrderedPlugins = beforeWebhooks(kubeapiserveroptions.AllOrderedPlugins, clusterworkspacetype.PluginName, clusterworkspacetypeexists.PluginName, apibinding.PluginName, + kcpvalidatingwebhook.PluginName, + kcpmutatingwebhook.PluginName, ) func beforeWebhooks(recommended []string, plugins ...string) []string { @@ -81,13 +85,13 @@ func RegisterAllKcpAdmissionPlugins(plugins *admission.Plugins) { apiresourceschema.Register(plugins) apibinding.Register(plugins) workspacenamespacelifecycle.Register(plugins) + kcpvalidatingwebhook.Register(plugins) + kcpmutatingwebhook.Register(plugins) } var defaultOnPluginsInKcp = sets.NewString( workspacenamespacelifecycle.PluginName, // WorkspaceNamespaceLifecycle limitranger.PluginName, // LimitRanger - mutatingwebhook.PluginName, // MutatingAdmissionWebhook - validatingwebhook.PluginName, // ValidatingAdmissionWebhook certapproval.PluginName, // CertificateApproval certsigning.PluginName, // CertificateSigning certsubjectrestriction.PluginName, // CertificateSubjectRestriction @@ -99,6 +103,8 @@ var defaultOnPluginsInKcp = sets.NewString( clusterworkspacetypeexists.PluginName, apiresourceschema.PluginName, apibinding.PluginName, + kcpvalidatingwebhook.PluginName, + kcpmutatingwebhook.PluginName, ) // defaultOnKubePluginsInKube is a copy of kubeapiserveroptions.defaultOnKubePlugins. diff --git a/pkg/admission/validatingwebhook/plugin.go b/pkg/admission/validatingwebhook/plugin.go new file mode 100644 index 00000000000..a1e0aac1c52 --- /dev/null +++ b/pkg/admission/validatingwebhook/plugin.go @@ -0,0 +1,110 @@ +/* +Copyright 2022 The KCP Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package validatingwebhook + +import ( + "context" + "io" + + admissionv1 "k8s.io/api/admission/v1" + admissionv1beta1 "k8s.io/api/admission/v1beta1" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apiserver/pkg/admission" + "k8s.io/apiserver/pkg/admission/configuration" + "k8s.io/apiserver/pkg/admission/plugin/webhook/config" + "k8s.io/apiserver/pkg/admission/plugin/webhook/generic" + "k8s.io/apiserver/pkg/admission/plugin/webhook/validating" + webhookutil "k8s.io/apiserver/pkg/util/webhook" + "k8s.io/client-go/informers" + + "github.com/kcp-dev/kcp/pkg/admission/webhook" +) + +const ( + PluginName = "apis.kcp.dev/ValidatingWebhook" +) + +type Plugin struct { + // Using validating plugin, for the dispatcher to use. + // This plugins admit function will never be called. + validating.Plugin + webhook.WebhookDispatcher +} + +func NewValidatingAdmissionWebhook(configfile io.Reader) (*Plugin, error) { + p := &Plugin{Plugin: validating.Plugin{Webhook: &generic.Webhook{}}} + p.WebhookDispatcher.Handler = admission.NewHandler(admission.Connect, admission.Create, admission.Delete, admission.Update) + + dispatcherFactory := validating.NewValidatingDispatcher(&p.Plugin) + + // Making our own dispatcher so that we can control the webhook accessors. + kubeconfigFile, err := config.LoadConfig(configfile) + if err != nil { + return nil, err + } + cm, err := webhookutil.NewClientManager( + []schema.GroupVersion{ + admissionv1beta1.SchemeGroupVersion, + admissionv1.SchemeGroupVersion, + }, + admissionv1beta1.AddToScheme, + admissionv1.AddToScheme, + ) + if err != nil { + return nil, err + } + authInfoResolver, err := webhookutil.NewDefaultAuthenticationInfoResolver(kubeconfigFile) + if err != nil { + return nil, err + } + // Set defaults which may be overridden later. + cm.SetAuthenticationInfoResolver(authInfoResolver) + cm.SetServiceResolver(webhookutil.NewDefaultServiceResolver()) + + p.WebhookDispatcher.SetDispatcher(dispatcherFactory(&cm)) + // Need to do this, to make sure that the underlying objects for the call to ShouldCallHook have the right values + p.Plugin.Webhook, err = generic.NewWebhook(p.Handler, configfile, configuration.NewValidatingWebhookConfigurationManager, dispatcherFactory) + if err != nil { + return nil, err + } + + //Override the ready func + + p.SetReadyFunc(func() bool { + if p.WebhookDispatcher.HasSynced() && p.Plugin.WaitForReady() { + return true + } + return false + }) + return p, nil +} + +func Register(plugins *admission.Plugins) { + plugins.Register(PluginName, func(configFile io.Reader) (admission.Interface, error) { + return NewValidatingAdmissionWebhook(configFile) + }) +} + +func (a *Plugin) Validate(ctx context.Context, attr admission.Attributes, o admission.ObjectInterfaces) error { + return a.WebhookDispatcher.Dispatch(ctx, attr, o) +} + +// SetExternalKubeInformerFactory implements the WantsExternalKubeInformerFactory interface. +func (p *Plugin) SetExternalKubeInformerFactory(f informers.SharedInformerFactory) { + p.WebhookDispatcher.SetHookSource(configuration.NewValidatingWebhookConfigurationManager(f)) + p.Plugin.SetExternalKubeInformerFactory(f) +} diff --git a/pkg/admission/webhook/generic_webhook.go b/pkg/admission/webhook/generic_webhook.go new file mode 100644 index 00000000000..b4f7d7c0fc9 --- /dev/null +++ b/pkg/admission/webhook/generic_webhook.go @@ -0,0 +1,149 @@ +/* +Copyright 2022 The KCP Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package webhook + +import ( + "context" + "fmt" + + "github.com/kcp-dev/apimachinery/pkg/logicalcluster" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apiserver/pkg/admission" + webhookconfiguration "k8s.io/apiserver/pkg/admission/configuration" + "k8s.io/apiserver/pkg/admission/plugin/webhook" + "k8s.io/apiserver/pkg/admission/plugin/webhook/generic" + "k8s.io/apiserver/pkg/admission/plugin/webhook/rules" + genericapirequest "k8s.io/apiserver/pkg/endpoints/request" + "k8s.io/client-go/tools/cache" + "k8s.io/klog/v2" + + "github.com/kcp-dev/kcp/pkg/admission/initializers" + apisv1alpha1 "github.com/kcp-dev/kcp/pkg/apis/apis/v1alpha1" + kcpinformers "github.com/kcp-dev/kcp/pkg/client/informers/externalversions" +) + +const byWorkspaceIndex = "webhookDispatcher-byWorkspace" + +var _ initializers.WantsKcpInformers = &WebhookDispatcher{} + +type WebhookDispatcher struct { + dispatcher generic.Dispatcher + hookSource generic.Source + apiBindingsIndexer cache.Indexer + apiBindingsHasSynced func() bool + *admission.Handler +} + +func (p *WebhookDispatcher) HasSynced() bool { + return p.hookSource.HasSynced() && p.apiBindingsHasSynced() +} + +func (p *WebhookDispatcher) SetDispatcher(dispatch generic.Dispatcher) { + p.dispatcher = dispatch +} + +func (p *WebhookDispatcher) Dispatch(ctx context.Context, attr admission.Attributes, o admission.ObjectInterfaces) error { + // If the object is a Webhook configuration, do not call webhooks + // This is because we need some way to recover if a webhook is preventing a cluster resources from being updated + if rules.IsWebhookConfigurationResource(attr) { + return nil + } + lcluster, err := genericapirequest.ClusterNameFrom(ctx) + if err != nil { + return err + } + if !p.WaitForReady() { + return admission.NewForbidden(attr, fmt.Errorf("not yet ready to handle request")) + } + + hooks := p.hookSource.Webhooks() + var whAccessor []webhook.WebhookAccessor + + // Determine the type of request, is it api binding or not. + if workspace, isAPIBinding, err := p.getAPIBindingWorkspace(attr, lcluster); err != nil { + return err + } else if isAPIBinding { + whAccessor = p.restrictToLogicalCluster(hooks, workspace) + klog.V(3).Infof("restricting call to api registration hooks in cluster: %v", workspace) + } else { + whAccessor = p.restrictToLogicalCluster(hooks, lcluster) + klog.V(3).Infof("restricting call to hooks in cluster: %v", lcluster) + } + + return p.dispatcher.Dispatch(ctx, attr, o, whAccessor) +} + +func (p *WebhookDispatcher) getAPIBindingWorkspace(attr admission.Attributes, clusterName logicalcluster.LogicalCluster) (logicalcluster.LogicalCluster, bool, error) { + parentClusterName, hasParent := clusterName.Parent() + if !hasParent { + // APIBindings in root are not possible (they can only point to sibling workspaces). + return logicalcluster.New(""), false, nil + } + + objs, err := p.apiBindingsIndexer.ByIndex(byWorkspaceIndex, clusterName.String()) + if err != nil { + return logicalcluster.New(""), false, err + } + for _, obj := range objs { + apiBinding := obj.(*apisv1alpha1.APIBinding) + for _, br := range apiBinding.Status.BoundResources { + if apiBinding.Status.BoundAPIExport.Workspace == nil { + // this will never happen today. But as soon as we add other reference types (like exports), this log output will remind out of necessary work here. + klog.Errorf("APIBinding %s has no referenced workspace", clusterName, apiBinding.Name) + continue + } + if br.Group == attr.GetResource().Group && br.Resource == attr.GetResource().Resource { + return parentClusterName.Join(apiBinding.Status.BoundAPIExport.Workspace.WorkspaceName), true, nil + } + } + } + return logicalcluster.New(""), false, nil +} + +// In the future use a restricted list call +func (p *WebhookDispatcher) restrictToLogicalCluster(hooks []webhook.WebhookAccessor, lc logicalcluster.LogicalCluster) []webhook.WebhookAccessor { + // TODO(sttts): this might not scale if there are many webhooks. This is called per request, and traverses all + // webhook registrations. The hope is that there are not many webhooks per shard. + wh := []webhook.WebhookAccessor{} + for _, hook := range hooks { + if hook.(webhookconfiguration.WebhookClusterAccessor).GetLogicalCluster() == lc { + wh = append(wh, hook) + } + } + return wh +} + +func (p *WebhookDispatcher) SetHookSource(s generic.Source) { + p.hookSource = s +} + +// SetKcpInformers implements the WantsExternalKcpInformerFactory interface. +func (p *WebhookDispatcher) SetKcpInformers(f kcpinformers.SharedInformerFactory) { + if _, found := f.Apis().V1alpha1().APIBindings().Informer().GetIndexer().GetIndexers()[byWorkspaceIndex]; !found { + if err := f.Apis().V1alpha1().APIBindings().Informer().AddIndexers(cache.Indexers{ + byWorkspaceIndex: func(obj interface{}) ([]string, error) { + return []string{logicalcluster.From(obj.(metav1.Object)).String()}, nil + }, + }); err != nil { + // nothing we can do here. But this should also never happen. We check for existence before. + klog.Errorf("failed to add indexer for APIBindings: %v", err) + } + } + p.apiBindingsIndexer = f.Apis().V1alpha1().APIBindings().Informer().GetIndexer() + p.apiBindingsHasSynced = f.Apis().V1alpha1().APIBindings().Informer().HasSynced +} diff --git a/pkg/admission/webhook/generic_webhook_test.go b/pkg/admission/webhook/generic_webhook_test.go new file mode 100644 index 00000000000..1d1526b66a5 --- /dev/null +++ b/pkg/admission/webhook/generic_webhook_test.go @@ -0,0 +1,301 @@ +/* +Copyright 2022 The KCP Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package webhook + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/kcp-dev/apimachinery/pkg/logicalcluster" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apiserver/pkg/admission" + webhookconfiguration "k8s.io/apiserver/pkg/admission/configuration" + "k8s.io/apiserver/pkg/admission/plugin/webhook" + "k8s.io/apiserver/pkg/authentication/user" + "k8s.io/apiserver/pkg/endpoints/request" + "k8s.io/client-go/tools/cache" + + "github.com/kcp-dev/kcp/pkg/apis/apis/v1alpha1" + "github.com/kcp-dev/kcp/pkg/client/clientset/versioned/fake" + kcpinformers "github.com/kcp-dev/kcp/pkg/client/informers/externalversions" +) + +func attr(gvk schema.GroupVersionKind, name, resource string, op admission.Operation) admission.Attributes { + obj := unstructured.Unstructured{} + obj.SetGroupVersionKind(gvk) + obj.SetName(name) + return admission.NewAttributesRecord( + &obj, + nil, + obj.GroupVersionKind(), + "", + obj.GetName(), + obj.GroupVersionKind().GroupVersion().WithResource(resource), + "", + op, + &metav1.CreateOptions{}, + false, + &user.DefaultInfo{}, + ) +} + +type validatingDispatcher struct { + hooks []webhook.WebhookAccessor +} + +func (d *validatingDispatcher) Dispatch(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces, hooks []webhook.WebhookAccessor) error { + if len(hooks) != len(d.hooks) { + return fmt.Errorf("invalid number of hooks sent to dispatcher") + } + uidMatches := map[string]*struct{}{} + for _, h := range hooks { + for _, expectedHook := range d.hooks { + if h.GetUID() == expectedHook.GetUID() { + uidMatches[h.GetUID()] = &struct{}{} + } + } + } + if len(uidMatches) != len(d.hooks) { + return fmt.Errorf("hooks UID did not match expected") + } + return nil +} + +type fakeHookSource struct { + hooks []webhook.WebhookAccessor + hasSynced bool +} + +func (f fakeHookSource) Webhooks() []webhook.WebhookAccessor { + return f.hooks + +} +func (f fakeHookSource) HasSynced() bool { + return f.hasSynced +} + +func TestDispatch(t *testing.T) { + tests := []struct { + name string + attr admission.Attributes + cluster string + expectedHooks []webhook.WebhookAccessor + hooksInSource []webhook.WebhookAccessor + hookSourceNotSynced bool + apiBindings []*v1alpha1.APIBinding + apiBindingsSynced func() bool + wantErr bool + }{ + { + name: "call for APIBinding only calls hooks in api registration logical cluster", + attr: attr( + schema.GroupVersionKind{Kind: "Cowboy", Group: "wildwest.dev", Version: "v1"}, + "bound-resource", + "cowboys", + admission.Create, + ), + cluster: "root:org:dest-cluster", + expectedHooks: []webhook.WebhookAccessor{ + webhookconfiguration.WithCluster(logicalcluster.New("root:org:source-cluster"), webhook.NewValidatingWebhookAccessor("1", "api-registration-hook", nil)), + }, + hooksInSource: []webhook.WebhookAccessor{ + webhookconfiguration.WithCluster(logicalcluster.New("root:org:source-cluster"), webhook.NewValidatingWebhookAccessor("1", "api-registration-hook", nil)), + webhookconfiguration.WithCluster(logicalcluster.New("root:org:dest-cluster"), webhook.NewValidatingWebhookAccessor("2", "secrets", nil)), + }, + apiBindings: []*v1alpha1.APIBinding{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "one", + ClusterName: "root:org:dest-cluster", + }, + Status: v1alpha1.APIBindingStatus{ + BoundResources: []v1alpha1.BoundAPIResource{ + { + Group: "wildwest.dev", + Resource: "cowboys", + }, + }, + BoundAPIExport: &v1alpha1.ExportReference{ + Workspace: &v1alpha1.WorkspaceExportReference{ + WorkspaceName: "source-cluster", + }, + }, + }, + }, + }, + }, + { + name: "call for resource only calls hooks in logical cluster", + attr: attr( + schema.GroupVersionKind{Kind: "Cowboy", Group: "wildwest.dev", Version: "v1"}, + "bound-resource", + "cowboys", + admission.Create, + ), + cluster: "root:org:dest-cluster", + expectedHooks: []webhook.WebhookAccessor{ + webhookconfiguration.WithCluster(logicalcluster.New("root:org:dest-cluster"), webhook.NewValidatingWebhookAccessor("3", "secrets", nil)), + }, + hooksInSource: []webhook.WebhookAccessor{ + webhookconfiguration.WithCluster(logicalcluster.New("root:org:source-cluster"), webhook.NewValidatingWebhookAccessor("1", "cowboy-hook", nil)), + webhookconfiguration.WithCluster(logicalcluster.New("root:org:source-cluster"), webhook.NewValidatingWebhookAccessor("2", "secrets", nil)), + webhookconfiguration.WithCluster(logicalcluster.New("root:org:dest-cluster"), webhook.NewValidatingWebhookAccessor("3", "secrets", nil)), + }, + }, + { + name: "API Bindings for other logical cluster call webhooks for dest cluster", + attr: attr( + schema.GroupVersionKind{Kind: "Cowboy", Group: "wildwest.dev", Version: "v1"}, + "bound-resource", + "cowboys", + admission.Create, + ), + cluster: "root:org:dest-cluster", + expectedHooks: []webhook.WebhookAccessor{ + webhookconfiguration.WithCluster(logicalcluster.New("root:org:dest-cluster"), webhook.NewValidatingWebhookAccessor("3", "secrets", nil)), + }, + hooksInSource: []webhook.WebhookAccessor{ + webhookconfiguration.WithCluster(logicalcluster.New("root:org:source-cluster"), webhook.NewValidatingWebhookAccessor("1", "cowboy-hook", nil)), + webhookconfiguration.WithCluster(logicalcluster.New("root:org:source-cluster"), webhook.NewValidatingWebhookAccessor("2", "secrets", nil)), + webhookconfiguration.WithCluster(logicalcluster.New("root:org:dest-cluster"), webhook.NewValidatingWebhookAccessor("3", "secrets", nil)), + }, + apiBindings: []*v1alpha1.APIBinding{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "two", + ClusterName: "root:org:dest-cluster", + }, + Status: v1alpha1.APIBindingStatus{ + BoundResources: []v1alpha1.BoundAPIResource{ + { + Group: "wildwest.dev", + Resource: "Horses", + }, + }, + BoundAPIExport: &v1alpha1.ExportReference{ + Workspace: &v1alpha1.WorkspaceExportReference{ + WorkspaceName: "source-cluster", + }, + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "one", + ClusterName: "root:org:dest-cluster-2", + }, + Status: v1alpha1.APIBindingStatus{ + BoundResources: []v1alpha1.BoundAPIResource{ + { + Group: "wildwest.dev", + Resource: "Cowboys", + }, + }, + BoundAPIExport: &v1alpha1.ExportReference{ + Workspace: &v1alpha1.WorkspaceExportReference{ + WorkspaceName: "source-cluster", + }, + }, + }, + }, + }, + }, + { + name: "API Bindings Lister not synced", + attr: attr( + schema.GroupVersionKind{Kind: "Cowboy", Group: "wildwest.dev", Version: "v1"}, + "bound-resource", + "cowboys", + admission.Create, + ), + cluster: "root:org:dest-cluster", + apiBindingsSynced: func() bool { + return false + }, + wantErr: true, + }, + { + name: "hook source not synced", + attr: attr( + schema.GroupVersionKind{Kind: "Cowboy", Group: "wildwest.dev", Version: "v1"}, + "bound-resource", + "cowboys", + admission.Create, + ), + cluster: "root:org:dest-cluster", + hookSourceNotSynced: true, + wantErr: true, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + ctx, cancelFn := context.WithCancel(context.Background()) + t.Cleanup(cancelFn) + + fakeClient := fake.NewSimpleClientset(toObjects(tc.apiBindings)...) + fakeInformerFactory := kcpinformers.NewSharedInformerFactory(fakeClient, time.Hour) + err := fakeInformerFactory.Apis().V1alpha1().APIBindings().Informer().AddIndexers(cache.Indexers{ + byWorkspaceIndex: func(obj interface{}) ([]string, error) { + return []string{logicalcluster.From(obj.(metav1.Object)).String()}, nil + }, + }) + if err != nil { + t.Errorf("unable to add indexer to fake informer-%v", err) + } + + o := &WebhookDispatcher{ + Handler: admission.NewHandler(admission.Connect, admission.Create, admission.Delete, admission.Update), + dispatcher: &validatingDispatcher{hooks: tc.expectedHooks}, + hookSource: &fakeHookSource{hooks: tc.hooksInSource, hasSynced: !tc.hookSourceNotSynced}, + apiBindingsIndexer: fakeInformerFactory.Apis().V1alpha1().APIBindings().Informer().GetIndexer(), + apiBindingsHasSynced: tc.apiBindingsSynced, + } + + fakeInformerFactory.Start(ctx.Done()) + fakeInformerFactory.WaitForCacheSync(ctx.Done()) + + if tc.apiBindingsSynced == nil { + o.apiBindingsHasSynced = func() bool { return true } + } + + // Want to make sure that ready would fail based on these. + o.SetReadyFunc(func() bool { + return o.apiBindingsHasSynced() && o.hookSource.HasSynced() + }) + + ctx = request.WithCluster(ctx, request.Cluster{Name: logicalcluster.New(tc.cluster)}) + if err := o.Dispatch(ctx, tc.attr, nil); (err != nil) != tc.wantErr { + t.Fatalf("Dispatch() error = %v, wantErr %v", err, tc.wantErr) + } + }) + } +} + +func toObjects(bindings []*v1alpha1.APIBinding) []runtime.Object { + objs := make([]runtime.Object, 0, len(bindings)) + for _, binding := range bindings { + objs = append(objs, binding) + } + return objs +} diff --git a/test/e2e/apibinding/apibinding_webhook_test.go b/test/e2e/apibinding/apibinding_webhook_test.go new file mode 100644 index 00000000000..ed48881340f --- /dev/null +++ b/test/e2e/apibinding/apibinding_webhook_test.go @@ -0,0 +1,458 @@ +/* +Copyright 2021 The KCP Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package apibinding + +import ( + "context" + "sync" + "testing" + "time" + + "github.com/stretchr/testify/require" + + v1 "k8s.io/api/admission/v1" + admissionregistrationv1 "k8s.io/api/admissionregistration/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/runtime/serializer" + "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/client-go/discovery/cached/memory" + "k8s.io/client-go/dynamic" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/restmapper" + + "github.com/kcp-dev/kcp/config/helpers" + apisv1alpha1 "github.com/kcp-dev/kcp/pkg/apis/apis/v1alpha1" + clientset "github.com/kcp-dev/kcp/pkg/client/clientset/versioned" + webhookserver "github.com/kcp-dev/kcp/test/e2e/fixtures/webhook" + "github.com/kcp-dev/kcp/test/e2e/fixtures/wildwest/apis/wildwest/v1alpha1" + client "github.com/kcp-dev/kcp/test/e2e/fixtures/wildwest/client/clientset/versioned" + "github.com/kcp-dev/kcp/test/e2e/framework" +) + +func TestAPIBindingMutatingWebhook(t *testing.T) { + t.Parallel() + + server := framework.SharedKcpServer(t) + + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + + orgClusterName := framework.NewOrganizationFixture(t, server) + sourceWorkspace := framework.NewWorkspaceFixture(t, server, orgClusterName, "Universal") + targetWorkspace := framework.NewWorkspaceFixture(t, server, orgClusterName, "Universal") + + cfg := server.DefaultConfig(t) + + kcpClients, err := clientset.NewClusterForConfig(cfg) + require.NoError(t, err, "failed to construct kcp cluster client for server") + + dynamicClients, err := dynamic.NewClusterForConfig(cfg) + require.NoError(t, err, "failed to construct dynamic cluster client for server") + + kubeClusterClient, err := kubernetes.NewClusterForConfig(cfg) + require.NoError(t, err, "failed to construct client for server") + + t.Logf("Install a cowboys APIResourceSchema into workspace %q", sourceWorkspace) + mapper := restmapper.NewDeferredDiscoveryRESTMapper(memory.NewMemCacheClient(kcpClients.Cluster(sourceWorkspace).Discovery())) + err = helpers.CreateResourceFromFS(ctx, dynamicClients.Cluster(sourceWorkspace), mapper, "apiresourceschema_cowboys.yaml", testFiles) + require.NoError(t, err) + + t.Logf("Create an APIExport for it") + cowboysAPIExport := &apisv1alpha1.APIExport{ + ObjectMeta: metav1.ObjectMeta{ + Name: "today-cowboys", + }, + Spec: apisv1alpha1.APIExportSpec{ + LatestResourceSchemas: []string{"today.cowboys.wildwest.dev"}, + }, + } + _, err = kcpClients.Cluster(sourceWorkspace).ApisV1alpha1().APIExports().Create(ctx, cowboysAPIExport, metav1.CreateOptions{}) + require.NoError(t, err) + + t.Logf("Create an APIBinding in workspace %q that points to the today-cowboys export", targetWorkspace) + require.NoError(t, err) + apiBinding := &apisv1alpha1.APIBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cowboys", + }, + Spec: apisv1alpha1.APIBindingSpec{ + Reference: apisv1alpha1.ExportReference{ + Workspace: &apisv1alpha1.WorkspaceExportReference{ + WorkspaceName: sourceWorkspace.Base(), + ExportName: cowboysAPIExport.Name, + }, + }, + }, + } + + _, err = kcpClients.Cluster(targetWorkspace).ApisV1alpha1().APIBindings().Create(ctx, apiBinding, metav1.CreateOptions{}) + require.NoError(t, err) + + scheme := runtime.NewScheme() + err = admissionregistrationv1.AddToScheme(scheme) + require.NoError(t, err, "failed to add admission registration v1 scheme") + err = v1.AddToScheme(scheme) + require.NoError(t, err, "failed to add admission v1 scheme") + err = v1alpha1.AddToScheme(scheme) + require.NoError(t, err, "failed to add cowboy v1alpha1 to scheme") + cowbyClients, err := client.NewClusterForConfig(cfg) + require.NoError(t, err, "failed to add cowboy v1alpha1 to scheme") + codecs := serializer.NewCodecFactory(scheme) + deserializer := codecs.UniversalDeserializer() + + //Create Test Server and Create Validating Webhook for Cowboys in source cluster. + testWebhook := webhookserver.WebhookServer{ + Response: v1.AdmissionResponse{ + Allowed: true, + }, + ObjectGVK: schema.GroupVersionKind{ + Group: "wildwest.dev", + Version: "v1alpha1", + Kind: "Cowboy", + }, + T: t, + Lock: sync.Mutex{}, + Deserializer: deserializer, + } + + port, err := framework.GetFreePort(t) + require.NoError(t, err, "failed to get free port for test webhook") + testWebhook.StartServer(ctx, server, port) + + t.Logf("Installing webhook into the source workspace") + sideEffect := admissionregistrationv1.SideEffectClassNone + webhook := &admissionregistrationv1.MutatingWebhookConfiguration{ + TypeMeta: metav1.TypeMeta{}, + ObjectMeta: metav1.ObjectMeta{Name: "test-webhook"}, + Webhooks: []admissionregistrationv1.MutatingWebhook{{ + Name: "test-webhook.cowboy.io", + ClientConfig: admissionregistrationv1.WebhookClientConfig{ + URL: testWebhook.GetURL(), + CABundle: cfg.CAData, + }, + Rules: []admissionregistrationv1.RuleWithOperations{{ + Operations: []admissionregistrationv1.OperationType{ + admissionregistrationv1.Create, + }, + Rule: admissionregistrationv1.Rule{ + APIGroups: []string{"wildwest.dev"}, + APIVersions: []string{"v1alpha1"}, + Resources: []string{"cowboys"}, + }, + }}, + SideEffects: &sideEffect, + AdmissionReviewVersions: []string{"v1"}, + }}, + } + _, err = kubeClusterClient.Cluster(sourceWorkspace).AdmissionregistrationV1().MutatingWebhookConfigurations().Create(ctx, webhook, metav1.CreateOptions{}) + require.NoError(t, err, "failed to add validating webhook configurations") + + cowboy := v1alpha1.Cowboy{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: "testing", + }, + Spec: v1alpha1.CowboySpec{}, + } + + // Avoid race condition here by making sure that CRD is served after installing the types into logical clusters + t.Logf("Creating cowboy resource in target logical cluster") + require.Eventually(t, func() bool { + _, err = cowbyClients.Cluster(targetWorkspace).WildwestV1alpha1().Cowboys("default").Create(ctx, &cowboy, metav1.CreateOptions{}) + if err != nil && !errors.IsAlreadyExists(err) { + return false + } + return testWebhook.Calls >= 1 + + }, wait.ForeverTestTimeout, 100*time.Millisecond) +} + +func TestAPIBindingValidatingWebhook(t *testing.T) { + t.Parallel() + + server := framework.SharedKcpServer(t) + + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + + orgClusterName := framework.NewOrganizationFixture(t, server) + sourceWorkspace := framework.NewWorkspaceFixture(t, server, orgClusterName, "Universal") + targetWorkspace := framework.NewWorkspaceFixture(t, server, orgClusterName, "Universal") + + cfg := server.DefaultConfig(t) + + kcpClients, err := clientset.NewClusterForConfig(cfg) + require.NoError(t, err, "failed to construct kcp cluster client for server") + + dynamicClients, err := dynamic.NewClusterForConfig(cfg) + require.NoError(t, err, "failed to construct dynamic cluster client for server") + + kubeClusterClient, err := kubernetes.NewClusterForConfig(cfg) + require.NoError(t, err, "failed to construct client for server") + + t.Logf("Install a cowboys APIResourceSchema into workspace %q", sourceWorkspace) + mapper := restmapper.NewDeferredDiscoveryRESTMapper(memory.NewMemCacheClient(kcpClients.Cluster(sourceWorkspace).Discovery())) + err = helpers.CreateResourceFromFS(ctx, dynamicClients.Cluster(sourceWorkspace), mapper, "apiresourceschema_cowboys.yaml", testFiles) + require.NoError(t, err) + + t.Logf("Create an APIExport for it") + cowboysAPIExport := &apisv1alpha1.APIExport{ + ObjectMeta: metav1.ObjectMeta{ + Name: "today-cowboys", + }, + Spec: apisv1alpha1.APIExportSpec{ + LatestResourceSchemas: []string{"today.cowboys.wildwest.dev"}, + }, + } + _, err = kcpClients.Cluster(sourceWorkspace).ApisV1alpha1().APIExports().Create(ctx, cowboysAPIExport, metav1.CreateOptions{}) + require.NoError(t, err) + + t.Logf("Create an APIBinding in workspace %q that points to the today-cowboys export", targetWorkspace) + require.NoError(t, err) + apiBinding := &apisv1alpha1.APIBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cowboys", + }, + Spec: apisv1alpha1.APIBindingSpec{ + Reference: apisv1alpha1.ExportReference{ + Workspace: &apisv1alpha1.WorkspaceExportReference{ + WorkspaceName: sourceWorkspace.Base(), + ExportName: cowboysAPIExport.Name, + }, + }, + }, + } + + _, err = kcpClients.Cluster(targetWorkspace).ApisV1alpha1().APIBindings().Create(ctx, apiBinding, metav1.CreateOptions{}) + require.NoError(t, err) + + scheme := runtime.NewScheme() + err = admissionregistrationv1.AddToScheme(scheme) + require.NoError(t, err, "failed to add admission registration v1 scheme") + err = v1.AddToScheme(scheme) + require.NoError(t, err, "failed to add admission v1 scheme") + err = v1alpha1.AddToScheme(scheme) + require.NoError(t, err, "failed to add cowboy v1alpha1 to scheme") + cowbyClients, err := client.NewClusterForConfig(cfg) + require.NoError(t, err, "failed to add cowboy v1alpha1 to scheme") + codecs := serializer.NewCodecFactory(scheme) + deserializer := codecs.UniversalDeserializer() + + //Create Test Server and Create Validating Webhook for Cowboys in source cluster. + testWebhook := webhookserver.WebhookServer{ + Response: v1.AdmissionResponse{ + Allowed: true, + }, + ObjectGVK: schema.GroupVersionKind{ + Group: "wildwest.dev", + Version: "v1alpha1", + Kind: "Cowboy", + }, + T: t, + Lock: sync.Mutex{}, + Deserializer: deserializer, + } + + port, err := framework.GetFreePort(t) + require.NoError(t, err, "failed to get free port for test webhook") + testWebhook.StartServer(ctx, server, port) + + t.Logf("Installing webhook into the source workspace") + sideEffect := admissionregistrationv1.SideEffectClassNone + webhook := &admissionregistrationv1.ValidatingWebhookConfiguration{ + TypeMeta: metav1.TypeMeta{}, + ObjectMeta: metav1.ObjectMeta{Name: "test-webhook"}, + Webhooks: []admissionregistrationv1.ValidatingWebhook{{ + Name: "test-webhook.cowboy.io", + ClientConfig: admissionregistrationv1.WebhookClientConfig{ + URL: testWebhook.GetURL(), + CABundle: cfg.CAData, + }, + Rules: []admissionregistrationv1.RuleWithOperations{{ + Operations: []admissionregistrationv1.OperationType{ + admissionregistrationv1.Create, + }, + Rule: admissionregistrationv1.Rule{ + APIGroups: []string{"wildwest.dev"}, + APIVersions: []string{"v1alpha1"}, + Resources: []string{"cowboys"}, + }, + }}, + SideEffects: &sideEffect, + AdmissionReviewVersions: []string{"v1"}, + }}, + } + _, err = kubeClusterClient.Cluster(sourceWorkspace).AdmissionregistrationV1().ValidatingWebhookConfigurations().Create(ctx, webhook, metav1.CreateOptions{}) + require.NoError(t, err, "failed to add validating webhook configurations") + + cowboy := v1alpha1.Cowboy{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: "testing", + }, + Spec: v1alpha1.CowboySpec{}, + } + + // Avoid race condition here by making sure that CRD is served after installing the types into logical clusters + t.Logf("Creating cowboy resource in target logical cluster") + require.Eventually(t, func() bool { + _, err = cowbyClients.Cluster(targetWorkspace).WildwestV1alpha1().Cowboys("default").Create(ctx, &cowboy, metav1.CreateOptions{}) + if err != nil && !errors.IsAlreadyExists(err) { + return false + } + return testWebhook.Calls >= 1 + + }, wait.ForeverTestTimeout, 100*time.Millisecond) +} + +func TestAPIBindingValidatingWebhookNotCalledWhenBound(t *testing.T) { + t.Parallel() + + server := framework.SharedKcpServer(t) + + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + + orgClusterName := framework.NewOrganizationFixture(t, server) + sourceWorkspace := framework.NewWorkspaceFixture(t, server, orgClusterName, "Universal") + targetWorkspace := framework.NewWorkspaceFixture(t, server, orgClusterName, "Universal") + + cfg := server.DefaultConfig(t) + + kcpClients, err := clientset.NewClusterForConfig(cfg) + require.NoError(t, err, "failed to construct kcp cluster client for server") + + dynamicClients, err := dynamic.NewClusterForConfig(cfg) + require.NoError(t, err, "failed to construct dynamic cluster client for server") + + kubeClusterClient, err := kubernetes.NewClusterForConfig(cfg) + require.NoError(t, err, "failed to construct client for server") + + t.Logf("Install a cowboys APIResourceSchema into workspace %q", sourceWorkspace) + mapper := restmapper.NewDeferredDiscoveryRESTMapper(memory.NewMemCacheClient(kcpClients.Cluster(sourceWorkspace).Discovery())) + err = helpers.CreateResourceFromFS(ctx, dynamicClients.Cluster(sourceWorkspace), mapper, "apiresourceschema_cowboys.yaml", testFiles) + require.NoError(t, err) + + t.Logf("Create an APIExport for it") + cowboysAPIExport := &apisv1alpha1.APIExport{ + ObjectMeta: metav1.ObjectMeta{ + Name: "today-cowboys", + }, + Spec: apisv1alpha1.APIExportSpec{ + LatestResourceSchemas: []string{"today.cowboys.wildwest.dev"}, + }, + } + _, err = kcpClients.Cluster(sourceWorkspace).ApisV1alpha1().APIExports().Create(ctx, cowboysAPIExport, metav1.CreateOptions{}) + require.NoError(t, err) + + t.Logf("Create an APIBinding in workspace %q that points to the today-cowboys export", targetWorkspace) + require.NoError(t, err) + apiBinding := &apisv1alpha1.APIBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cowboys", + }, + Spec: apisv1alpha1.APIBindingSpec{ + Reference: apisv1alpha1.ExportReference{ + Workspace: &apisv1alpha1.WorkspaceExportReference{ + WorkspaceName: sourceWorkspace.Base(), + ExportName: cowboysAPIExport.Name, + }, + }, + }, + } + + _, err = kcpClients.Cluster(targetWorkspace).ApisV1alpha1().APIBindings().Create(ctx, apiBinding, metav1.CreateOptions{}) + require.NoError(t, err) + + scheme := runtime.NewScheme() + err = admissionregistrationv1.AddToScheme(scheme) + require.NoError(t, err, "failed to add admission registration v1 scheme") + err = v1.AddToScheme(scheme) + require.NoError(t, err, "failed to add admission v1 scheme") + err = v1alpha1.AddToScheme(scheme) + require.NoError(t, err, "failed to add cowboy v1alpha1 to scheme") + cowbyClients, err := client.NewClusterForConfig(cfg) + require.NoError(t, err, "failed to add cowboy v1alpha1 to scheme") + codecs := serializer.NewCodecFactory(scheme) + deserializer := codecs.UniversalDeserializer() + + //Create Test Server and Create Validating Webhook for Cowboys in source cluster. + testWebhook := webhookserver.WebhookServer{ + Response: v1.AdmissionResponse{ + Allowed: true, + }, + ObjectGVK: schema.GroupVersionKind{ + Group: "wildwest.dev", + Version: "v1alpha1", + Kind: "Cowboy", + }, + T: t, + Lock: sync.Mutex{}, + Deserializer: deserializer, + } + + port, err := framework.GetFreePort(t) + require.NoError(t, err, "failed to get free port for test webhook") + testWebhook.StartServer(ctx, server, port) + + t.Logf("Installing webhook into the source workspace") + sideEffect := admissionregistrationv1.SideEffectClassNone + webhook := &admissionregistrationv1.ValidatingWebhookConfiguration{ + TypeMeta: metav1.TypeMeta{}, + ObjectMeta: metav1.ObjectMeta{Name: "test-webhook"}, + Webhooks: []admissionregistrationv1.ValidatingWebhook{{ + Name: "test-webhook.cowboy.io", + ClientConfig: admissionregistrationv1.WebhookClientConfig{ + URL: testWebhook.GetURL(), + CABundle: cfg.CAData, + }, + Rules: []admissionregistrationv1.RuleWithOperations{{ + Operations: []admissionregistrationv1.OperationType{ + admissionregistrationv1.Create, + }, + Rule: admissionregistrationv1.Rule{ + APIGroups: []string{"wildwest.dev"}, + APIVersions: []string{"v1alpha1"}, + Resources: []string{"cowboys"}, + }, + }}, + SideEffects: &sideEffect, + AdmissionReviewVersions: []string{"v1"}, + }}, + } + _, err = kubeClusterClient.Cluster(targetWorkspace).AdmissionregistrationV1().ValidatingWebhookConfigurations().Create(ctx, webhook, metav1.CreateOptions{}) + require.NoError(t, err, "failed to add validating webhook configurations") + + cowboy := v1alpha1.Cowboy{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: "testing", + }, + Spec: v1alpha1.CowboySpec{}, + } + + // Avoid race condition here by making sure that CRD is served after installing the types into logical clusters + t.Logf("Creating cowboy resource in target logical cluster") + require.Eventually(t, func() bool { + _, err = cowbyClients.Cluster(targetWorkspace).WildwestV1alpha1().Cowboys("default").Create(ctx, &cowboy, metav1.CreateOptions{}) + if err != nil && !errors.IsAlreadyExists(err) { + return false + } + return testWebhook.Calls == 0 + + }, wait.ForeverTestTimeout, 100*time.Millisecond) +} diff --git a/test/e2e/conformance/webhook_test.go b/test/e2e/conformance/webhook_test.go new file mode 100644 index 00000000000..7ff27c4f2bd --- /dev/null +++ b/test/e2e/conformance/webhook_test.go @@ -0,0 +1,292 @@ +/* +Copyright 2022 The KCP Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package conformance + +import ( + "context" + "sync" + "testing" + "time" + + "github.com/kcp-dev/apimachinery/pkg/logicalcluster" + "github.com/stretchr/testify/require" + + v1 "k8s.io/api/admission/v1" + admissionregistrationv1 "k8s.io/api/admissionregistration/v1" + apiextensionsclient "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/runtime/serializer" + "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/client-go/kubernetes" + + webhookserver "github.com/kcp-dev/kcp/test/e2e/fixtures/webhook" + "github.com/kcp-dev/kcp/test/e2e/fixtures/wildwest" + "github.com/kcp-dev/kcp/test/e2e/fixtures/wildwest/apis/wildwest/v1alpha1" + client "github.com/kcp-dev/kcp/test/e2e/fixtures/wildwest/client/clientset/versioned" + "github.com/kcp-dev/kcp/test/e2e/framework" +) + +func TestMutatingWebhookInWorkspace(t *testing.T) { + t.Parallel() + + server := framework.SharedKcpServer(t) + + ctx, cancelFunc := context.WithCancel(context.Background()) + t.Cleanup(cancelFunc) + + // using known path to cert and key + cfg := server.DefaultConfig(t) + + scheme := runtime.NewScheme() + err := admissionregistrationv1.AddToScheme(scheme) + require.NoError(t, err, "failed to add admission registration v1 scheme") + err = v1.AddToScheme(scheme) + require.NoError(t, err, "failed to add admission v1 scheme") + err = v1alpha1.AddToScheme(scheme) + require.NoError(t, err, "failed to add cowboy v1alpha1 to scheme") + + codecs := serializer.NewCodecFactory(scheme) + deserializer := codecs.UniversalDeserializer() + + testWebhook := webhookserver.WebhookServer{ + Response: v1.AdmissionResponse{ + Allowed: true, + }, + ObjectGVK: schema.GroupVersionKind{ + Group: "wildwest.dev", + Version: "v1alpha1", + Kind: "Cowboy", + }, + T: t, + Lock: sync.Mutex{}, + Deserializer: deserializer, + } + + port, err := framework.GetFreePort(t) + require.NoError(t, err, "failed to get free port for test webhook") + testWebhook.StartServer(ctx, server, port) + + organization := framework.NewOrganizationFixture(t, server) + logicalClusters := []logicalcluster.LogicalCluster{ + framework.NewWorkspaceFixture(t, server, organization, "Universal"), + framework.NewWorkspaceFixture(t, server, organization, "Universal"), + } + + kubeClusterClient, err := kubernetes.NewClusterForConfig(cfg) + require.NoError(t, err, "failed to construct client for server") + cowbyClients, err := client.NewClusterForConfig(cfg) + require.NoError(t, err, "failed to construct cowboy client for server") + apiExtensionsClients, err := apiextensionsclient.NewClusterForConfig(cfg) + require.NoError(t, err, "failed to construct apiextensions client for server") + + t.Logf("Install the Cowboy resources into logical clusters") + for _, logicalCluster := range logicalClusters { + t.Logf("Bootstrapping ClusterWorkspace CRDs in logical cluster %s", logicalCluster) + crdClient := apiExtensionsClients.Cluster(logicalCluster).ApiextensionsV1().CustomResourceDefinitions() + wildwest.Create(t, crdClient, metav1.GroupResource{Group: "wildwest.dev", Resource: "cowboys"}) + } + + t.Logf("Installing webhook into the first workspace") + sideEffect := admissionregistrationv1.SideEffectClassNone + webhook := &admissionregistrationv1.MutatingWebhookConfiguration{ + TypeMeta: metav1.TypeMeta{}, + ObjectMeta: metav1.ObjectMeta{Name: "test-webhook"}, + Webhooks: []admissionregistrationv1.MutatingWebhook{{ + Name: "test-webhook.cowboy.io", + ClientConfig: admissionregistrationv1.WebhookClientConfig{ + URL: testWebhook.GetURL(), + CABundle: cfg.CAData, + }, + Rules: []admissionregistrationv1.RuleWithOperations{{ + Operations: []admissionregistrationv1.OperationType{ + admissionregistrationv1.Create, + }, + Rule: admissionregistrationv1.Rule{ + APIGroups: []string{"wildwest.dev"}, + APIVersions: []string{"v1alpha1"}, + Resources: []string{"cowboys"}, + }, + }}, + SideEffects: &sideEffect, + AdmissionReviewVersions: []string{"v1"}, + }}, + } + _, err = kubeClusterClient.Cluster(logicalClusters[0]).AdmissionregistrationV1().MutatingWebhookConfigurations().Create(ctx, webhook, metav1.CreateOptions{}) + require.NoError(t, err, "failed to add validating webhook configurations") + + cowboy := v1alpha1.Cowboy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "testing", + }, + Spec: v1alpha1.CowboySpec{}, + } + + // Avoid race condition here by making sure that CRD is served after installing the types into logical clusters + t.Logf("Creating cowboy resource in second logical cluster") + require.Eventually(t, func() bool { + _, err = cowbyClients.Cluster(logicalClusters[1]).WildwestV1alpha1().Cowboys("default").Create(ctx, &cowboy, metav1.CreateOptions{}) + if err != nil && !errors.IsAlreadyExists(err) { + return false + } + return true + + }, wait.ForeverTestTimeout, 100*time.Millisecond) + require.Equal(t, 0, testWebhook.Calls, "expected that the webhook is not called for logical cluster where webhook is not installed") + + t.Logf("Creating cowboy resource in first logical cluster") + require.Eventually(t, func() bool { + _, err = cowbyClients.Cluster(logicalClusters[0]).WildwestV1alpha1().Cowboys("default").Create(ctx, &cowboy, metav1.CreateOptions{}) + if err != nil && !errors.IsAlreadyExists(err) { + return false + } + return true + + }, wait.ForeverTestTimeout, 100*time.Millisecond) + + //Avoid race condition where webhook informer is not updated before the call to create was made. + t.Log("Verify webhook is eventually called") + require.Eventually(t, func() bool { + return testWebhook.Calls == 1 + }, wait.ForeverTestTimeout, 100*time.Millisecond) +} + +func TestValidatingWebhookInWorkspace(t *testing.T) { + t.Parallel() + + server := framework.SharedKcpServer(t) + + ctx, cancelFunc := context.WithCancel(context.Background()) + t.Cleanup(cancelFunc) + + // using known path to cert and key + cfg := server.DefaultConfig(t) + + scheme := runtime.NewScheme() + err := admissionregistrationv1.AddToScheme(scheme) + require.NoError(t, err, "failed to add admission registration v1 scheme") + err = v1.AddToScheme(scheme) + require.NoError(t, err, "failed to add admission v1 scheme") + err = v1alpha1.AddToScheme(scheme) + require.NoError(t, err, "failed to add cowboy v1alpha1 to scheme") + + codecs := serializer.NewCodecFactory(scheme) + deserializer := codecs.UniversalDeserializer() + + testWebhook := webhookserver.WebhookServer{ + Response: v1.AdmissionResponse{ + Allowed: true, + }, + ObjectGVK: schema.GroupVersionKind{ + Group: "wildwest.dev", + Version: "v1alpha1", + Kind: "Cowboy", + }, + T: t, + Lock: sync.Mutex{}, + Deserializer: deserializer, + } + + port, err := framework.GetFreePort(t) + require.NoError(t, err, "failed to get free port for test webhook") + testWebhook.StartServer(ctx, server, port) + + organization := framework.NewOrganizationFixture(t, server) + logicalClusters := []logicalcluster.LogicalCluster{ + framework.NewWorkspaceFixture(t, server, organization, "Universal"), + framework.NewWorkspaceFixture(t, server, organization, "Universal"), + } + + kubeClusterClient, err := kubernetes.NewClusterForConfig(cfg) + require.NoError(t, err, "failed to construct client for server") + cowbyClients, err := client.NewClusterForConfig(cfg) + require.NoError(t, err, "failed to construct cowboy client for server") + apiExtensionsClients, err := apiextensionsclient.NewClusterForConfig(cfg) + require.NoError(t, err, "failed to construct apiextensions client for server") + + t.Logf("Install the Cowboy resources into logical clusters") + for _, logicalCluster := range logicalClusters { + t.Logf("Bootstrapping ClusterWorkspace CRDs in logical cluster %s", logicalCluster) + crdClient := apiExtensionsClients.Cluster(logicalCluster).ApiextensionsV1().CustomResourceDefinitions() + wildwest.Create(t, crdClient, metav1.GroupResource{Group: "wildwest.dev", Resource: "cowboys"}) + } + + t.Logf("Installing webhook into the first workspace") + sideEffect := admissionregistrationv1.SideEffectClassNone + webhook := &admissionregistrationv1.ValidatingWebhookConfiguration{ + TypeMeta: metav1.TypeMeta{}, + ObjectMeta: metav1.ObjectMeta{Name: "test-webhook"}, + Webhooks: []admissionregistrationv1.ValidatingWebhook{{ + Name: "test-webhook.cowboy.io", + ClientConfig: admissionregistrationv1.WebhookClientConfig{ + URL: testWebhook.GetURL(), + CABundle: cfg.CAData, + }, + Rules: []admissionregistrationv1.RuleWithOperations{{ + Operations: []admissionregistrationv1.OperationType{ + admissionregistrationv1.Create, + }, + Rule: admissionregistrationv1.Rule{ + APIGroups: []string{"wildwest.dev"}, + APIVersions: []string{"v1alpha1"}, + Resources: []string{"cowboys"}, + }, + }}, + SideEffects: &sideEffect, + AdmissionReviewVersions: []string{"v1"}, + }}, + } + _, err = kubeClusterClient.Cluster(logicalClusters[0]).AdmissionregistrationV1().ValidatingWebhookConfigurations().Create(ctx, webhook, metav1.CreateOptions{}) + require.NoError(t, err, "failed to add validating webhook configurations") + + cowboy := v1alpha1.Cowboy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "testing", + }, + Spec: v1alpha1.CowboySpec{}, + } + + // Avoid race condition here by making sure that CRD is served after installing the types into logical clusters + t.Logf("Creating cowboy resource in second logical cluster") + require.Eventually(t, func() bool { + _, err = cowbyClients.Cluster(logicalClusters[1]).WildwestV1alpha1().Cowboys("default").Create(ctx, &cowboy, metav1.CreateOptions{}) + if err != nil && !errors.IsAlreadyExists(err) { + return false + } + return true + + }, wait.ForeverTestTimeout, 100*time.Millisecond) + require.Equal(t, 0, testWebhook.Calls, "expected that the webhook is not called for logical cluster where webhook is not installed") + + t.Logf("Creating cowboy resource in first logical cluster") + require.Eventually(t, func() bool { + _, err = cowbyClients.Cluster(logicalClusters[0]).WildwestV1alpha1().Cowboys("default").Create(ctx, &cowboy, metav1.CreateOptions{}) + if err != nil && !errors.IsAlreadyExists(err) { + return false + } + return true + + }, wait.ForeverTestTimeout, 100*time.Millisecond) + + //Avoid race condition where webhook informer is not updated before the call to create was made. + t.Log("Verify webhook is eventually called") + require.Eventually(t, func() bool { + return testWebhook.Calls == 1 + }, wait.ForeverTestTimeout, 100*time.Millisecond) +} diff --git a/test/e2e/fixtures/webhook/webhook.go b/test/e2e/fixtures/webhook/webhook.go new file mode 100644 index 00000000000..163d23f19c6 --- /dev/null +++ b/test/e2e/fixtures/webhook/webhook.go @@ -0,0 +1,161 @@ +/* +Copyright 2022 The KCP Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package Webhook + +import ( + "context" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "path/filepath" + "sync" + "testing" + + v1 "k8s.io/api/admission/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + + "github.com/kcp-dev/kcp/test/e2e/framework" +) + +type WebhookServer struct { + Response v1.AdmissionResponse + ObjectGVK schema.GroupVersionKind + T *testing.T + Deserializer runtime.Decoder + + port string + + Lock sync.Mutex + Calls int +} + +func (t *WebhookServer) StartServer(ctx context.Context, server framework.RunningServer, port string) { + dirPath := filepath.Dir(server.KubeconfigPath()) + // using known path to cert and key + cfg := server.DefaultConfig(t.T) + cfg.CertFile = filepath.Join(dirPath, "apiserver.crt") + cfg.KeyFile = filepath.Join(dirPath, "apiserver.key") + t.port = port + serv := &http.Server{Addr: fmt.Sprintf(":%v", port), Handler: t} + go func() { + <-ctx.Done() + fmt.Printf("Shutting down the HTTP server") + err := serv.Shutdown(context.TODO()) + if err != nil { + fmt.Printf("unable to shutdown server gracefully err: %v", err) + } + }() + go func() { + err := serv.ListenAndServeTLS(cfg.CertFile, cfg.KeyFile) + if err != nil && err != http.ErrServerClosed { + fmt.Printf("unable to shutdown server gracefully err: %v", err) + } + }() +} + +func (t *WebhookServer) GetURL() *string { + s := fmt.Sprintf("https://localhost:%v/hello", t.port) + return &s +} + +func (t *WebhookServer) ServeHTTP(resp http.ResponseWriter, req *http.Request) { + // Make sure that this is a request for the object that was set. + t.T.Log("made it webhook") + if req.Body == nil { + msg := "Expected request body to be non-empty" + t.T.Logf("%v", msg) + http.Error(resp, msg, http.StatusBadRequest) + } + + data, err := ioutil.ReadAll(req.Body) + if err != nil { + msg := fmt.Sprintf("Request could not be decoded: %v", err) + t.T.Logf("%v", msg) + http.Error(resp, msg, http.StatusBadRequest) + } + + // verify the content type is accurate + contentType := req.Header.Get("Content-Type") + if contentType != "application/json" { + msg := fmt.Sprintf("contentType=%s, expect application/json", contentType) + t.T.Logf("%v", msg) + http.Error(resp, msg, http.StatusBadRequest) + return + } + + obj, gvk, err := t.Deserializer.Decode(data, nil, nil) + if err != nil { + msg := fmt.Sprintf("Unable to decode object: %v", err) + t.T.Logf("%v", msg) + http.Error(resp, msg, http.StatusBadRequest) + return + } + + if *gvk != v1.SchemeGroupVersion.WithKind("AdmissionReview") { + msg := fmt.Sprintf("Expected AdmissionReview but got: %T", obj) + t.T.Logf("%v", msg) + http.Error(resp, msg, http.StatusBadRequest) + return + } + requestedAdmissionReview, ok := obj.(*v1.AdmissionReview) + if !ok { + //return an error + msg := fmt.Sprintf("Expected AdmissionReview but got: %T", obj) + t.T.Logf("%v", msg) + http.Error(resp, msg, http.StatusBadRequest) + return + } + obj, objGVK, err := t.Deserializer.Decode(requestedAdmissionReview.Request.Object.Raw, nil, nil) + if err != nil { + msg := fmt.Sprintf("Unable to decode admissions reqeusted object: %v", err) + t.T.Logf("%v", msg) + http.Error(resp, msg, http.StatusBadRequest) + return + } + + if t.ObjectGVK != *objGVK { + //return an error + msg := fmt.Sprintf("Expected ObjectGVK: %v but got: %T", t.ObjectGVK, obj) + t.T.Logf("%v", msg) + http.Error(resp, msg, http.StatusBadRequest) + return + } + + responseAdmissionReview := &v1.AdmissionReview{ + TypeMeta: requestedAdmissionReview.TypeMeta, + } + responseAdmissionReview.Response = &t.Response + responseAdmissionReview.Response.UID = requestedAdmissionReview.Request.UID + + respBytes, err := json.Marshal(responseAdmissionReview) + if err != nil { + t.T.Logf("%v", err) + http.Error(resp, err.Error(), http.StatusInternalServerError) + return + } + + t.Lock.Lock() + t.Calls = t.Calls + 1 + t.Lock.Unlock() + + resp.Header().Set("Content-Type", "application/json") + if _, err := resp.Write(respBytes); err != nil { + t.T.Logf("%v", err) + } +}