Commit ea123f82 authored by Chao Xu's avatar Chao Xu

Adding the mutating webhook

parent 2aaab817
...@@ -316,7 +316,7 @@ if [[ -n "${GCE_GLBC_IMAGE:-}" ]]; then ...@@ -316,7 +316,7 @@ if [[ -n "${GCE_GLBC_IMAGE:-}" ]]; then
fi fi
if [[ -z "${KUBE_ADMISSION_CONTROL:-}" ]]; then 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 if [[ "${ENABLE_POD_SECURITY_POLICY:-}" == "true" ]]; then
ADMISSION_CONTROL="${ADMISSION_CONTROL},PodSecurityPolicy" ADMISSION_CONTROL="${ADMISSION_CONTROL},PodSecurityPolicy"
fi fi
......
...@@ -104,8 +104,8 @@ func TestAddFlags(t *testing.T) { ...@@ -104,8 +104,8 @@ func TestAddFlags(t *testing.T) {
MinRequestTimeout: 1800, MinRequestTimeout: 1800,
}, },
Admission: &apiserveroptions.AdmissionOptions{ Admission: &apiserveroptions.AdmissionOptions{
RecommendedPluginOrder: []string{"NamespaceLifecycle", "Initializers", "GenericAdmissionWebhook"}, RecommendedPluginOrder: []string{"MutatingAdmissionWebhook", "NamespaceLifecycle", "Initializers", "GenericAdmissionWebhook"},
DefaultOffPlugins: []string{"Initializers", "GenericAdmissionWebhook"}, DefaultOffPlugins: []string{"MutatingAdmissionWebhook", "Initializers", "GenericAdmissionWebhook"},
PluginNames: []string{"AlwaysDeny"}, PluginNames: []string{"AlwaysDeny"},
ConfigFile: "/admission-control-config", ConfigFile: "/admission-control-config",
Plugins: s.Admission.Plugins, Plugins: s.Admission.Plugins,
......
...@@ -419,7 +419,7 @@ function start_apiserver { ...@@ -419,7 +419,7 @@ function start_apiserver {
fi fi
# Admission Controllers to invoke prior to persisting objects in cluster # 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 # 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 # which should be able to be used as the CA to verify itself
......
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/validating",
visibility = ["//visibility:public"],
deps = [
"//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/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",
"certs_test.go",
],
importpath = "k8s.io/apiserver/pkg/admission/plugin/webhook/validating",
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/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"
/*
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 testdata contains generated key pairs used by the unit tests of
// mutating and validating webhooks. They are for testing only.
package testdata // import "k8s.io/apiserver/pkg/admission/plugin/webhook/testdata"
...@@ -54,24 +54,24 @@ DNS.1 = webhook-test.default.svc ...@@ -54,24 +54,24 @@ DNS.1 = webhook-test.default.svc
EOF EOF
# Create a certificate authority # Create a certificate authority
openssl genrsa -out caKey.pem 2048 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 req -x509 -new -nodes -key CAKey.pem -days 100000 -out CACert.pem -subj "/CN=${CN_BASE}_ca"
# Create a second certificate authority # Create a second certificate authority
openssl genrsa -out badCAKey.pem 2048 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 req -x509 -new -nodes -key BadCAKey.pem -days 100000 -out BadCACert.pem -subj "/CN=${CN_BASE}_ca"
# Create a server certiticate # Create a server certiticate
openssl genrsa -out serverKey.pem 2048 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 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 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 # Create a client certiticate
openssl genrsa -out clientKey.pem 2048 openssl genrsa -out ClientKey.pem 2048
openssl req -new -key clientKey.pem -out client.csr -subj "/CN=${CN_BASE}_client" -config client.conf 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 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 cat > $outfile << EOF
/* /*
...@@ -95,8 +95,8 @@ EOF ...@@ -95,8 +95,8 @@ EOF
echo "// This file was generated using openssl by the gencerts.sh script" >> $outfile echo "// This file was generated using openssl by the gencerts.sh script" >> $outfile
echo "// and holds raw certificates for the webhook tests." >> $outfile echo "// and holds raw certificates for the webhook tests." >> $outfile
echo "" >> $outfile echo "" >> $outfile
echo "package validating" >> $outfile echo "package testdata" >> $outfile
for file in caKey caCert badCAKey badCACert serverKey serverCert clientKey clientCert; do for file in CAKey CACert BadCAKey BadCACert ServerKey ServerCert ClientKey ClientCert; do
data=$(cat ${file}.pem) data=$(cat ${file}.pem)
echo "" >> $outfile echo "" >> $outfile
echo "var $file = []byte(\`$data\`)" >> $outfile echo "var $file = []byte(\`$data\`)" >> $outfile
......
...@@ -151,16 +151,16 @@ func (a *GenericAdmissionWebhook) SetExternalKubeInformerFactory(f informers.Sha ...@@ -151,16 +151,16 @@ func (a *GenericAdmissionWebhook) SetExternalKubeInformerFactory(f informers.Sha
// ValidateInitialization implements the InitializationValidator interface. // ValidateInitialization implements the InitializationValidator interface.
func (a *GenericAdmissionWebhook) ValidateInitialization() error { func (a *GenericAdmissionWebhook) ValidateInitialization() error {
if a.hookSource == nil { 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 { 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 { 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 { 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) go a.hookSource.Run(wait.NeverStop)
return nil return nil
...@@ -248,7 +248,6 @@ func (a *GenericAdmissionWebhook) Admit(attr admission.Attributes) error { ...@@ -248,7 +248,6 @@ func (a *GenericAdmissionWebhook) Admit(attr admission.Attributes) error {
if ignoreClientCallFailures { if ignoreClientCallFailures {
glog.Warningf("Failed calling webhook, failing open %v: %v", hook.Name, callErr) glog.Warningf("Failed calling webhook, failing open %v: %v", hook.Name, callErr)
utilruntime.HandleError(callErr) utilruntime.HandleError(callErr)
// Since we are failing open to begin with, we do not send an error down the channel
return return
} }
...@@ -280,6 +279,7 @@ func (a *GenericAdmissionWebhook) Admit(attr admission.Attributes) error { ...@@ -280,6 +279,7 @@ func (a *GenericAdmissionWebhook) Admit(attr admission.Attributes) error {
return errs[0] 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) { func (a *GenericAdmissionWebhook) shouldCallHook(h *v1alpha1.Webhook, attr admission.Attributes) (bool, *apierrors.StatusError) {
var matches bool var matches bool
for _, r := range h.Rules { for _, r := range h.Rules {
......
...@@ -38,6 +38,7 @@ import ( ...@@ -38,6 +38,7 @@ import (
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apiserver/pkg/admission" "k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/admission/plugin/webhook/config" "k8s.io/apiserver/pkg/admission/plugin/webhook/config"
"k8s.io/apiserver/pkg/admission/plugin/webhook/testdata"
"k8s.io/apiserver/pkg/authentication/user" "k8s.io/apiserver/pkg/authentication/user"
"k8s.io/client-go/rest" "k8s.io/client-go/rest"
) )
...@@ -96,7 +97,7 @@ func ccfgSVC(urlPath string) registrationv1alpha1.WebhookClientConfig { ...@@ -96,7 +97,7 @@ func ccfgSVC(urlPath string) registrationv1alpha1.WebhookClientConfig {
Namespace: "default", Namespace: "default",
Path: &urlPath, Path: &urlPath,
}, },
CABundle: caCert, CABundle: testdata.CACert,
} }
} }
...@@ -111,7 +112,7 @@ func (c urlConfigGenerator) ccfgURL(urlPath string) registrationv1alpha1.Webhook ...@@ -111,7 +112,7 @@ func (c urlConfigGenerator) ccfgURL(urlPath string) registrationv1alpha1.Webhook
urlString := u2.String() urlString := u2.String()
return registrationv1alpha1.WebhookClientConfig{ return registrationv1alpha1.WebhookClientConfig{
URL: &urlString, URL: &urlString,
CABundle: caCert, CABundle: testdata.CACert,
} }
} }
...@@ -578,12 +579,12 @@ func TestAdmitCachedClient(t *testing.T) { ...@@ -578,12 +579,12 @@ func TestAdmitCachedClient(t *testing.T) {
func newTestServer(t *testing.T) *httptest.Server { func newTestServer(t *testing.T) *httptest.Server {
// Create the test webhook server // Create the test webhook server
sCert, err := tls.X509KeyPair(serverCert, serverKey) sCert, err := tls.X509KeyPair(testdata.ServerCert, testdata.ServerKey)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
rootCAs := x509.NewCertPool() rootCAs := x509.NewCertPool()
rootCAs.AppendCertsFromPEM(caCert) rootCAs.AppendCertsFromPEM(testdata.CACert)
testServer := httptest.NewUnstartedServer(http.HandlerFunc(webhookHandler)) testServer := httptest.NewUnstartedServer(http.HandlerFunc(webhookHandler))
testServer.TLS = &tls.Config{ testServer.TLS = &tls.Config{
Certificates: []tls.Certificate{sCert}, Certificates: []tls.Certificate{sCert},
...@@ -642,9 +643,9 @@ func newFakeAuthenticationInfoResolver(count *int32) *fakeAuthenticationInfoReso ...@@ -642,9 +643,9 @@ func newFakeAuthenticationInfoResolver(count *int32) *fakeAuthenticationInfoReso
return &fakeAuthenticationInfoResolver{ return &fakeAuthenticationInfoResolver{
restConfig: &rest.Config{ restConfig: &rest.Config{
TLSClientConfig: rest.TLSClientConfig{ TLSClientConfig: rest.TLSClientConfig{
CAData: caCert, CAData: testdata.CACert,
CertData: clientCert, CertData: testdata.ClientCert,
KeyData: clientKey, KeyData: testdata.ClientKey,
}, },
}, },
cachedCount: count, cachedCount: count,
......
...@@ -14,5 +14,6 @@ See the License for the specific language governing permissions and ...@@ -14,5 +14,6 @@ See the License for the specific language governing permissions and
limitations under the License. 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" package validating // import "k8s.io/apiserver/pkg/admission/plugin/webhook/validating"
...@@ -28,6 +28,18 @@ type Convertor struct { ...@@ -28,6 +28,18 @@ type Convertor struct {
Scheme *runtime.Scheme 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. // ConvertToGVK converts object to the desired gvk.
func (c Convertor) ConvertToGVK(obj runtime.Object, gvk schema.GroupVersionKind) (runtime.Object, error) { func (c Convertor) ConvertToGVK(obj runtime.Object, gvk schema.GroupVersionKind) (runtime.Object, error) {
// Unlike other resources, custom resources do not have internal version, so // Unlike other resources, custom resources do not have internal version, so
......
...@@ -130,3 +130,90 @@ func TestConvertToGVK(t *testing.T) { ...@@ -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)
}
})
}
}
...@@ -26,6 +26,7 @@ import ( ...@@ -26,6 +26,7 @@ import (
"k8s.io/apiserver/pkg/admission/initializer" "k8s.io/apiserver/pkg/admission/initializer"
"k8s.io/apiserver/pkg/admission/plugin/initialization" "k8s.io/apiserver/pkg/admission/plugin/initialization"
"k8s.io/apiserver/pkg/admission/plugin/namespace/lifecycle" "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" validatingwebhook "k8s.io/apiserver/pkg/admission/plugin/webhook/validating"
"k8s.io/apiserver/pkg/server" "k8s.io/apiserver/pkg/server"
"k8s.io/client-go/informers" "k8s.io/client-go/informers"
...@@ -56,8 +57,8 @@ func NewAdmissionOptions() *AdmissionOptions { ...@@ -56,8 +57,8 @@ func NewAdmissionOptions() *AdmissionOptions {
options := &AdmissionOptions{ options := &AdmissionOptions{
Plugins: &admission.Plugins{}, Plugins: &admission.Plugins{},
PluginNames: []string{}, PluginNames: []string{},
RecommendedPluginOrder: []string{lifecycle.PluginName, initialization.PluginName, validatingwebhook.PluginName}, RecommendedPluginOrder: []string{mutatingwebhook.PluginName, lifecycle.PluginName, initialization.PluginName, validatingwebhook.PluginName},
DefaultOffPlugins: []string{initialization.PluginName, validatingwebhook.PluginName}, DefaultOffPlugins: []string{mutatingwebhook.PluginName, initialization.PluginName, validatingwebhook.PluginName},
} }
server.RegisterAllAdmissionPlugins(options.Plugins) server.RegisterAllAdmissionPlugins(options.Plugins)
return options return options
......
...@@ -21,6 +21,7 @@ import ( ...@@ -21,6 +21,7 @@ import (
"k8s.io/apiserver/pkg/admission" "k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/admission/plugin/initialization" "k8s.io/apiserver/pkg/admission/plugin/initialization"
"k8s.io/apiserver/pkg/admission/plugin/namespace/lifecycle" "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" validatingwebhook "k8s.io/apiserver/pkg/admission/plugin/webhook/validating"
) )
...@@ -29,4 +30,5 @@ func RegisterAllAdmissionPlugins(plugins *admission.Plugins) { ...@@ -29,4 +30,5 @@ func RegisterAllAdmissionPlugins(plugins *admission.Plugins) {
lifecycle.Register(plugins) lifecycle.Register(plugins)
initialization.Register(plugins) initialization.Register(plugins)
validatingwebhook.Register(plugins) validatingwebhook.Register(plugins)
mutatingwebhook.Register(plugins)
} }
...@@ -14,7 +14,7 @@ ...@@ -14,7 +14,7 @@
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.8v5 . docker build --no-cache -t gcr.io/kubernetes-e2e-test-images/k8s-sample-admission-webhook-amd64:1.8v6 .
rm -rf webhook rm -rf webhook
push: 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 ( ...@@ -28,6 +28,18 @@ import (
"k8s.io/api/admission/v1alpha1" "k8s.io/api/admission/v1alpha1"
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/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. // Config contains the server (the webhook) cert and key.
...@@ -120,6 +132,64 @@ func admitConfigMaps(ar v1alpha1.AdmissionReview) *v1alpha1.AdmissionResponse { ...@@ -120,6 +132,64 @@ func admitConfigMaps(ar v1alpha1.AdmissionReview) *v1alpha1.AdmissionResponse {
return &reviewResponse 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 { func admitCRD(ar v1alpha1.AdmissionReview) *v1alpha1.AdmissionResponse {
glog.V(2).Info("admitting crd") glog.V(2).Info("admitting crd")
cr := struct { cr := struct {
...@@ -179,6 +249,9 @@ func serve(w http.ResponseWriter, r *http.Request, admit admitFunc) { ...@@ -179,6 +249,9 @@ func serve(w http.ResponseWriter, r *http.Request, admit admitFunc) {
response.Response = reviewResponse response.Response = reviewResponse
response.Response.UID = ar.Request.UID 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) resp, err := json.Marshal(response)
if err != nil { if err != nil {
...@@ -197,10 +270,18 @@ func serveConfigmaps(w http.ResponseWriter, r *http.Request) { ...@@ -197,10 +270,18 @@ func serveConfigmaps(w http.ResponseWriter, r *http.Request) {
serve(w, r, admitConfigMaps) serve(w, r, admitConfigMaps)
} }
func serveMutateConfigmaps(w http.ResponseWriter, r *http.Request) {
serve(w, r, mutateConfigmaps)
}
func serveCRD(w http.ResponseWriter, r *http.Request) { func serveCRD(w http.ResponseWriter, r *http.Request) {
serve(w, r, admitCRD) serve(w, r, admitCRD)
} }
func serveMutateCRD(w http.ResponseWriter, r *http.Request) {
serve(w, r, mutateCRD)
}
func main() { func main() {
var config Config var config Config
config.addFlags() config.addFlags()
...@@ -208,7 +289,9 @@ func main() { ...@@ -208,7 +289,9 @@ func main() {
http.HandleFunc("/pods", servePods) http.HandleFunc("/pods", servePods)
http.HandleFunc("/configmaps", serveConfigmaps) http.HandleFunc("/configmaps", serveConfigmaps)
http.HandleFunc("/mutating-configmaps", serveMutateConfigmaps)
http.HandleFunc("/crd", serveCRD) http.HandleFunc("/crd", serveCRD)
http.HandleFunc("/mutating-crd", serveMutateCRD)
clientset := getClient() clientset := getClient()
server := &http.Server{ server := &http.Server{
Addr: ":443", 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