mirror of https://github.com/k3s-io/k3s
Prevent webhooks from affecting admission requests for webhooks
parent
07240b7166
commit
58b43ad27d
|
@ -135,6 +135,10 @@ type Webhook struct {
|
||||||
|
|
||||||
// Rules describes what operations on what resources/subresources the webhook cares about.
|
// Rules describes what operations on what resources/subresources the webhook cares about.
|
||||||
// The webhook cares about an operation if it matches _any_ Rule.
|
// The webhook cares about an operation if it matches _any_ Rule.
|
||||||
|
// However, in order to prevent ValidatingAdmissionWebhooks and MutatingAdmissionWebhooks
|
||||||
|
// from putting the cluster in a state which cannot be recovered from without completely
|
||||||
|
// disabling the plugin, ValidatingAdmissionWebhooks and MutatingAdmissionWebhooks are never called
|
||||||
|
// on admission requests for ValidatingWebhookConfiguration and MutatingWebhookConfiguration objects.
|
||||||
Rules []RuleWithOperations `json:"rules,omitempty" protobuf:"bytes,3,rep,name=rules"`
|
Rules []RuleWithOperations `json:"rules,omitempty" protobuf:"bytes,3,rep,name=rules"`
|
||||||
|
|
||||||
// FailurePolicy defines how unrecognized errors from the admission endpoint are handled -
|
// FailurePolicy defines how unrecognized errors from the admission endpoint are handled -
|
||||||
|
|
|
@ -186,6 +186,10 @@ func (a *MutatingWebhook) loadConfiguration(attr admission.Attributes) *v1beta1.
|
||||||
|
|
||||||
// Admit makes an admission decision based on the request attributes.
|
// Admit makes an admission decision based on the request attributes.
|
||||||
func (a *MutatingWebhook) Admit(attr admission.Attributes) error {
|
func (a *MutatingWebhook) Admit(attr admission.Attributes) error {
|
||||||
|
if rules.IsWebhookConfigurationResource(attr) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
if !a.WaitForReady() {
|
if !a.WaitForReady() {
|
||||||
return admission.NewForbidden(attr, fmt.Errorf("not yet ready to handle request"))
|
return admission.NewForbidden(attr, fmt.Errorf("not yet ready to handle request"))
|
||||||
}
|
}
|
||||||
|
|
|
@ -93,3 +93,15 @@ func (r *Matcher) resource() bool {
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsWebhookConfigurationResource determines if an admission.Attributes object is describing
|
||||||
|
// the admission of a ValidatingWebhookConfiguration or a MutatingWebhookConfiguration
|
||||||
|
func IsWebhookConfigurationResource(attr admission.Attributes) bool {
|
||||||
|
gvk := attr.GetKind()
|
||||||
|
if gvk.Group == "admissionregistration.k8s.io" {
|
||||||
|
if gvk.Kind == "ValidatingWebhookConfiguration" || gvk.Kind == "MutatingWebhookConfiguration" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
|
@ -174,6 +174,10 @@ func (a *ValidatingAdmissionWebhook) loadConfiguration(attr admission.Attributes
|
||||||
|
|
||||||
// Validate makes an admission decision based on the request attributes.
|
// Validate makes an admission decision based on the request attributes.
|
||||||
func (a *ValidatingAdmissionWebhook) Validate(attr admission.Attributes) error {
|
func (a *ValidatingAdmissionWebhook) Validate(attr admission.Attributes) error {
|
||||||
|
if rules.IsWebhookConfigurationResource(attr) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
if !a.WaitForReady() {
|
if !a.WaitForReady() {
|
||||||
return admission.NewForbidden(attr, fmt.Errorf("not yet ready to handle request"))
|
return admission.NewForbidden(attr, fmt.Errorf("not yet ready to handle request"))
|
||||||
}
|
}
|
||||||
|
@ -266,7 +270,7 @@ func (a *ValidatingAdmissionWebhook) Validate(attr admission.Attributes) error {
|
||||||
return errs[0]
|
return errs[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: factor into a common place along with the validating webhook version.
|
// TODO: factor into a common place along with the mutating webhook version.
|
||||||
func (a *ValidatingAdmissionWebhook) shouldCallHook(h *v1beta1.Webhook, attr admission.Attributes) (bool, *apierrors.StatusError) {
|
func (a *ValidatingAdmissionWebhook) shouldCallHook(h *v1beta1.Webhook, attr admission.Attributes) (bool, *apierrors.StatusError) {
|
||||||
var matches bool
|
var matches bool
|
||||||
for _, r := range h.Rules {
|
for _, r := range h.Rules {
|
||||||
|
|
|
@ -57,6 +57,9 @@ const (
|
||||||
podMutatingWebhookConfigName = "e2e-test-mutating-webhook-pod"
|
podMutatingWebhookConfigName = "e2e-test-mutating-webhook-pod"
|
||||||
crdMutatingWebhookConfigName = "e2e-test-mutating-webhook-config-crd"
|
crdMutatingWebhookConfigName = "e2e-test-mutating-webhook-config-crd"
|
||||||
webhookFailClosedConfigName = "e2e-test-webhook-fail-closed"
|
webhookFailClosedConfigName = "e2e-test-webhook-fail-closed"
|
||||||
|
webhookForWebhooksConfigName = "e2e-test-webhook-for-webhooks-config"
|
||||||
|
removableValidatingHookName = "e2e-test-should-be-removable-validating-webhook-config"
|
||||||
|
removableMutatingHookName = "e2e-test-should-be-removable-mutating-webhook-config"
|
||||||
|
|
||||||
skipNamespaceLabelKey = "skip-webhook-admission"
|
skipNamespaceLabelKey = "skip-webhook-admission"
|
||||||
skipNamespaceLabelValue = "yes"
|
skipNamespaceLabelValue = "yes"
|
||||||
|
@ -141,6 +144,12 @@ var _ = SIGDescribe("AdmissionWebhook", func() {
|
||||||
testMutatingPodWebhook(f)
|
testMutatingPodWebhook(f)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
It("Should not be able to prevent deleting validating-webhook-configurations or mutating-webhook-configurations", func() {
|
||||||
|
webhookCleanup := registerWebhookForWebhookConfigurations(f, context)
|
||||||
|
defer webhookCleanup()
|
||||||
|
testWebhookForWebhookConfigurations(f)
|
||||||
|
})
|
||||||
|
|
||||||
It("Should mutate crd", func() {
|
It("Should mutate crd", func() {
|
||||||
testcrd, err := framework.CreateTestCRD(f)
|
testcrd, err := framework.CreateTestCRD(f)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -375,7 +384,7 @@ func registerWebhook(f *framework.Framework, context *certContext) func() {
|
||||||
})
|
})
|
||||||
framework.ExpectNoError(err, "registering webhook config %s with namespace %s", configName, namespace)
|
framework.ExpectNoError(err, "registering webhook config %s with namespace %s", configName, namespace)
|
||||||
|
|
||||||
// The webhook configuration is honored in 1s.
|
// The webhook configuration is honored in 10s.
|
||||||
time.Sleep(10 * time.Second)
|
time.Sleep(10 * time.Second)
|
||||||
|
|
||||||
return func() {
|
return func() {
|
||||||
|
@ -437,7 +446,7 @@ func registerMutatingWebhookForConfigMap(f *framework.Framework, context *certCo
|
||||||
})
|
})
|
||||||
framework.ExpectNoError(err, "registering mutating webhook config %s with namespace %s", configName, namespace)
|
framework.ExpectNoError(err, "registering mutating webhook config %s with namespace %s", configName, namespace)
|
||||||
|
|
||||||
// The webhook configuration is honored in 1s.
|
// The webhook configuration is honored in 10s.
|
||||||
time.Sleep(10 * time.Second)
|
time.Sleep(10 * time.Second)
|
||||||
return func() { client.AdmissionregistrationV1beta1().MutatingWebhookConfigurations().Delete(configName, nil) }
|
return func() { client.AdmissionregistrationV1beta1().MutatingWebhookConfigurations().Delete(configName, nil) }
|
||||||
}
|
}
|
||||||
|
@ -493,7 +502,7 @@ func registerMutatingWebhookForPod(f *framework.Framework, context *certContext)
|
||||||
})
|
})
|
||||||
framework.ExpectNoError(err, "registering mutating webhook config %s with namespace %s", configName, namespace)
|
framework.ExpectNoError(err, "registering mutating webhook config %s with namespace %s", configName, namespace)
|
||||||
|
|
||||||
// The webhook configuration is honored in 1s.
|
// The webhook configuration is honored in 10s.
|
||||||
time.Sleep(10 * time.Second)
|
time.Sleep(10 * time.Second)
|
||||||
|
|
||||||
return func() { client.AdmissionregistrationV1beta1().MutatingWebhookConfigurations().Delete(configName, nil) }
|
return func() { client.AdmissionregistrationV1beta1().MutatingWebhookConfigurations().Delete(configName, nil) }
|
||||||
|
@ -709,6 +718,152 @@ func testFailClosedWebhook(f *framework.Framework) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func registerWebhookForWebhookConfigurations(f *framework.Framework, context *certContext) func() {
|
||||||
|
var err error
|
||||||
|
client := f.ClientSet
|
||||||
|
By("Registering a webhook on ValidatingWebhookConfiguration and MutatingWebhookConfiguration objects, via the AdmissionRegistration API")
|
||||||
|
|
||||||
|
namespace := f.Namespace.Name
|
||||||
|
configName := webhookForWebhooksConfigName
|
||||||
|
failurePolicy := v1beta1.Fail
|
||||||
|
|
||||||
|
// This webhook will deny all requests to Delete admissionregistration objects
|
||||||
|
_, err = client.AdmissionregistrationV1beta1().ValidatingWebhookConfigurations().Create(&v1beta1.ValidatingWebhookConfiguration{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: configName,
|
||||||
|
},
|
||||||
|
Webhooks: []v1beta1.Webhook{
|
||||||
|
{
|
||||||
|
Name: "deny-webhook-configuration-deletions.k8s.io",
|
||||||
|
Rules: []v1beta1.RuleWithOperations{{
|
||||||
|
Operations: []v1beta1.OperationType{v1beta1.Delete},
|
||||||
|
Rule: v1beta1.Rule{
|
||||||
|
APIGroups: []string{"admissionregistration.k8s.io"},
|
||||||
|
APIVersions: []string{"*"},
|
||||||
|
Resources: []string{
|
||||||
|
"validatingwebhookconfigurations",
|
||||||
|
"mutatingwebhookconfigurations",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
ClientConfig: v1beta1.WebhookClientConfig{
|
||||||
|
Service: &v1beta1.ServiceReference{
|
||||||
|
Namespace: namespace,
|
||||||
|
Name: serviceName,
|
||||||
|
Path: strPtr("/always-deny"),
|
||||||
|
},
|
||||||
|
CABundle: context.signingCert,
|
||||||
|
},
|
||||||
|
FailurePolicy: &failurePolicy,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
framework.ExpectNoError(err, "registering webhook config %s with namespace %s", configName, namespace)
|
||||||
|
|
||||||
|
// The webhook configuration is honored in 10s.
|
||||||
|
time.Sleep(10 * time.Second)
|
||||||
|
return func() {
|
||||||
|
err := client.AdmissionregistrationV1beta1().ValidatingWebhookConfigurations().Delete(configName, nil)
|
||||||
|
framework.ExpectNoError(err, "deleting webhook config %s with namespace %s", configName, namespace)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This test assumes that the deletion-rejecting webhook defined in
|
||||||
|
// registerWebhookForWebhookConfigurations is in place.
|
||||||
|
func testWebhookForWebhookConfigurations(f *framework.Framework) {
|
||||||
|
var err error
|
||||||
|
client := f.ClientSet
|
||||||
|
By("Creating a validating-webhook-configuration object")
|
||||||
|
|
||||||
|
namespace := f.Namespace.Name
|
||||||
|
failurePolicy := v1beta1.Ignore
|
||||||
|
|
||||||
|
_, err = client.AdmissionregistrationV1beta1().ValidatingWebhookConfigurations().Create(&v1beta1.ValidatingWebhookConfiguration{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: removableValidatingHookName,
|
||||||
|
},
|
||||||
|
Webhooks: []v1beta1.Webhook{
|
||||||
|
{
|
||||||
|
Name: "should-be-removable-validating-webhook.k8s.io",
|
||||||
|
Rules: []v1beta1.RuleWithOperations{{
|
||||||
|
Operations: []v1beta1.OperationType{v1beta1.Create},
|
||||||
|
Rule: v1beta1.Rule{
|
||||||
|
APIGroups: []string{"*"},
|
||||||
|
APIVersions: []string{"*"},
|
||||||
|
Resources: []string{"*"},
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
ClientConfig: v1beta1.WebhookClientConfig{
|
||||||
|
Service: &v1beta1.ServiceReference{
|
||||||
|
Namespace: namespace,
|
||||||
|
Name: serviceName,
|
||||||
|
// This path not recognized by the webhook service,
|
||||||
|
// so the call to this webhook will always fail,
|
||||||
|
// but because the failure policy is ignore, it will
|
||||||
|
// have no effect on admission requests.
|
||||||
|
Path: strPtr(""),
|
||||||
|
},
|
||||||
|
CABundle: nil,
|
||||||
|
},
|
||||||
|
FailurePolicy: &failurePolicy,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
framework.ExpectNoError(err, "registering webhook config %s with namespace %s", removableValidatingHookName, namespace)
|
||||||
|
|
||||||
|
// The webhook configuration is honored in 10s.
|
||||||
|
time.Sleep(10 * time.Second)
|
||||||
|
|
||||||
|
By("Deleting the validating-webhook-configuration, which should be possible to remove")
|
||||||
|
|
||||||
|
err = client.AdmissionregistrationV1beta1().ValidatingWebhookConfigurations().Delete(removableValidatingHookName, nil)
|
||||||
|
framework.ExpectNoError(err, "deleting webhook config %s with namespace %s", removableValidatingHookName, namespace)
|
||||||
|
|
||||||
|
By("Creating a mutating-webhook-configuration object")
|
||||||
|
|
||||||
|
_, err = client.AdmissionregistrationV1beta1().MutatingWebhookConfigurations().Create(&v1beta1.MutatingWebhookConfiguration{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: removableMutatingHookName,
|
||||||
|
},
|
||||||
|
Webhooks: []v1beta1.Webhook{
|
||||||
|
{
|
||||||
|
Name: "should-be-removable-mutating-webhook.k8s.io",
|
||||||
|
Rules: []v1beta1.RuleWithOperations{{
|
||||||
|
Operations: []v1beta1.OperationType{v1beta1.Create},
|
||||||
|
Rule: v1beta1.Rule{
|
||||||
|
APIGroups: []string{"*"},
|
||||||
|
APIVersions: []string{"*"},
|
||||||
|
Resources: []string{"*"},
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
ClientConfig: v1beta1.WebhookClientConfig{
|
||||||
|
Service: &v1beta1.ServiceReference{
|
||||||
|
Namespace: namespace,
|
||||||
|
Name: serviceName,
|
||||||
|
// This path not recognized by the webhook service,
|
||||||
|
// so the call to this webhook will always fail,
|
||||||
|
// but because the failure policy is ignore, it will
|
||||||
|
// have no effect on admission requests.
|
||||||
|
Path: strPtr(""),
|
||||||
|
},
|
||||||
|
CABundle: nil,
|
||||||
|
},
|
||||||
|
FailurePolicy: &failurePolicy,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
framework.ExpectNoError(err, "registering webhook config %s with namespace %s", removableMutatingHookName, namespace)
|
||||||
|
|
||||||
|
// The webhook configuration is honored in 10s.
|
||||||
|
time.Sleep(10 * time.Second)
|
||||||
|
|
||||||
|
By("Deleting the mutating-webhook-configuration, which should be possible to remove")
|
||||||
|
|
||||||
|
err = client.AdmissionregistrationV1beta1().MutatingWebhookConfigurations().Delete(removableMutatingHookName, nil)
|
||||||
|
framework.ExpectNoError(err, "deleting webhook config %s with namespace %s", removableMutatingHookName, namespace)
|
||||||
|
}
|
||||||
|
|
||||||
func createNamespace(f *framework.Framework, ns *v1.Namespace) error {
|
func createNamespace(f *framework.Framework, ns *v1.Namespace) error {
|
||||||
return wait.PollImmediate(100*time.Millisecond, 30*time.Second, func() (bool, error) {
|
return wait.PollImmediate(100*time.Millisecond, 30*time.Second, func() (bool, error) {
|
||||||
_, err := f.ClientSet.CoreV1().Namespaces().Create(ns)
|
_, err := f.ClientSet.CoreV1().Namespaces().Create(ns)
|
||||||
|
@ -849,7 +1004,7 @@ func registerWebhookForCRD(f *framework.Framework, context *certContext, testcrd
|
||||||
})
|
})
|
||||||
framework.ExpectNoError(err, "registering crd webhook config %s with namespace %s", configName, namespace)
|
framework.ExpectNoError(err, "registering crd webhook config %s with namespace %s", configName, namespace)
|
||||||
|
|
||||||
// The webhook configuration is honored in 1s.
|
// The webhook configuration is honored in 10s.
|
||||||
time.Sleep(10 * time.Second)
|
time.Sleep(10 * time.Second)
|
||||||
return func() {
|
return func() {
|
||||||
client.AdmissionregistrationV1beta1().ValidatingWebhookConfigurations().Delete(configName, nil)
|
client.AdmissionregistrationV1beta1().ValidatingWebhookConfigurations().Delete(configName, nil)
|
||||||
|
@ -909,7 +1064,7 @@ func registerMutatingWebhookForCRD(f *framework.Framework, context *certContext,
|
||||||
})
|
})
|
||||||
framework.ExpectNoError(err, "registering crd webhook config %s with namespace %s", configName, namespace)
|
framework.ExpectNoError(err, "registering crd webhook config %s with namespace %s", configName, namespace)
|
||||||
|
|
||||||
// The webhook configuration is honored in 1s.
|
// The webhook configuration is honored in 10s.
|
||||||
time.Sleep(10 * time.Second)
|
time.Sleep(10 * time.Second)
|
||||||
|
|
||||||
return func() { client.AdmissionregistrationV1beta1().MutatingWebhookConfigurations().Delete(configName, nil) }
|
return func() { client.AdmissionregistrationV1beta1().MutatingWebhookConfigurations().Delete(configName, nil) }
|
||||||
|
|
|
@ -12,9 +12,12 @@
|
||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
|
IMAGE = gcr.io/kubernetes-e2e-test-images/k8s-sample-admission-webhook-amd64
|
||||||
|
TAG = 1.9v2
|
||||||
|
|
||||||
build:
|
build:
|
||||||
CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o webhook .
|
CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o webhook .
|
||||||
docker build --no-cache -t gcr.io/kubernetes-e2e-test-images/k8s-sample-admission-webhook-amd64:1.9v1 .
|
docker build --no-cache -t $(IMAGE):$(TAG) .
|
||||||
rm -rf webhook
|
rm -rf webhook
|
||||||
push:
|
push:
|
||||||
docker push gcr.io/kubernetes-e2e-test-images/k8s-sample-admission-webhook-amd64:1.9v1
|
docker push $(IMAGE):$(TAG)
|
||||||
|
|
|
@ -67,6 +67,15 @@ func toAdmissionResponse(err error) *v1beta1.AdmissionResponse {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Deny all requests made to this function.
|
||||||
|
func alwaysDeny(ar v1beta1.AdmissionReview) *v1beta1.AdmissionResponse {
|
||||||
|
glog.V(2).Info("calling always-deny")
|
||||||
|
reviewResponse := v1beta1.AdmissionResponse{}
|
||||||
|
reviewResponse.Allowed = false
|
||||||
|
reviewResponse.Result = &metav1.Status{Message: "this webhook denies all requests"}
|
||||||
|
return &reviewResponse
|
||||||
|
}
|
||||||
|
|
||||||
// only allow pods to pull images from specific registry.
|
// only allow pods to pull images from specific registry.
|
||||||
func admitPods(ar v1beta1.AdmissionReview) *v1beta1.AdmissionResponse {
|
func admitPods(ar v1beta1.AdmissionReview) *v1beta1.AdmissionResponse {
|
||||||
glog.V(2).Info("admitting pods")
|
glog.V(2).Info("admitting pods")
|
||||||
|
@ -295,6 +304,10 @@ func serve(w http.ResponseWriter, r *http.Request, admit admitFunc) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func serveAlwaysDeny(w http.ResponseWriter, r *http.Request) {
|
||||||
|
serve(w, r, alwaysDeny)
|
||||||
|
}
|
||||||
|
|
||||||
func servePods(w http.ResponseWriter, r *http.Request) {
|
func servePods(w http.ResponseWriter, r *http.Request) {
|
||||||
serve(w, r, admitPods)
|
serve(w, r, admitPods)
|
||||||
}
|
}
|
||||||
|
@ -324,6 +337,7 @@ func main() {
|
||||||
config.addFlags()
|
config.addFlags()
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
|
http.HandleFunc("/always-deny", serveAlwaysDeny)
|
||||||
http.HandleFunc("/pods", servePods)
|
http.HandleFunc("/pods", servePods)
|
||||||
http.HandleFunc("/mutating-pods", serveMutatePods)
|
http.HandleFunc("/mutating-pods", serveMutatePods)
|
||||||
http.HandleFunc("/configmaps", serveConfigmaps)
|
http.HandleFunc("/configmaps", serveConfigmaps)
|
||||||
|
|
|
@ -48,7 +48,7 @@ func (i *ImageConfig) SetVersion(version string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
AdmissionWebhook = ImageConfig{e2eRegistry, "k8s-sample-admission-webhook", "1.9v1", true}
|
AdmissionWebhook = ImageConfig{e2eRegistry, "k8s-sample-admission-webhook", "1.9v2", true}
|
||||||
APIServer = ImageConfig{e2eRegistry, "k8s-aggregator-sample-apiserver", "1.7v2", true}
|
APIServer = ImageConfig{e2eRegistry, "k8s-aggregator-sample-apiserver", "1.7v2", true}
|
||||||
AppArmorLoader = ImageConfig{gcRegistry, "apparmor-loader", "0.1", false}
|
AppArmorLoader = ImageConfig{gcRegistry, "apparmor-loader", "0.1", false}
|
||||||
BusyBox = ImageConfig{gcRegistry, "busybox", "1.24", false}
|
BusyBox = ImageConfig{gcRegistry, "busybox", "1.24", false}
|
||||||
|
|
Loading…
Reference in New Issue