mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-23 03:41:45 +00:00
Merge pull request #66066 from jasonrichardsmith/cleanup-e2e-tests
Automatic merge from submit-queue (batch tested with PRs 66877, 66066, 66851). 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>. Cleanup the Admission Webhook E2E code **What this PR does / why we need it**: The current documentation for Admission Webhooks points to the e2e tests as an example. The code appears to have some legacy configuration left behind. The variables are confusingly named and it lacked comments developers could follow. This is an attempt to clean up some code and make the code more readable to make webhook development easier. **Special notes for your reviewer**: **Release note**: ```release-note None ```
This commit is contained in:
commit
56679f4e85
@ -3,8 +3,13 @@ load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library", "go_test")
|
||||
go_library(
|
||||
name = "go_default_library",
|
||||
srcs = [
|
||||
"alwaysdeny.go",
|
||||
"config.go",
|
||||
"configmap.go",
|
||||
"crd.go",
|
||||
"customresource.go",
|
||||
"main.go",
|
||||
"pods.go",
|
||||
"scheme.go",
|
||||
],
|
||||
importpath = "k8s.io/kubernetes/test/images/webhook",
|
||||
@ -18,8 +23,6 @@ go_library(
|
||||
"//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/runtime/serializer:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/util/runtime:go_default_library",
|
||||
"//staging/src/k8s.io/client-go/kubernetes:go_default_library",
|
||||
"//staging/src/k8s.io/client-go/rest:go_default_library",
|
||||
"//vendor/github.com/golang/glog:go_default_library",
|
||||
],
|
||||
)
|
||||
|
@ -1 +1 @@
|
||||
1.10v2
|
||||
1.12v1
|
||||
|
32
test/images/webhook/alwaysdeny.go
Normal file
32
test/images/webhook/alwaysdeny.go
Normal file
@ -0,0 +1,32 @@
|
||||
/*
|
||||
Copyright 2018 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 (
|
||||
"github.com/golang/glog"
|
||||
"k8s.io/api/admission/v1beta1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
// alwaysDeny 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
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2017 The Kubernetes Authors.
|
||||
Copyright 2018 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.
|
||||
@ -18,27 +18,26 @@ package main
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/rest"
|
||||
"flag"
|
||||
|
||||
"github.com/golang/glog"
|
||||
)
|
||||
|
||||
// Get a clientset with in-cluster config.
|
||||
func getClient() *kubernetes.Clientset {
|
||||
config, err := rest.InClusterConfig()
|
||||
if err != nil {
|
||||
glog.Fatal(err)
|
||||
}
|
||||
clientset, err := kubernetes.NewForConfig(config)
|
||||
if err != nil {
|
||||
glog.Fatal(err)
|
||||
}
|
||||
return clientset
|
||||
// Config contains the server (the webhook) cert and key.
|
||||
type Config struct {
|
||||
CertFile string
|
||||
KeyFile string
|
||||
}
|
||||
|
||||
func configTLS(config Config, clientset *kubernetes.Clientset) *tls.Config {
|
||||
func (c *Config) addFlags() {
|
||||
flag.StringVar(&c.CertFile, "tls-cert-file", c.CertFile, ""+
|
||||
"File containing the default x509 Certificate for HTTPS. (CA cert, if any, concatenated "+
|
||||
"after server cert).")
|
||||
flag.StringVar(&c.KeyFile, "tls-private-key-file", c.KeyFile, ""+
|
||||
"File containing the default x509 private key matching --tls-cert-file.")
|
||||
}
|
||||
|
||||
func configTLS(config Config) *tls.Config {
|
||||
sCert, err := tls.LoadX509KeyPair(config.CertFile, config.KeyFile)
|
||||
if err != nil {
|
||||
glog.Fatal(err)
|
||||
|
92
test/images/webhook/configmap.go
Normal file
92
test/images/webhook/configmap.go
Normal file
@ -0,0 +1,92 @@
|
||||
/*
|
||||
Copyright 2018 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 (
|
||||
"github.com/golang/glog"
|
||||
"k8s.io/api/admission/v1beta1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
const (
|
||||
configMapPatch1 string = `[
|
||||
{ "op": "add", "path": "/data/mutation-stage-1", "value": "yes" }
|
||||
]`
|
||||
configMapPatch2 string = `[
|
||||
{ "op": "add", "path": "/data/mutation-stage-2", "value": "yes" }
|
||||
]`
|
||||
)
|
||||
|
||||
// deny configmaps with specific key-value pair.
|
||||
func admitConfigMaps(ar v1beta1.AdmissionReview) *v1beta1.AdmissionResponse {
|
||||
glog.V(2).Info("admitting 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 := v1beta1.AdmissionResponse{}
|
||||
reviewResponse.Allowed = true
|
||||
for k, v := range configmap.Data {
|
||||
if k == "webhook-e2e-test" && v == "webhook-disallow" {
|
||||
reviewResponse.Allowed = false
|
||||
reviewResponse.Result = &metav1.Status{
|
||||
Reason: "the configmap contains unwanted key and value",
|
||||
}
|
||||
}
|
||||
}
|
||||
return &reviewResponse
|
||||
}
|
||||
|
||||
func mutateConfigmaps(ar v1beta1.AdmissionReview) *v1beta1.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 := v1beta1.AdmissionResponse{}
|
||||
reviewResponse.Allowed = true
|
||||
if configmap.Data["mutation-start"] == "yes" {
|
||||
reviewResponse.Patch = []byte(configMapPatch1)
|
||||
}
|
||||
if configmap.Data["mutation-stage-1"] == "yes" {
|
||||
reviewResponse.Patch = []byte(configMapPatch2)
|
||||
}
|
||||
|
||||
pt := v1beta1.PatchTypeJSONPatch
|
||||
reviewResponse.PatchType = &pt
|
||||
|
||||
return &reviewResponse
|
||||
}
|
57
test/images/webhook/crd.go
Normal file
57
test/images/webhook/crd.go
Normal file
@ -0,0 +1,57 @@
|
||||
/*
|
||||
Copyright 2018 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 (
|
||||
"fmt"
|
||||
|
||||
apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
"github.com/golang/glog"
|
||||
"k8s.io/api/admission/v1beta1"
|
||||
)
|
||||
|
||||
// This function expects all CRDs submitted to it to be apiextensions.k8s.io/v1beta1
|
||||
// TODO: When apiextensions.k8s.io/v1 is added we will need to update this function.
|
||||
func admitCRD(ar v1beta1.AdmissionReview) *v1beta1.AdmissionResponse {
|
||||
glog.V(2).Info("admitting crd")
|
||||
crdResource := metav1.GroupVersionResource{Group: "apiextensions.k8s.io", Version: "v1beta1", Resource: "customresourcedefinitions"}
|
||||
if ar.Request.Resource != crdResource {
|
||||
err := fmt.Errorf("expect resource to be %s", crdResource)
|
||||
glog.Error(err)
|
||||
return toAdmissionResponse(err)
|
||||
}
|
||||
|
||||
raw := ar.Request.Object.Raw
|
||||
crd := apiextensionsv1beta1.CustomResourceDefinition{}
|
||||
deserializer := codecs.UniversalDeserializer()
|
||||
if _, _, err := deserializer.Decode(raw, nil, &crd); err != nil {
|
||||
glog.Error(err)
|
||||
return toAdmissionResponse(err)
|
||||
}
|
||||
reviewResponse := v1beta1.AdmissionResponse{}
|
||||
reviewResponse.Allowed = true
|
||||
|
||||
if v, ok := crd.Labels["webhook-e2e-test"]; ok {
|
||||
if v == "webhook-disallow" {
|
||||
reviewResponse.Allowed = false
|
||||
reviewResponse.Result = &metav1.Status{Message: "the crd contains unwanted label"}
|
||||
}
|
||||
}
|
||||
return &reviewResponse
|
||||
}
|
90
test/images/webhook/customresource.go
Normal file
90
test/images/webhook/customresource.go
Normal file
@ -0,0 +1,90 @@
|
||||
/*
|
||||
Copyright 2018 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"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
"github.com/golang/glog"
|
||||
"k8s.io/api/admission/v1beta1"
|
||||
)
|
||||
|
||||
const (
|
||||
customResourcePatch1 string = `[
|
||||
{ "op": "add", "path": "/data/mutation-stage-1", "value": "yes" }
|
||||
]`
|
||||
customResourcePatch2 string = `[
|
||||
{ "op": "add", "path": "/data/mutation-stage-2", "value": "yes" }
|
||||
]`
|
||||
)
|
||||
|
||||
func mutateCustomResource(ar v1beta1.AdmissionReview) *v1beta1.AdmissionResponse {
|
||||
glog.V(2).Info("mutating custom resource")
|
||||
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 := v1beta1.AdmissionResponse{}
|
||||
reviewResponse.Allowed = true
|
||||
|
||||
if cr.Data["mutation-start"] == "yes" {
|
||||
reviewResponse.Patch = []byte(customResourcePatch1)
|
||||
}
|
||||
if cr.Data["mutation-stage-1"] == "yes" {
|
||||
reviewResponse.Patch = []byte(customResourcePatch2)
|
||||
}
|
||||
pt := v1beta1.PatchTypeJSONPatch
|
||||
reviewResponse.PatchType = &pt
|
||||
return &reviewResponse
|
||||
}
|
||||
|
||||
func admitCustomResource(ar v1beta1.AdmissionReview) *v1beta1.AdmissionResponse {
|
||||
glog.V(2).Info("admitting custom resource")
|
||||
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 := v1beta1.AdmissionResponse{}
|
||||
reviewResponse.Allowed = true
|
||||
for k, v := range cr.Data {
|
||||
if k == "webhook-e2e-test" && v == "webhook-disallow" {
|
||||
reviewResponse.Allowed = false
|
||||
reviewResponse.Result = &metav1.Status{
|
||||
Reason: "the custom resource contains unwanted data",
|
||||
}
|
||||
}
|
||||
}
|
||||
return &reviewResponse
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2017 The Kubernetes Authors.
|
||||
Copyright 2018 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.
|
||||
@ -22,44 +22,16 @@ import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/golang/glog"
|
||||
"k8s.io/api/admission/v1beta1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
|
||||
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" }
|
||||
]`
|
||||
addInitContainerPatch string = `[
|
||||
{"op":"add","path":"/spec/initContainers","value":[{"image":"webhook-added-image","name":"webhook-added-init-container","resources":{}}]}
|
||||
]`
|
||||
)
|
||||
|
||||
// Config contains the server (the webhook) cert and key.
|
||||
type Config struct {
|
||||
CertFile string
|
||||
KeyFile string
|
||||
}
|
||||
|
||||
func (c *Config) addFlags() {
|
||||
flag.StringVar(&c.CertFile, "tls-cert-file", c.CertFile, ""+
|
||||
"File containing the default x509 Certificate for HTTPS. (CA cert, if any, concatenated "+
|
||||
"after server cert).")
|
||||
flag.StringVar(&c.KeyFile, "tls-private-key-file", c.KeyFile, ""+
|
||||
"File containing the default x509 private key matching --tls-cert-file.")
|
||||
}
|
||||
|
||||
// toAdmissionResponse is a helper function to create an AdmissionResponse
|
||||
// with an embedded error
|
||||
func toAdmissionResponse(err error) *v1beta1.AdmissionResponse {
|
||||
return &v1beta1.AdmissionResponse{
|
||||
Result: &metav1.Status{
|
||||
@ -68,231 +40,11 @@ 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.
|
||||
func admitPods(ar v1beta1.AdmissionReview) *v1beta1.AdmissionResponse {
|
||||
glog.V(2).Info("admitting pods")
|
||||
podResource := metav1.GroupVersionResource{Group: "", Version: "v1", Resource: "pods"}
|
||||
if ar.Request.Resource != podResource {
|
||||
err := fmt.Errorf("expect resource to be %s", podResource)
|
||||
glog.Error(err)
|
||||
return toAdmissionResponse(err)
|
||||
}
|
||||
|
||||
raw := ar.Request.Object.Raw
|
||||
pod := corev1.Pod{}
|
||||
deserializer := codecs.UniversalDeserializer()
|
||||
if _, _, err := deserializer.Decode(raw, nil, &pod); err != nil {
|
||||
glog.Error(err)
|
||||
return toAdmissionResponse(err)
|
||||
}
|
||||
reviewResponse := v1beta1.AdmissionResponse{}
|
||||
reviewResponse.Allowed = true
|
||||
|
||||
var msg string
|
||||
if v, ok := pod.Labels["webhook-e2e-test"]; ok {
|
||||
if v == "webhook-disallow" {
|
||||
reviewResponse.Allowed = false
|
||||
msg = msg + "the pod contains unwanted label; "
|
||||
}
|
||||
if v == "wait-forever" {
|
||||
reviewResponse.Allowed = false
|
||||
msg = msg + "the pod response should not be sent; "
|
||||
<-make(chan int) // Sleep forever - no one sends to this channel
|
||||
}
|
||||
}
|
||||
for _, container := range pod.Spec.Containers {
|
||||
if strings.Contains(container.Name, "webhook-disallow") {
|
||||
reviewResponse.Allowed = false
|
||||
msg = msg + "the pod contains unwanted container name; "
|
||||
}
|
||||
}
|
||||
if !reviewResponse.Allowed {
|
||||
reviewResponse.Result = &metav1.Status{Message: strings.TrimSpace(msg)}
|
||||
}
|
||||
return &reviewResponse
|
||||
}
|
||||
|
||||
func mutatePods(ar v1beta1.AdmissionReview) *v1beta1.AdmissionResponse {
|
||||
glog.V(2).Info("mutating pods")
|
||||
podResource := metav1.GroupVersionResource{Group: "", Version: "v1", Resource: "pods"}
|
||||
if ar.Request.Resource != podResource {
|
||||
glog.Errorf("expect resource to be %s", podResource)
|
||||
return nil
|
||||
}
|
||||
|
||||
raw := ar.Request.Object.Raw
|
||||
pod := corev1.Pod{}
|
||||
deserializer := codecs.UniversalDeserializer()
|
||||
if _, _, err := deserializer.Decode(raw, nil, &pod); err != nil {
|
||||
glog.Error(err)
|
||||
return toAdmissionResponse(err)
|
||||
}
|
||||
reviewResponse := v1beta1.AdmissionResponse{}
|
||||
reviewResponse.Allowed = true
|
||||
if pod.Name == "webhook-to-be-mutated" {
|
||||
reviewResponse.Patch = []byte(addInitContainerPatch)
|
||||
pt := v1beta1.PatchTypeJSONPatch
|
||||
reviewResponse.PatchType = &pt
|
||||
}
|
||||
return &reviewResponse
|
||||
}
|
||||
|
||||
// deny configmaps with specific key-value pair.
|
||||
func admitConfigMaps(ar v1beta1.AdmissionReview) *v1beta1.AdmissionResponse {
|
||||
glog.V(2).Info("admitting 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 := v1beta1.AdmissionResponse{}
|
||||
reviewResponse.Allowed = true
|
||||
for k, v := range configmap.Data {
|
||||
if k == "webhook-e2e-test" && v == "webhook-disallow" {
|
||||
reviewResponse.Allowed = false
|
||||
reviewResponse.Result = &metav1.Status{
|
||||
Reason: "the configmap contains unwanted key and value",
|
||||
}
|
||||
}
|
||||
}
|
||||
return &reviewResponse
|
||||
}
|
||||
|
||||
func mutateConfigmaps(ar v1beta1.AdmissionReview) *v1beta1.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 := v1beta1.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 := v1beta1.PatchTypeJSONPatch
|
||||
reviewResponse.PatchType = &pt
|
||||
|
||||
return &reviewResponse
|
||||
}
|
||||
|
||||
func mutateCustomResource(ar v1beta1.AdmissionReview) *v1beta1.AdmissionResponse {
|
||||
glog.V(2).Info("mutating custom resource")
|
||||
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 := v1beta1.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 := v1beta1.PatchTypeJSONPatch
|
||||
reviewResponse.PatchType = &pt
|
||||
return &reviewResponse
|
||||
}
|
||||
|
||||
func admitCustomResource(ar v1beta1.AdmissionReview) *v1beta1.AdmissionResponse {
|
||||
glog.V(2).Info("admitting custom resource")
|
||||
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 := v1beta1.AdmissionResponse{}
|
||||
reviewResponse.Allowed = true
|
||||
for k, v := range cr.Data {
|
||||
if k == "webhook-e2e-test" && v == "webhook-disallow" {
|
||||
reviewResponse.Allowed = false
|
||||
reviewResponse.Result = &metav1.Status{
|
||||
Reason: "the custom resource contains unwanted data",
|
||||
}
|
||||
}
|
||||
}
|
||||
return &reviewResponse
|
||||
}
|
||||
|
||||
// Deny all crds with the label "webhook-e2e-test":"webhook-disallow"
|
||||
// This function expects all CRDs submitted to it to be apiextensions.k8s.io/v1beta1
|
||||
// TODO: When apiextensions.k8s.io/v1 is added we will need to update this function.
|
||||
func admitCRD(ar v1beta1.AdmissionReview) *v1beta1.AdmissionResponse {
|
||||
glog.V(2).Info("admitting crd")
|
||||
crdResource := metav1.GroupVersionResource{Group: "apiextensions.k8s.io", Version: "v1beta1", Resource: "customresourcedefinitions"}
|
||||
if ar.Request.Resource != crdResource {
|
||||
err := fmt.Errorf("expect resource to be %s", crdResource)
|
||||
glog.Error(err)
|
||||
return toAdmissionResponse(err)
|
||||
}
|
||||
|
||||
raw := ar.Request.Object.Raw
|
||||
crd := apiextensionsv1beta1.CustomResourceDefinition{}
|
||||
deserializer := codecs.UniversalDeserializer()
|
||||
if _, _, err := deserializer.Decode(raw, nil, &crd); err != nil {
|
||||
glog.Error(err)
|
||||
return toAdmissionResponse(err)
|
||||
}
|
||||
reviewResponse := v1beta1.AdmissionResponse{}
|
||||
reviewResponse.Allowed = true
|
||||
|
||||
if v, ok := crd.Labels["webhook-e2e-test"]; ok {
|
||||
if v == "webhook-disallow" {
|
||||
reviewResponse.Allowed = false
|
||||
reviewResponse.Result = &metav1.Status{Message: "the crd contains unwanted label"}
|
||||
}
|
||||
}
|
||||
return &reviewResponse
|
||||
}
|
||||
|
||||
// admitFunc is the type we use for all of our validators and mutators
|
||||
type admitFunc func(v1beta1.AdmissionReview) *v1beta1.AdmissionResponse
|
||||
|
||||
// serve handles the http portion of a request prior to handing to an admit
|
||||
// function
|
||||
func serve(w http.ResponseWriter, r *http.Request, admit admitFunc) {
|
||||
var body []byte
|
||||
if r.Body != nil {
|
||||
@ -309,31 +61,32 @@ func serve(w http.ResponseWriter, r *http.Request, admit admitFunc) {
|
||||
}
|
||||
|
||||
glog.V(2).Info(fmt.Sprintf("handling request: %v", body))
|
||||
var reviewResponse *v1beta1.AdmissionResponse
|
||||
ar := v1beta1.AdmissionReview{}
|
||||
|
||||
// The AdmissionReview that was sent to the webhook
|
||||
requestedAdmissionReview := v1beta1.AdmissionReview{}
|
||||
|
||||
// The AdmissionReview that will be returned
|
||||
responseAdmissionReview := v1beta1.AdmissionReview{}
|
||||
|
||||
deserializer := codecs.UniversalDeserializer()
|
||||
if _, _, err := deserializer.Decode(body, nil, &ar); err != nil {
|
||||
if _, _, err := deserializer.Decode(body, nil, &requestedAdmissionReview); err != nil {
|
||||
glog.Error(err)
|
||||
reviewResponse = toAdmissionResponse(err)
|
||||
responseAdmissionReview.Response = toAdmissionResponse(err)
|
||||
} else {
|
||||
reviewResponse = admit(ar)
|
||||
// pass to admitFunc
|
||||
responseAdmissionReview.Response = admit(requestedAdmissionReview)
|
||||
}
|
||||
glog.V(2).Info(fmt.Sprintf("sending response: %v", reviewResponse))
|
||||
|
||||
response := v1beta1.AdmissionReview{}
|
||||
if reviewResponse != nil {
|
||||
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{}
|
||||
// Return the same UID
|
||||
responseAdmissionReview.Response.UID = requestedAdmissionReview.Request.UID
|
||||
|
||||
resp, err := json.Marshal(response)
|
||||
glog.V(2).Info(fmt.Sprintf("sending response: %v", responseAdmissionReview.Response))
|
||||
|
||||
respBytes, err := json.Marshal(responseAdmissionReview)
|
||||
if err != nil {
|
||||
glog.Error(err)
|
||||
}
|
||||
if _, err := w.Write(resp); err != nil {
|
||||
if _, err := w.Write(respBytes); err != nil {
|
||||
glog.Error(err)
|
||||
}
|
||||
}
|
||||
@ -383,10 +136,9 @@ func main() {
|
||||
http.HandleFunc("/custom-resource", serveCustomResource)
|
||||
http.HandleFunc("/mutating-custom-resource", serveMutateCustomResource)
|
||||
http.HandleFunc("/crd", serveCRD)
|
||||
clientset := getClient()
|
||||
server := &http.Server{
|
||||
Addr: ":443",
|
||||
TLSConfig: configTLS(config, clientset),
|
||||
TLSConfig: configTLS(config),
|
||||
}
|
||||
server.ListenAndServeTLS("", "")
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2017 The Kubernetes Authors.
|
||||
Copyright 2018 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.
|
||||
@ -26,37 +26,87 @@ import (
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
)
|
||||
|
||||
func TestJSONPatchForConfigMap(t *testing.T) {
|
||||
cm := corev1.ConfigMap{
|
||||
Data: map[string]string{
|
||||
"mutation-start": "yes",
|
||||
func TestPatches(t *testing.T) {
|
||||
testCases := []struct {
|
||||
patch string
|
||||
initial interface{}
|
||||
expected interface{}
|
||||
toTest interface{}
|
||||
}{
|
||||
{
|
||||
patch: configMapPatch1,
|
||||
initial: corev1.ConfigMap{
|
||||
Data: map[string]string{
|
||||
"mutation-start": "yes",
|
||||
},
|
||||
},
|
||||
expected: &corev1.ConfigMap{
|
||||
Data: map[string]string{
|
||||
"mutation-start": "yes",
|
||||
"mutation-stage-1": "yes",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
patch: configMapPatch2,
|
||||
initial: corev1.ConfigMap{
|
||||
Data: map[string]string{
|
||||
"mutation-start": "yes",
|
||||
},
|
||||
},
|
||||
expected: &corev1.ConfigMap{
|
||||
Data: map[string]string{
|
||||
"mutation-start": "yes",
|
||||
"mutation-stage-2": "yes",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
patch: podsInitContainerPatch,
|
||||
initial: corev1.Pod{
|
||||
Spec: corev1.PodSpec{
|
||||
InitContainers: []corev1.Container{},
|
||||
},
|
||||
},
|
||||
expected: &corev1.Pod{
|
||||
Spec: corev1.PodSpec{
|
||||
InitContainers: []corev1.Container{
|
||||
{
|
||||
Image: "webhook-added-image",
|
||||
Name: "webhook-added-init-container",
|
||||
Resources: corev1.ResourceRequirements{},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
cmJS, err := json.Marshal(cm)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
for _, testcase := range testCases {
|
||||
objJS, err := json.Marshal(testcase.initial)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
patchObj, err := jsonpatch.DecodePatch([]byte(testcase.patch))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
patchedJS, err := patchObj.Apply(objJS)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
objType := reflect.TypeOf(testcase.initial)
|
||||
objTest := reflect.New(objType).Interface()
|
||||
err = json.Unmarshal(patchedJS, objTest)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !reflect.DeepEqual(objTest, testcase.expected) {
|
||||
t.Errorf("\nexpected %#v\n, got %#v", testcase.expected, objTest)
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
@ -74,7 +124,7 @@ func TestJSONPatchForUnstructured(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
patchObj, err := jsonpatch.DecodePatch([]byte(patch1))
|
||||
patchObj, err := jsonpatch.DecodePatch([]byte(configMapPatch1))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
103
test/images/webhook/pods.go
Normal file
103
test/images/webhook/pods.go
Normal file
@ -0,0 +1,103 @@
|
||||
/*
|
||||
Copyright 2018 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 (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
"github.com/golang/glog"
|
||||
"k8s.io/api/admission/v1beta1"
|
||||
)
|
||||
|
||||
const (
|
||||
podsInitContainerPatch string = `[
|
||||
{"op":"add","path":"/spec/initContainers","value":[{"image":"webhook-added-image","name":"webhook-added-init-container","resources":{}}]}
|
||||
]`
|
||||
)
|
||||
|
||||
// only allow pods to pull images from specific registry.
|
||||
func admitPods(ar v1beta1.AdmissionReview) *v1beta1.AdmissionResponse {
|
||||
glog.V(2).Info("admitting pods")
|
||||
podResource := metav1.GroupVersionResource{Group: "", Version: "v1", Resource: "pods"}
|
||||
if ar.Request.Resource != podResource {
|
||||
err := fmt.Errorf("expect resource to be %s", podResource)
|
||||
glog.Error(err)
|
||||
return toAdmissionResponse(err)
|
||||
}
|
||||
|
||||
raw := ar.Request.Object.Raw
|
||||
pod := corev1.Pod{}
|
||||
deserializer := codecs.UniversalDeserializer()
|
||||
if _, _, err := deserializer.Decode(raw, nil, &pod); err != nil {
|
||||
glog.Error(err)
|
||||
return toAdmissionResponse(err)
|
||||
}
|
||||
reviewResponse := v1beta1.AdmissionResponse{}
|
||||
reviewResponse.Allowed = true
|
||||
|
||||
var msg string
|
||||
if v, ok := pod.Labels["webhook-e2e-test"]; ok {
|
||||
if v == "webhook-disallow" {
|
||||
reviewResponse.Allowed = false
|
||||
msg = msg + "the pod contains unwanted label; "
|
||||
}
|
||||
if v == "wait-forever" {
|
||||
reviewResponse.Allowed = false
|
||||
msg = msg + "the pod response should not be sent; "
|
||||
<-make(chan int) // Sleep forever - no one sends to this channel
|
||||
}
|
||||
}
|
||||
for _, container := range pod.Spec.Containers {
|
||||
if strings.Contains(container.Name, "webhook-disallow") {
|
||||
reviewResponse.Allowed = false
|
||||
msg = msg + "the pod contains unwanted container name; "
|
||||
}
|
||||
}
|
||||
if !reviewResponse.Allowed {
|
||||
reviewResponse.Result = &metav1.Status{Message: strings.TrimSpace(msg)}
|
||||
}
|
||||
return &reviewResponse
|
||||
}
|
||||
|
||||
func mutatePods(ar v1beta1.AdmissionReview) *v1beta1.AdmissionResponse {
|
||||
glog.V(2).Info("mutating pods")
|
||||
podResource := metav1.GroupVersionResource{Group: "", Version: "v1", Resource: "pods"}
|
||||
if ar.Request.Resource != podResource {
|
||||
glog.Errorf("expect resource to be %s", podResource)
|
||||
return nil
|
||||
}
|
||||
|
||||
raw := ar.Request.Object.Raw
|
||||
pod := corev1.Pod{}
|
||||
deserializer := codecs.UniversalDeserializer()
|
||||
if _, _, err := deserializer.Decode(raw, nil, &pod); err != nil {
|
||||
glog.Error(err)
|
||||
return toAdmissionResponse(err)
|
||||
}
|
||||
reviewResponse := v1beta1.AdmissionResponse{}
|
||||
reviewResponse.Allowed = true
|
||||
if pod.Name == "webhook-to-be-mutated" {
|
||||
reviewResponse.Patch = []byte(podsInitContainerPatch)
|
||||
pt := v1beta1.PatchTypeJSONPatch
|
||||
reviewResponse.PatchType = &pt
|
||||
}
|
||||
return &reviewResponse
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2017 The Kubernetes Authors.
|
||||
Copyright 2018 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.
|
||||
|
@ -48,7 +48,7 @@ func (i *ImageConfig) SetVersion(version string) {
|
||||
}
|
||||
|
||||
var (
|
||||
AdmissionWebhook = ImageConfig{e2eRegistry, "webhook", "1.10v2", false}
|
||||
AdmissionWebhook = ImageConfig{e2eRegistry, "webhook", "1.12v1", true}
|
||||
APIServer = ImageConfig{e2eRegistry, "sample-apiserver", "1.0", false}
|
||||
AppArmorLoader = ImageConfig{gcRegistry, "apparmor-loader", "0.1", false}
|
||||
BusyBox = ImageConfig{gcRegistry, "busybox", "1.24", false}
|
||||
|
Loading…
Reference in New Issue
Block a user