Unverified Commit 638add6d authored by Kubernetes Submit Queue's avatar Kubernetes Submit Queue Committed by GitHub

Merge pull request #54892 from caesarxuchao/add-mutating-webhook-plugin

Automatic merge from submit-queue. If you want to cherry-pick this change to another branch, please follow the instructions <a href="https://github.com/kubernetes/community/blob/master/contributors/devel/cherry-picks.md">here</a>. Adding mutating webhook admission plugin Ref #https://github.com/kubernetes/features/issues/492 I made a change to the API to plumb the `Patch` into the response. I'll rebase onto the actual API once https://github.com/kubernetes/kubernetes/pull/55829 is merged. We should update the release notes to point to the user docs when we have any. ```release-note Added mutation supports to admission webhooks. ``` TODO: - [ ] update test image to v6 after #55829 is merged - [ ] rename the GenericAdmissionWebhook to ValidatingAdmissionWebhook - [ ] reduce json marshal/unmarshal roundtrip: https://github.com/kubernetes/kubernetes/pull/54892#discussion_r151336838 - [ ] move the matching function to a common package that validating and mutating webhooks can both import. - [ ] handle namespace GET failure gracefully for fail open webhook?
parents 5e508b37 0b3ee540
......@@ -316,7 +316,7 @@ if [[ -n "${GCE_GLBC_IMAGE:-}" ]]; then
fi
if [[ -z "${KUBE_ADMISSION_CONTROL:-}" ]]; then
ADMISSION_CONTROL="Initializers,NamespaceLifecycle,LimitRanger,ServiceAccount,PersistentVolumeLabel,PodPreset,DefaultStorageClass,DefaultTolerationSeconds,NodeRestriction,Priority"
ADMISSION_CONTROL="MutatingAdmissionWebhook,Initializers,NamespaceLifecycle,LimitRanger,ServiceAccount,PersistentVolumeLabel,PodPreset,DefaultStorageClass,DefaultTolerationSeconds,NodeRestriction,Priority"
if [[ "${ENABLE_POD_SECURITY_POLICY:-}" == "true" ]]; then
ADMISSION_CONTROL="${ADMISSION_CONTROL},PodSecurityPolicy"
fi
......
......@@ -104,8 +104,8 @@ func TestAddFlags(t *testing.T) {
MinRequestTimeout: 1800,
},
Admission: &apiserveroptions.AdmissionOptions{
RecommendedPluginOrder: []string{"NamespaceLifecycle", "Initializers", "GenericAdmissionWebhook"},
DefaultOffPlugins: []string{"Initializers", "GenericAdmissionWebhook"},
RecommendedPluginOrder: []string{"MutatingAdmissionWebhook", "NamespaceLifecycle", "Initializers", "GenericAdmissionWebhook"},
DefaultOffPlugins: []string{"MutatingAdmissionWebhook", "Initializers", "GenericAdmissionWebhook"},
PluginNames: []string{"AlwaysDeny"},
ConfigFile: "/admission-control-config",
Plugins: s.Admission.Plugins,
......
......@@ -543,6 +543,8 @@ staging/src/k8s.io/apiserver/pkg/admission/configuration
staging/src/k8s.io/apiserver/pkg/admission/plugin/initialization
staging/src/k8s.io/apiserver/pkg/admission/plugin/namespace/lifecycle
staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/config
staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/mutating
staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/testcerts
staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/validating
staging/src/k8s.io/apiserver/pkg/apis/apiserver
staging/src/k8s.io/apiserver/pkg/apis/apiserver/v1alpha1
......
......@@ -419,7 +419,7 @@ function start_apiserver {
fi
# Admission Controllers to invoke prior to persisting objects in cluster
ADMISSION_CONTROL=Initializers,NamespaceLifecycle,LimitRanger,ServiceAccount${security_admission},DefaultStorageClass,DefaultTolerationSeconds,GenericAdmissionWebhook,ResourceQuota
ADMISSION_CONTROL=MutatingAdmissionWebhook,Initializers,NamespaceLifecycle,LimitRanger,ServiceAccount${security_admission},DefaultStorageClass,DefaultTolerationSeconds,GenericAdmissionWebhook,ResourceQuota
# This is the default dir and filename where the apiserver will generate a self-signed cert
# which should be able to be used as the CA to verify itself
......
......@@ -887,6 +887,10 @@
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
},
{
"ImportPath": "k8s.io/apiserver/pkg/admission/plugin/webhook/mutating",
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
},
{
"ImportPath": "k8s.io/apiserver/pkg/admission/plugin/webhook/namespace",
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
},
......
......@@ -80,9 +80,11 @@ filegroup(
"//staging/src/k8s.io/apiserver/pkg/admission/plugin/namespace/lifecycle:all-srcs",
"//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/config:all-srcs",
"//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/errors:all-srcs",
"//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/mutating:all-srcs",
"//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/namespace:all-srcs",
"//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/request:all-srcs",
"//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/rules:all-srcs",
"//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/testcerts:all-srcs",
"//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/validating:all-srcs",
"//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/versioned:all-srcs",
],
......
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
go_library(
name = "go_default_library",
srcs = [
"admission.go",
"doc.go",
],
importpath = "k8s.io/apiserver/pkg/admission/plugin/webhook/mutating",
visibility = ["//visibility:public"],
deps = [
"//vendor/github.com/evanphx/json-patch:go_default_library",
"//vendor/github.com/golang/glog:go_default_library",
"//vendor/k8s.io/api/admission/v1alpha1:go_default_library",
"//vendor/k8s.io/api/admissionregistration/v1alpha1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/api/errors:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime/serializer:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime/serializer/json:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/runtime:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/wait:go_default_library",
"//vendor/k8s.io/apiserver/pkg/admission:go_default_library",
"//vendor/k8s.io/apiserver/pkg/admission/configuration:go_default_library",
"//vendor/k8s.io/apiserver/pkg/admission/initializer:go_default_library",
"//vendor/k8s.io/apiserver/pkg/admission/plugin/webhook/config:go_default_library",
"//vendor/k8s.io/apiserver/pkg/admission/plugin/webhook/errors:go_default_library",
"//vendor/k8s.io/apiserver/pkg/admission/plugin/webhook/namespace:go_default_library",
"//vendor/k8s.io/apiserver/pkg/admission/plugin/webhook/request:go_default_library",
"//vendor/k8s.io/apiserver/pkg/admission/plugin/webhook/rules:go_default_library",
"//vendor/k8s.io/apiserver/pkg/admission/plugin/webhook/versioned:go_default_library",
"//vendor/k8s.io/client-go/informers:go_default_library",
"//vendor/k8s.io/client-go/kubernetes:go_default_library",
],
)
go_test(
name = "go_default_test",
srcs = ["admission_test.go"],
importpath = "k8s.io/apiserver/pkg/admission/plugin/webhook/mutating",
library = ":go_default_library",
deps = [
"//vendor/k8s.io/api/admission/v1alpha1:go_default_library",
"//vendor/k8s.io/api/admissionregistration/v1alpha1:go_default_library",
"//vendor/k8s.io/api/core/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/api/errors:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/labels:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library",
"//vendor/k8s.io/apiserver/pkg/admission:go_default_library",
"//vendor/k8s.io/apiserver/pkg/admission/plugin/webhook/config:go_default_library",
"//vendor/k8s.io/apiserver/pkg/admission/plugin/webhook/testcerts:go_default_library",
"//vendor/k8s.io/apiserver/pkg/authentication/user:go_default_library",
"//vendor/k8s.io/client-go/rest:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)
/*
Copyright 2017 The Kubernetes 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 mutating makes calls to mutating webhooks during the admission
// process.
package mutating // import "k8s.io/apiserver/pkg/admission/plugin/webhook/mutating"
load("@io_bazel_rules_go//go:def.bzl", "go_library")
go_library(
name = "go_default_library",
srcs = [
"certs.go",
"doc.go",
],
importpath = "k8s.io/apiserver/pkg/admission/plugin/webhook/testcerts",
visibility = ["//visibility:public"],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)
/*
Copyright 2017 The Kubernetes 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 testcerts contains generated key pairs used by the unit tests of
// mutating and validating webhooks. They are for testing only.
package testcerts // import "k8s.io/apiserver/pkg/admission/plugin/webhook/testcerts"
......@@ -54,24 +54,24 @@ DNS.1 = webhook-test.default.svc
EOF
# Create a certificate authority
openssl genrsa -out caKey.pem 2048
openssl req -x509 -new -nodes -key caKey.pem -days 100000 -out caCert.pem -subj "/CN=${CN_BASE}_ca"
openssl genrsa -out CAKey.pem 2048
openssl req -x509 -new -nodes -key CAKey.pem -days 100000 -out CACert.pem -subj "/CN=${CN_BASE}_ca"
# Create a second certificate authority
openssl genrsa -out badCAKey.pem 2048
openssl req -x509 -new -nodes -key badCAKey.pem -days 100000 -out badCACert.pem -subj "/CN=${CN_BASE}_ca"
openssl genrsa -out BadCAKey.pem 2048
openssl req -x509 -new -nodes -key BadCAKey.pem -days 100000 -out BadCACert.pem -subj "/CN=${CN_BASE}_ca"
# Create a server certiticate
openssl genrsa -out serverKey.pem 2048
openssl req -new -key serverKey.pem -out server.csr -subj "/CN=webhook-test.default.svc" -config server.conf
openssl x509 -req -in server.csr -CA caCert.pem -CAkey caKey.pem -CAcreateserial -out serverCert.pem -days 100000 -extensions v3_req -extfile server.conf
openssl genrsa -out ServerKey.pem 2048
openssl req -new -key ServerKey.pem -out server.csr -subj "/CN=webhook-test.default.svc" -config server.conf
openssl x509 -req -in server.csr -CA CACert.pem -CAkey CAKey.pem -CAcreateserial -out ServerCert.pem -days 100000 -extensions v3_req -extfile server.conf
# Create a client certiticate
openssl genrsa -out clientKey.pem 2048
openssl req -new -key clientKey.pem -out client.csr -subj "/CN=${CN_BASE}_client" -config client.conf
openssl x509 -req -in client.csr -CA caCert.pem -CAkey caKey.pem -CAcreateserial -out clientCert.pem -days 100000 -extensions v3_req -extfile client.conf
openssl genrsa -out ClientKey.pem 2048
openssl req -new -key ClientKey.pem -out client.csr -subj "/CN=${CN_BASE}_client" -config client.conf
openssl x509 -req -in client.csr -CA CACert.pem -CAkey CAKey.pem -CAcreateserial -out ClientCert.pem -days 100000 -extensions v3_req -extfile client.conf
outfile=certs_test.go
outfile=certs.go
cat > $outfile << EOF
/*
......@@ -95,8 +95,8 @@ EOF
echo "// This file was generated using openssl by the gencerts.sh script" >> $outfile
echo "// and holds raw certificates for the webhook tests." >> $outfile
echo "" >> $outfile
echo "package validating" >> $outfile
for file in caKey caCert badCAKey badCACert serverKey serverCert clientKey clientCert; do
echo "package testcerts" >> $outfile
for file in CAKey CACert BadCAKey BadCACert ServerKey ServerCert ClientKey ClientCert; do
data=$(cat ${file}.pem)
echo "" >> $outfile
echo "var $file = []byte(\`$data\`)" >> $outfile
......
......@@ -34,10 +34,7 @@ go_library(
go_test(
name = "go_default_test",
srcs = [
"admission_test.go",
"certs_test.go",
],
srcs = ["admission_test.go"],
importpath = "k8s.io/apiserver/pkg/admission/plugin/webhook/validating",
library = ":go_default_library",
deps = [
......@@ -50,6 +47,7 @@ go_test(
"//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library",
"//vendor/k8s.io/apiserver/pkg/admission:go_default_library",
"//vendor/k8s.io/apiserver/pkg/admission/plugin/webhook/config:go_default_library",
"//vendor/k8s.io/apiserver/pkg/admission/plugin/webhook/testcerts:go_default_library",
"//vendor/k8s.io/apiserver/pkg/authentication/user:go_default_library",
"//vendor/k8s.io/client-go/rest:go_default_library",
],
......
......@@ -151,16 +151,16 @@ func (a *GenericAdmissionWebhook) SetExternalKubeInformerFactory(f informers.Sha
// ValidateInitialization implements the InitializationValidator interface.
func (a *GenericAdmissionWebhook) ValidateInitialization() error {
if a.hookSource == nil {
return fmt.Errorf("the GenericAdmissionWebhook admission plugin requires a Kubernetes client to be provided")
return fmt.Errorf("GenericAdmissionWebhook admission plugin requires a Kubernetes client to be provided")
}
if err := a.namespaceMatcher.Validate(); err != nil {
return fmt.Errorf("the GenericAdmissionWebhook.namespaceMatcher is not properly setup: %v", err)
return fmt.Errorf("GenericAdmissionWebhook.namespaceMatcher is not properly setup: %v", err)
}
if err := a.clientManager.Validate(); err != nil {
return fmt.Errorf("the GenericAdmissionWebhook.clientManager is not properly setup: %v", err)
return fmt.Errorf("GenericAdmissionWebhook.clientManager is not properly setup: %v", err)
}
if err := a.convertor.Validate(); err != nil {
return fmt.Errorf("the GenericAdmissionWebhook.convertor is not properly setup: %v", err)
return fmt.Errorf("GenericAdmissionWebhook.convertor is not properly setup: %v", err)
}
go a.hookSource.Run(wait.NeverStop)
return nil
......@@ -248,7 +248,6 @@ func (a *GenericAdmissionWebhook) Admit(attr admission.Attributes) error {
if ignoreClientCallFailures {
glog.Warningf("Failed calling webhook, failing open %v: %v", hook.Name, callErr)
utilruntime.HandleError(callErr)
// Since we are failing open to begin with, we do not send an error down the channel
return
}
......@@ -280,6 +279,7 @@ func (a *GenericAdmissionWebhook) Admit(attr admission.Attributes) error {
return errs[0]
}
// TODO: factor into a common place along with the validating webhook version.
func (a *GenericAdmissionWebhook) shouldCallHook(h *v1alpha1.Webhook, attr admission.Attributes) (bool, *apierrors.StatusError) {
var matches bool
for _, r := range h.Rules {
......
......@@ -38,6 +38,7 @@ import (
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/admission/plugin/webhook/config"
"k8s.io/apiserver/pkg/admission/plugin/webhook/testcerts"
"k8s.io/apiserver/pkg/authentication/user"
"k8s.io/client-go/rest"
)
......@@ -96,7 +97,7 @@ func ccfgSVC(urlPath string) registrationv1alpha1.WebhookClientConfig {
Namespace: "default",
Path: &urlPath,
},
CABundle: caCert,
CABundle: testcerts.CACert,
}
}
......@@ -111,7 +112,7 @@ func (c urlConfigGenerator) ccfgURL(urlPath string) registrationv1alpha1.Webhook
urlString := u2.String()
return registrationv1alpha1.WebhookClientConfig{
URL: &urlString,
CABundle: caCert,
CABundle: testcerts.CACert,
}
}
......@@ -578,12 +579,12 @@ func TestAdmitCachedClient(t *testing.T) {
func newTestServer(t *testing.T) *httptest.Server {
// Create the test webhook server
sCert, err := tls.X509KeyPair(serverCert, serverKey)
sCert, err := tls.X509KeyPair(testcerts.ServerCert, testcerts.ServerKey)
if err != nil {
t.Fatal(err)
}
rootCAs := x509.NewCertPool()
rootCAs.AppendCertsFromPEM(caCert)
rootCAs.AppendCertsFromPEM(testcerts.CACert)
testServer := httptest.NewUnstartedServer(http.HandlerFunc(webhookHandler))
testServer.TLS = &tls.Config{
Certificates: []tls.Certificate{sCert},
......@@ -642,9 +643,9 @@ func newFakeAuthenticationInfoResolver(count *int32) *fakeAuthenticationInfoReso
return &fakeAuthenticationInfoResolver{
restConfig: &rest.Config{
TLSClientConfig: rest.TLSClientConfig{
CAData: caCert,
CertData: clientCert,
KeyData: clientKey,
CAData: testcerts.CACert,
CertData: testcerts.ClientCert,
KeyData: testcerts.ClientKey,
},
},
cachedCount: count,
......
......@@ -14,5 +14,6 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
// Package validating checks a non-mutating webhook for configured operation admission
// Package validating makes calls to validating (i.e., non-mutating) webhooks
// during the admission process.
package validating // import "k8s.io/apiserver/pkg/admission/plugin/webhook/validating"
......@@ -28,6 +28,18 @@ type Convertor struct {
Scheme *runtime.Scheme
}
// Convert converts the in object to the out object and returns an error if the
// conversion fails.
func (c Convertor) Convert(in runtime.Object, out runtime.Object) error {
// For custom resources, because ConvertToGVK reuses the passed in object as
// the output. c.Scheme.Convert resets the objects to empty if in == out, so
// we skip the conversion if that's the case.
if in == out {
return nil
}
return c.Scheme.Convert(in, out, nil)
}
// ConvertToGVK converts object to the desired gvk.
func (c Convertor) ConvertToGVK(obj runtime.Object, gvk schema.GroupVersionKind) (runtime.Object, error) {
// Unlike other resources, custom resources do not have internal version, so
......
......@@ -130,3 +130,90 @@ func TestConvertToGVK(t *testing.T) {
})
}
}
func TestConvert(t *testing.T) {
scheme := initiateScheme()
c := Convertor{Scheme: scheme}
sampleCRD := unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "mygroup.k8s.io/v1",
"kind": "Flunder",
"data": map[string]interface{}{
"Key": "Value",
},
},
}
table := map[string]struct {
in runtime.Object
out runtime.Object
expectedObj runtime.Object
}{
"convert example/v1#Pod to example#Pod": {
in: &examplev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "pod1",
Labels: map[string]string{
"key": "value",
},
},
Spec: examplev1.PodSpec{
RestartPolicy: examplev1.RestartPolicy("never"),
},
},
out: &example.Pod{},
expectedObj: &example.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "pod1",
Labels: map[string]string{
"key": "value",
},
},
Spec: example.PodSpec{
RestartPolicy: example.RestartPolicy("never"),
},
},
},
"convert example2/v1#replicaset to example#replicaset": {
in: &example2v1.ReplicaSet{
ObjectMeta: metav1.ObjectMeta{
Name: "rs1",
Labels: map[string]string{
"key": "value",
},
},
Spec: example2v1.ReplicaSetSpec{
Replicas: func() *int32 { var i int32; i = 1; return &i }(),
},
},
out: &example.ReplicaSet{},
expectedObj: &example.ReplicaSet{
ObjectMeta: metav1.ObjectMeta{
Name: "rs1",
Labels: map[string]string{
"key": "value",
},
},
Spec: example.ReplicaSetSpec{
Replicas: 1,
},
},
},
"no conversion if the object is the same": {
in: &sampleCRD,
out: &sampleCRD,
expectedObj: &sampleCRD,
},
}
for name, test := range table {
t.Run(name, func(t *testing.T) {
err := c.Convert(test.in, test.out)
if err != nil {
t.Error(err)
}
if !reflect.DeepEqual(test.out, test.expectedObj) {
t.Errorf("\nexpected:\n%#v\ngot:\n %#v\n", test.expectedObj, test.out)
}
})
}
}
......@@ -88,6 +88,7 @@ go_library(
"//vendor/k8s.io/apiserver/pkg/admission:go_default_library",
"//vendor/k8s.io/apiserver/pkg/admission/plugin/initialization:go_default_library",
"//vendor/k8s.io/apiserver/pkg/admission/plugin/namespace/lifecycle:go_default_library",
"//vendor/k8s.io/apiserver/pkg/admission/plugin/webhook/mutating:go_default_library",
"//vendor/k8s.io/apiserver/pkg/admission/plugin/webhook/validating:go_default_library",
"//vendor/k8s.io/apiserver/pkg/apis/apiserver/install:go_default_library",
"//vendor/k8s.io/apiserver/pkg/audit:go_default_library",
......
......@@ -32,6 +32,7 @@ go_library(
"//vendor/k8s.io/apiserver/pkg/admission/initializer:go_default_library",
"//vendor/k8s.io/apiserver/pkg/admission/plugin/initialization:go_default_library",
"//vendor/k8s.io/apiserver/pkg/admission/plugin/namespace/lifecycle:go_default_library",
"//vendor/k8s.io/apiserver/pkg/admission/plugin/webhook/mutating:go_default_library",
"//vendor/k8s.io/apiserver/pkg/admission/plugin/webhook/validating:go_default_library",
"//vendor/k8s.io/apiserver/pkg/apis/audit/v1beta1:go_default_library",
"//vendor/k8s.io/apiserver/pkg/audit:go_default_library",
......
......@@ -26,6 +26,7 @@ import (
"k8s.io/apiserver/pkg/admission/initializer"
"k8s.io/apiserver/pkg/admission/plugin/initialization"
"k8s.io/apiserver/pkg/admission/plugin/namespace/lifecycle"
mutatingwebhook "k8s.io/apiserver/pkg/admission/plugin/webhook/mutating"
validatingwebhook "k8s.io/apiserver/pkg/admission/plugin/webhook/validating"
"k8s.io/apiserver/pkg/server"
"k8s.io/client-go/informers"
......@@ -56,8 +57,8 @@ func NewAdmissionOptions() *AdmissionOptions {
options := &AdmissionOptions{
Plugins: &admission.Plugins{},
PluginNames: []string{},
RecommendedPluginOrder: []string{lifecycle.PluginName, initialization.PluginName, validatingwebhook.PluginName},
DefaultOffPlugins: []string{initialization.PluginName, validatingwebhook.PluginName},
RecommendedPluginOrder: []string{mutatingwebhook.PluginName, lifecycle.PluginName, initialization.PluginName, validatingwebhook.PluginName},
DefaultOffPlugins: []string{mutatingwebhook.PluginName, initialization.PluginName, validatingwebhook.PluginName},
}
server.RegisterAllAdmissionPlugins(options.Plugins)
return options
......
......@@ -21,6 +21,7 @@ import (
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/admission/plugin/initialization"
"k8s.io/apiserver/pkg/admission/plugin/namespace/lifecycle"
mutatingwebhook "k8s.io/apiserver/pkg/admission/plugin/webhook/mutating"
validatingwebhook "k8s.io/apiserver/pkg/admission/plugin/webhook/validating"
)
......@@ -29,4 +30,5 @@ func RegisterAllAdmissionPlugins(plugins *admission.Plugins) {
lifecycle.Register(plugins)
initialization.Register(plugins)
validatingwebhook.Register(plugins)
mutatingwebhook.Register(plugins)
}
......@@ -855,6 +855,10 @@
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
},
{
"ImportPath": "k8s.io/apiserver/pkg/admission/plugin/webhook/mutating",
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
},
{
"ImportPath": "k8s.io/apiserver/pkg/admission/plugin/webhook/namespace",
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
},
......
......@@ -851,6 +851,10 @@
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
},
{
"ImportPath": "k8s.io/apiserver/pkg/admission/plugin/webhook/mutating",
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
},
{
"ImportPath": "k8s.io/apiserver/pkg/admission/plugin/webhook/namespace",
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
},
......
load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library", "go_test")
go_library(
name = "go_default_library",
......@@ -42,3 +42,15 @@ filegroup(
tags = ["automanaged"],
visibility = ["//visibility:public"],
)
go_test(
name = "go_default_test",
srcs = ["patch_test.go"],
importpath = "k8s.io/kubernetes/test/images/webhook",
library = ":go_default_library",
deps = [
"//vendor/github.com/evanphx/json-patch:go_default_library",
"//vendor/k8s.io/api/core/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured:go_default_library",
],
)
......@@ -14,7 +14,7 @@
build:
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.8v5 .
docker build --no-cache -t gcr.io/kubernetes-e2e-test-images/k8s-sample-admission-webhook-amd64:1.8v6 .
rm -rf webhook
push:
gcloud docker -- push gcr.io/kubernetes-e2e-test-images/k8s-sample-admission-webhook-amd64:1.8v5
gcloud docker -- push gcr.io/kubernetes-e2e-test-images/k8s-sample-admission-webhook-amd64:1.8v6
......@@ -28,6 +28,18 @@ import (
"k8s.io/api/admission/v1alpha1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
// TODO: try this library to see if it generates correct json patch
// https://github.com/mattbaird/jsonpatch
)
const (
patch1 string = `[
{ "op": "add", "path": "/data/mutation-stage-1", "value": "yes" }
]`
patch2 string = `[
{ "op": "add", "path": "/data/mutation-stage-2", "value": "yes" }
]`
)
// Config contains the server (the webhook) cert and key.
......@@ -120,6 +132,64 @@ func admitConfigMaps(ar v1alpha1.AdmissionReview) *v1alpha1.AdmissionResponse {
return &reviewResponse
}
func mutateConfigmaps(ar v1alpha1.AdmissionReview) *v1alpha1.AdmissionResponse {
glog.V(2).Info("mutating configmaps")
configMapResource := metav1.GroupVersionResource{Group: "", Version: "v1", Resource: "configmaps"}
if ar.Request.Resource != configMapResource {
glog.Errorf("expect resource to be %s", configMapResource)
return nil
}
raw := ar.Request.Object.Raw
configmap := corev1.ConfigMap{}
deserializer := codecs.UniversalDeserializer()
if _, _, err := deserializer.Decode(raw, nil, &configmap); err != nil {
glog.Error(err)
return toAdmissionResponse(err)
}
reviewResponse := v1alpha1.AdmissionResponse{}
reviewResponse.Allowed = true
if configmap.Data["mutation-start"] == "yes" {
reviewResponse.Patch = []byte(patch1)
}
if configmap.Data["mutation-stage-1"] == "yes" {
reviewResponse.Patch = []byte(patch2)
}
pt := v1alpha1.PatchTypeJSONPatch
reviewResponse.PatchType = &pt
return &reviewResponse
}
func mutateCRD(ar v1alpha1.AdmissionReview) *v1alpha1.AdmissionResponse {
glog.V(2).Info("mutating crd")
cr := struct {
metav1.ObjectMeta
Data map[string]string
}{}
raw := ar.Request.Object.Raw
err := json.Unmarshal(raw, &cr)
if err != nil {
glog.Error(err)
return toAdmissionResponse(err)
}
reviewResponse := v1alpha1.AdmissionResponse{}
reviewResponse.Allowed = true
if cr.Data["mutation-start"] == "yes" {
reviewResponse.Patch = []byte(patch1)
}
if cr.Data["mutation-stage-1"] == "yes" {
reviewResponse.Patch = []byte(patch2)
}
pt := v1alpha1.PatchTypeJSONPatch
reviewResponse.PatchType = &pt
return &reviewResponse
}
func admitCRD(ar v1alpha1.AdmissionReview) *v1alpha1.AdmissionResponse {
glog.V(2).Info("admitting crd")
cr := struct {
......@@ -179,6 +249,9 @@ func serve(w http.ResponseWriter, r *http.Request, admit admitFunc) {
response.Response = reviewResponse
response.Response.UID = ar.Request.UID
}
// reset the Object and OldObject, they are not needed in a response.
ar.Request.Object = runtime.RawExtension{}
ar.Request.OldObject = runtime.RawExtension{}
resp, err := json.Marshal(response)
if err != nil {
......@@ -197,10 +270,18 @@ func serveConfigmaps(w http.ResponseWriter, r *http.Request) {
serve(w, r, admitConfigMaps)
}
func serveMutateConfigmaps(w http.ResponseWriter, r *http.Request) {
serve(w, r, mutateConfigmaps)
}
func serveCRD(w http.ResponseWriter, r *http.Request) {
serve(w, r, admitCRD)
}
func serveMutateCRD(w http.ResponseWriter, r *http.Request) {
serve(w, r, mutateCRD)
}
func main() {
var config Config
config.addFlags()
......@@ -208,7 +289,9 @@ func main() {
http.HandleFunc("/pods", servePods)
http.HandleFunc("/configmaps", serveConfigmaps)
http.HandleFunc("/mutating-configmaps", serveMutateConfigmaps)
http.HandleFunc("/crd", serveCRD)
http.HandleFunc("/mutating-crd", serveMutateCRD)
clientset := getClient()
server := &http.Server{
Addr: ":443",
......
/*
Copyright 2017 The Kubernetes 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 main
import (
"encoding/json"
"reflect"
"testing"
jsonpatch "github.com/evanphx/json-patch"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)
func TestJSONPatchForConfigMap(t *testing.T) {
cm := corev1.ConfigMap{
Data: map[string]string{
"mutation-start": "yes",
},
}
cmJS, err := json.Marshal(cm)
if err != nil {
t.Fatal(err)
}
patchObj, err := jsonpatch.DecodePatch([]byte(patch1))
if err != nil {
t.Fatal(err)
}
patchedJS, err := patchObj.Apply(cmJS)
patchedObj := corev1.ConfigMap{}
err = json.Unmarshal(patchedJS, &patchedObj)
if err != nil {
t.Fatal(err)
}
expected := corev1.ConfigMap{
Data: map[string]string{
"mutation-start": "yes",
"mutation-stage-1": "yes",
},
}
if !reflect.DeepEqual(patchedObj, expected) {
t.Errorf("\nexpected %#v\n, got %#v", expected, patchedObj)
}
}
func TestJSONPatchForUnstructured(t *testing.T) {
cr := &unstructured.Unstructured{
Object: map[string]interface{}{
"kind": "Something",
"apiVersion": "somegroup/v1",
"data": map[string]interface{}{
"mutation-start": "yes",
},
},
}
crJS, err := json.Marshal(cr)
if err != nil {
t.Fatal(err)
}
patchObj, err := jsonpatch.DecodePatch([]byte(patch1))
if err != nil {
t.Fatal(err)
}
patchedJS, err := patchObj.Apply(crJS)
patchedObj := unstructured.Unstructured{}
err = json.Unmarshal(patchedJS, &patchedObj)
if err != nil {
t.Fatal(err)
}
expectedData := map[string]interface{}{
"mutation-start": "yes",
"mutation-stage-1": "yes",
}
if !reflect.DeepEqual(patchedObj.Object["data"], expectedData) {
t.Errorf("\nexpected %#v\n, got %#v", expectedData, patchedObj.Object["data"])
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment