mirror of
				https://github.com/k3s-io/kubernetes.git
				synced 2025-11-04 07:49:35 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			1701 lines
		
	
	
		
			62 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			1701 lines
		
	
	
		
			62 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
/*
 | 
						|
Copyright 2019 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 admissionwebhook
 | 
						|
 | 
						|
import (
 | 
						|
	"context"
 | 
						|
	"crypto/tls"
 | 
						|
	"crypto/x509"
 | 
						|
	"encoding/json"
 | 
						|
	"fmt"
 | 
						|
	"io/ioutil"
 | 
						|
	"net/http"
 | 
						|
	"net/http/httptest"
 | 
						|
	"sort"
 | 
						|
	"strings"
 | 
						|
	"sync"
 | 
						|
	"testing"
 | 
						|
	"time"
 | 
						|
 | 
						|
	admissionreviewv1 "k8s.io/api/admission/v1"
 | 
						|
	"k8s.io/api/admission/v1beta1"
 | 
						|
	admissionv1 "k8s.io/api/admissionregistration/v1"
 | 
						|
	admissionv1beta1 "k8s.io/api/admissionregistration/v1beta1"
 | 
						|
	appsv1beta1 "k8s.io/api/apps/v1beta1"
 | 
						|
	corev1 "k8s.io/api/core/v1"
 | 
						|
	v1 "k8s.io/api/core/v1"
 | 
						|
	extensionsv1beta1 "k8s.io/api/extensions/v1beta1"
 | 
						|
	policyv1beta1 "k8s.io/api/policy/v1beta1"
 | 
						|
	apiextensionsclientset "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
 | 
						|
	apierrors "k8s.io/apimachinery/pkg/api/errors"
 | 
						|
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 | 
						|
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
 | 
						|
	"k8s.io/apimachinery/pkg/runtime"
 | 
						|
	"k8s.io/apimachinery/pkg/runtime/schema"
 | 
						|
	"k8s.io/apimachinery/pkg/types"
 | 
						|
	"k8s.io/apimachinery/pkg/util/sets"
 | 
						|
	"k8s.io/apimachinery/pkg/util/wait"
 | 
						|
	dynamic "k8s.io/client-go/dynamic"
 | 
						|
	clientset "k8s.io/client-go/kubernetes"
 | 
						|
	"k8s.io/client-go/rest"
 | 
						|
	"k8s.io/client-go/util/retry"
 | 
						|
	kubeapiservertesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing"
 | 
						|
	"k8s.io/kubernetes/test/integration/etcd"
 | 
						|
	"k8s.io/kubernetes/test/integration/framework"
 | 
						|
)
 | 
						|
 | 
						|
const (
 | 
						|
	testNamespace      = "webhook-integration"
 | 
						|
	testClientUsername = "webhook-integration-client"
 | 
						|
 | 
						|
	mutation   = "mutation"
 | 
						|
	validation = "validation"
 | 
						|
)
 | 
						|
 | 
						|
type testContext struct {
 | 
						|
	t *testing.T
 | 
						|
 | 
						|
	admissionHolder *holder
 | 
						|
 | 
						|
	client    dynamic.Interface
 | 
						|
	clientset clientset.Interface
 | 
						|
	verb      string
 | 
						|
	gvr       schema.GroupVersionResource
 | 
						|
	resource  metav1.APIResource
 | 
						|
	resources map[schema.GroupVersionResource]metav1.APIResource
 | 
						|
}
 | 
						|
 | 
						|
type testFunc func(*testContext)
 | 
						|
 | 
						|
var (
 | 
						|
	// defaultResourceFuncs holds the default test functions.
 | 
						|
	// may be overridden for specific resources by customTestFuncs.
 | 
						|
	defaultResourceFuncs = map[string]testFunc{
 | 
						|
		"create":           testResourceCreate,
 | 
						|
		"update":           testResourceUpdate,
 | 
						|
		"patch":            testResourcePatch,
 | 
						|
		"delete":           testResourceDelete,
 | 
						|
		"deletecollection": testResourceDeletecollection,
 | 
						|
	}
 | 
						|
 | 
						|
	// defaultSubresourceFuncs holds default subresource test functions.
 | 
						|
	// may be overridden for specific resources by customTestFuncs.
 | 
						|
	defaultSubresourceFuncs = map[string]testFunc{
 | 
						|
		"update": testSubresourceUpdate,
 | 
						|
		"patch":  testSubresourcePatch,
 | 
						|
	}
 | 
						|
 | 
						|
	// customTestFuncs holds custom test functions by resource and verb.
 | 
						|
	customTestFuncs = map[schema.GroupVersionResource]map[string]testFunc{
 | 
						|
		gvr("", "v1", "namespaces"): {"delete": testNamespaceDelete},
 | 
						|
 | 
						|
		gvr("apps", "v1beta1", "deployments/rollback"):       {"create": testDeploymentRollback},
 | 
						|
		gvr("extensions", "v1beta1", "deployments/rollback"): {"create": testDeploymentRollback},
 | 
						|
 | 
						|
		gvr("", "v1", "pods/attach"):      {"create": testPodConnectSubresource},
 | 
						|
		gvr("", "v1", "pods/exec"):        {"create": testPodConnectSubresource},
 | 
						|
		gvr("", "v1", "pods/portforward"): {"create": testPodConnectSubresource},
 | 
						|
 | 
						|
		gvr("", "v1", "bindings"):      {"create": testPodBindingEviction},
 | 
						|
		gvr("", "v1", "pods/binding"):  {"create": testPodBindingEviction},
 | 
						|
		gvr("", "v1", "pods/eviction"): {"create": testPodBindingEviction},
 | 
						|
 | 
						|
		gvr("", "v1", "nodes/proxy"):    {"*": testSubresourceProxy},
 | 
						|
		gvr("", "v1", "pods/proxy"):     {"*": testSubresourceProxy},
 | 
						|
		gvr("", "v1", "services/proxy"): {"*": testSubresourceProxy},
 | 
						|
 | 
						|
		gvr("random.numbers.com", "v1", "integers"): {"create": testPruningRandomNumbers},
 | 
						|
		gvr("custom.fancy.com", "v2", "pants"):      {"create": testNoPruningCustomFancy},
 | 
						|
	}
 | 
						|
 | 
						|
	// admissionExemptResources lists objects which are exempt from admission validation/mutation,
 | 
						|
	// only resources exempted from admission processing by API server should be listed here.
 | 
						|
	admissionExemptResources = map[schema.GroupVersionResource]bool{
 | 
						|
		gvr("admissionregistration.k8s.io", "v1beta1", "mutatingwebhookconfigurations"):   true,
 | 
						|
		gvr("admissionregistration.k8s.io", "v1beta1", "validatingwebhookconfigurations"): true,
 | 
						|
		gvr("admissionregistration.k8s.io", "v1", "mutatingwebhookconfigurations"):        true,
 | 
						|
		gvr("admissionregistration.k8s.io", "v1", "validatingwebhookconfigurations"):      true,
 | 
						|
	}
 | 
						|
 | 
						|
	parentResources = map[schema.GroupVersionResource]schema.GroupVersionResource{
 | 
						|
		gvr("extensions", "v1beta1", "replicationcontrollers/scale"): gvr("", "v1", "replicationcontrollers"),
 | 
						|
	}
 | 
						|
 | 
						|
	// stubDataOverrides holds either non persistent resources' definitions or resources where default stub needs to be overridden.
 | 
						|
	stubDataOverrides = map[schema.GroupVersionResource]string{
 | 
						|
		// Non persistent Reviews resource
 | 
						|
		gvr("authentication.k8s.io", "v1", "tokenreviews"):                  `{"metadata": {"name": "tokenreview"}, "spec": {"token": "token", "audience": ["audience1","audience2"]}}`,
 | 
						|
		gvr("authentication.k8s.io", "v1beta1", "tokenreviews"):             `{"metadata": {"name": "tokenreview"}, "spec": {"token": "token", "audience": ["audience1","audience2"]}}`,
 | 
						|
		gvr("authorization.k8s.io", "v1", "localsubjectaccessreviews"):      `{"metadata": {"name": "", "namespace":"` + testNamespace + `"}, "spec": {"uid": "token", "user": "user1","groups": ["group1","group2"],"resourceAttributes": {"name":"name1","namespace":"` + testNamespace + `"}}}`,
 | 
						|
		gvr("authorization.k8s.io", "v1", "subjectaccessreviews"):           `{"metadata": {"name": "", "namespace":""}, "spec": {"user":"user1","resourceAttributes": {"name":"name1", "namespace":"` + testNamespace + `"}}}`,
 | 
						|
		gvr("authorization.k8s.io", "v1", "selfsubjectaccessreviews"):       `{"metadata": {"name": "", "namespace":""}, "spec": {"resourceAttributes": {"name":"name1", "namespace":""}}}`,
 | 
						|
		gvr("authorization.k8s.io", "v1", "selfsubjectrulesreviews"):        `{"metadata": {"name": "", "namespace":"` + testNamespace + `"}, "spec": {"namespace":"` + testNamespace + `"}}`,
 | 
						|
		gvr("authorization.k8s.io", "v1beta1", "localsubjectaccessreviews"): `{"metadata": {"name": "", "namespace":"` + testNamespace + `"}, "spec": {"uid": "token", "user": "user1","groups": ["group1","group2"],"resourceAttributes": {"name":"name1","namespace":"` + testNamespace + `"}}}`,
 | 
						|
		gvr("authorization.k8s.io", "v1beta1", "subjectaccessreviews"):      `{"metadata": {"name": "", "namespace":""}, "spec": {"user":"user1","resourceAttributes": {"name":"name1", "namespace":"` + testNamespace + `"}}}`,
 | 
						|
		gvr("authorization.k8s.io", "v1beta1", "selfsubjectaccessreviews"):  `{"metadata": {"name": "", "namespace":""}, "spec": {"resourceAttributes": {"name":"name1", "namespace":""}}}`,
 | 
						|
		gvr("authorization.k8s.io", "v1beta1", "selfsubjectrulesreviews"):   `{"metadata": {"name": "", "namespace":"` + testNamespace + `"}, "spec": {"namespace":"` + testNamespace + `"}}`,
 | 
						|
 | 
						|
		// Other Non persistent resources
 | 
						|
	}
 | 
						|
)
 | 
						|
 | 
						|
type webhookOptions struct {
 | 
						|
	version string
 | 
						|
 | 
						|
	// phase indicates whether this is a mutating or validating webhook
 | 
						|
	phase string
 | 
						|
	// converted indicates if this webhook makes use of matchPolicy:equivalent and expects conversion.
 | 
						|
	// if true, recordGVR and expectGVK are mapped through gvrToConvertedGVR/gvrToConvertedGVK.
 | 
						|
	// if false, recordGVR and expectGVK are compared directly to the admission review.
 | 
						|
	converted bool
 | 
						|
}
 | 
						|
 | 
						|
type holder struct {
 | 
						|
	lock sync.RWMutex
 | 
						|
 | 
						|
	t *testing.T
 | 
						|
 | 
						|
	warningHandler *warningHandler
 | 
						|
 | 
						|
	recordGVR       metav1.GroupVersionResource
 | 
						|
	recordOperation string
 | 
						|
	recordNamespace string
 | 
						|
	recordName      string
 | 
						|
 | 
						|
	expectGVK        schema.GroupVersionKind
 | 
						|
	expectObject     bool
 | 
						|
	expectOldObject  bool
 | 
						|
	expectOptionsGVK schema.GroupVersionKind
 | 
						|
	expectOptions    bool
 | 
						|
 | 
						|
	// gvrToConvertedGVR maps the GVR submitted to the API server to the expected GVR when converted to the webhook-recognized resource.
 | 
						|
	// When a converted request is recorded, gvrToConvertedGVR[recordGVR] is compared to the GVR seen by the webhook.
 | 
						|
	gvrToConvertedGVR map[metav1.GroupVersionResource]metav1.GroupVersionResource
 | 
						|
	// gvrToConvertedGVR maps the GVR submitted to the API server to the expected GVK when converted to the webhook-recognized resource.
 | 
						|
	// When a converted request is recorded, gvrToConvertedGVR[expectGVK] is compared to the GVK seen by the webhook.
 | 
						|
	gvrToConvertedGVK map[metav1.GroupVersionResource]schema.GroupVersionKind
 | 
						|
 | 
						|
	recorded map[webhookOptions]*admissionRequest
 | 
						|
}
 | 
						|
 | 
						|
func (h *holder) reset(t *testing.T) {
 | 
						|
	h.lock.Lock()
 | 
						|
	defer h.lock.Unlock()
 | 
						|
	h.t = t
 | 
						|
	h.recordGVR = metav1.GroupVersionResource{}
 | 
						|
	h.expectGVK = schema.GroupVersionKind{}
 | 
						|
	h.recordOperation = ""
 | 
						|
	h.recordName = ""
 | 
						|
	h.recordNamespace = ""
 | 
						|
	h.expectObject = false
 | 
						|
	h.expectOldObject = false
 | 
						|
	h.expectOptionsGVK = schema.GroupVersionKind{}
 | 
						|
	h.expectOptions = false
 | 
						|
	h.warningHandler.reset()
 | 
						|
 | 
						|
	// Set up the recorded map with nil records for all combinations
 | 
						|
	h.recorded = map[webhookOptions]*admissionRequest{}
 | 
						|
	for _, phase := range []string{mutation, validation} {
 | 
						|
		for _, converted := range []bool{true, false} {
 | 
						|
			for _, version := range []string{"v1", "v1beta1"} {
 | 
						|
				h.recorded[webhookOptions{version: version, phase: phase, converted: converted}] = nil
 | 
						|
			}
 | 
						|
		}
 | 
						|
	}
 | 
						|
}
 | 
						|
func (h *holder) expect(gvr schema.GroupVersionResource, gvk, optionsGVK schema.GroupVersionKind, operation v1beta1.Operation, name, namespace string, object, oldObject, options bool) {
 | 
						|
	// Special-case namespaces, since the object name shows up in request attributes for update/delete requests
 | 
						|
	if len(namespace) == 0 && gvk.Group == "" && gvk.Version == "v1" && gvk.Kind == "Namespace" && operation != v1beta1.Create {
 | 
						|
		namespace = name
 | 
						|
	}
 | 
						|
 | 
						|
	h.lock.Lock()
 | 
						|
	defer h.lock.Unlock()
 | 
						|
	h.recordGVR = metav1.GroupVersionResource{Group: gvr.Group, Version: gvr.Version, Resource: gvr.Resource}
 | 
						|
	h.expectGVK = gvk
 | 
						|
	h.recordOperation = string(operation)
 | 
						|
	h.recordName = name
 | 
						|
	h.recordNamespace = namespace
 | 
						|
	h.expectObject = object
 | 
						|
	h.expectOldObject = oldObject
 | 
						|
	h.expectOptionsGVK = optionsGVK
 | 
						|
	h.expectOptions = options
 | 
						|
	h.warningHandler.reset()
 | 
						|
 | 
						|
	// Set up the recorded map with nil records for all combinations
 | 
						|
	h.recorded = map[webhookOptions]*admissionRequest{}
 | 
						|
	for _, phase := range []string{mutation, validation} {
 | 
						|
		for _, converted := range []bool{true, false} {
 | 
						|
			for _, version := range []string{"v1", "v1beta1"} {
 | 
						|
				h.recorded[webhookOptions{version: version, phase: phase, converted: converted}] = nil
 | 
						|
			}
 | 
						|
		}
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
type admissionRequest struct {
 | 
						|
	Operation   string
 | 
						|
	Resource    metav1.GroupVersionResource
 | 
						|
	SubResource string
 | 
						|
	Namespace   string
 | 
						|
	Name        string
 | 
						|
	Object      runtime.RawExtension
 | 
						|
	OldObject   runtime.RawExtension
 | 
						|
	Options     runtime.RawExtension
 | 
						|
}
 | 
						|
 | 
						|
func (h *holder) record(version string, phase string, converted bool, request *admissionRequest) {
 | 
						|
	h.lock.Lock()
 | 
						|
	defer h.lock.Unlock()
 | 
						|
 | 
						|
	// this is useful to turn on if items aren't getting recorded and you need to figure out why
 | 
						|
	debug := false
 | 
						|
	if debug {
 | 
						|
		h.t.Logf("%s %#v %v", request.Operation, request.Resource, request.SubResource)
 | 
						|
	}
 | 
						|
 | 
						|
	resource := request.Resource
 | 
						|
	if len(request.SubResource) > 0 {
 | 
						|
		resource.Resource += "/" + request.SubResource
 | 
						|
	}
 | 
						|
 | 
						|
	// See if we should record this
 | 
						|
	gvrToRecord := h.recordGVR
 | 
						|
	if converted {
 | 
						|
		// If this is a converted webhook, map to the GVR we expect the webhook to see
 | 
						|
		gvrToRecord = h.gvrToConvertedGVR[h.recordGVR]
 | 
						|
	}
 | 
						|
	if resource != gvrToRecord {
 | 
						|
		if debug {
 | 
						|
			h.t.Log(resource, "!=", gvrToRecord)
 | 
						|
		}
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	if request.Operation != h.recordOperation {
 | 
						|
		if debug {
 | 
						|
			h.t.Log(request.Operation, "!=", h.recordOperation)
 | 
						|
		}
 | 
						|
		return
 | 
						|
	}
 | 
						|
	if request.Namespace != h.recordNamespace {
 | 
						|
		if debug {
 | 
						|
			h.t.Log(request.Namespace, "!=", h.recordNamespace)
 | 
						|
		}
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	name := request.Name
 | 
						|
	if name != h.recordName {
 | 
						|
		if debug {
 | 
						|
			h.t.Log(name, "!=", h.recordName)
 | 
						|
		}
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	if debug {
 | 
						|
		h.t.Logf("recording: %#v = %s %#v %v", webhookOptions{version: version, phase: phase, converted: converted}, request.Operation, request.Resource, request.SubResource)
 | 
						|
	}
 | 
						|
	h.recorded[webhookOptions{version: version, phase: phase, converted: converted}] = request
 | 
						|
}
 | 
						|
 | 
						|
func (h *holder) verify(t *testing.T) {
 | 
						|
	h.lock.Lock()
 | 
						|
	defer h.lock.Unlock()
 | 
						|
 | 
						|
	for options, value := range h.recorded {
 | 
						|
		if err := h.verifyRequest(options, value); err != nil {
 | 
						|
			t.Errorf("version: %v, phase:%v, converted:%v error: %v", options.version, options.phase, options.converted, err)
 | 
						|
		}
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func (h *holder) verifyRequest(webhookOptions webhookOptions, request *admissionRequest) error {
 | 
						|
	converted := webhookOptions.converted
 | 
						|
 | 
						|
	// Check if current resource should be exempted from Admission processing
 | 
						|
	if admissionExemptResources[gvr(h.recordGVR.Group, h.recordGVR.Version, h.recordGVR.Resource)] {
 | 
						|
		if request == nil {
 | 
						|
			return nil
 | 
						|
		}
 | 
						|
		return fmt.Errorf("admission webhook was called, but not supposed to")
 | 
						|
	}
 | 
						|
 | 
						|
	if request == nil {
 | 
						|
		return fmt.Errorf("no request received")
 | 
						|
	}
 | 
						|
 | 
						|
	if h.expectObject {
 | 
						|
		if err := h.verifyObject(converted, request.Object.Object); err != nil {
 | 
						|
			return fmt.Errorf("object error: %v", err)
 | 
						|
		}
 | 
						|
	} else if request.Object.Object != nil {
 | 
						|
		return fmt.Errorf("unexpected object: %#v", request.Object.Object)
 | 
						|
	}
 | 
						|
 | 
						|
	if h.expectOldObject {
 | 
						|
		if err := h.verifyObject(converted, request.OldObject.Object); err != nil {
 | 
						|
			return fmt.Errorf("old object error: %v", err)
 | 
						|
		}
 | 
						|
	} else if request.OldObject.Object != nil {
 | 
						|
		return fmt.Errorf("unexpected old object: %#v", request.OldObject.Object)
 | 
						|
	}
 | 
						|
 | 
						|
	if h.expectOptions {
 | 
						|
		if err := h.verifyOptions(request.Options.Object); err != nil {
 | 
						|
			return fmt.Errorf("options error: %v", err)
 | 
						|
		}
 | 
						|
	} else if request.Options.Object != nil {
 | 
						|
		return fmt.Errorf("unexpected options: %#v", request.Options.Object)
 | 
						|
	}
 | 
						|
 | 
						|
	if !h.warningHandler.hasWarning(makeWarning(webhookOptions.version, webhookOptions.phase, webhookOptions.converted)) {
 | 
						|
		return fmt.Errorf("no warning received from webhook")
 | 
						|
	}
 | 
						|
 | 
						|
	return nil
 | 
						|
}
 | 
						|
 | 
						|
func (h *holder) verifyObject(converted bool, obj runtime.Object) error {
 | 
						|
	if obj == nil {
 | 
						|
		return fmt.Errorf("no object sent")
 | 
						|
	}
 | 
						|
	expectGVK := h.expectGVK
 | 
						|
	if converted {
 | 
						|
		expectGVK = h.gvrToConvertedGVK[h.recordGVR]
 | 
						|
	}
 | 
						|
	if obj.GetObjectKind().GroupVersionKind() != expectGVK {
 | 
						|
		return fmt.Errorf("expected %#v, got %#v", expectGVK, obj.GetObjectKind().GroupVersionKind())
 | 
						|
	}
 | 
						|
	return nil
 | 
						|
}
 | 
						|
 | 
						|
func (h *holder) verifyOptions(options runtime.Object) error {
 | 
						|
	if options == nil {
 | 
						|
		return fmt.Errorf("no options sent")
 | 
						|
	}
 | 
						|
	if options.GetObjectKind().GroupVersionKind() != h.expectOptionsGVK {
 | 
						|
		return fmt.Errorf("expected %#v, got %#v", h.expectOptionsGVK, options.GetObjectKind().GroupVersionKind())
 | 
						|
	}
 | 
						|
	return nil
 | 
						|
}
 | 
						|
 | 
						|
type warningHandler struct {
 | 
						|
	lock     sync.Mutex
 | 
						|
	warnings map[string]bool
 | 
						|
}
 | 
						|
 | 
						|
func (w *warningHandler) reset() {
 | 
						|
	w.lock.Lock()
 | 
						|
	defer w.lock.Unlock()
 | 
						|
	w.warnings = map[string]bool{}
 | 
						|
}
 | 
						|
func (w *warningHandler) hasWarning(warning string) bool {
 | 
						|
	w.lock.Lock()
 | 
						|
	defer w.lock.Unlock()
 | 
						|
	return w.warnings[warning]
 | 
						|
}
 | 
						|
func makeWarning(version string, phase string, converted bool) string {
 | 
						|
	return fmt.Sprintf("%v/%v/%v", version, phase, converted)
 | 
						|
}
 | 
						|
 | 
						|
func (w *warningHandler) HandleWarningHeader(code int, agent string, message string) {
 | 
						|
	if code != 299 || len(message) == 0 {
 | 
						|
		return
 | 
						|
	}
 | 
						|
	w.lock.Lock()
 | 
						|
	defer w.lock.Unlock()
 | 
						|
	w.warnings[message] = true
 | 
						|
}
 | 
						|
 | 
						|
// TestWebhookAdmissionWithWatchCache tests communication between API server and webhook process.
 | 
						|
func TestWebhookAdmissionWithWatchCache(t *testing.T) {
 | 
						|
	testWebhookAdmission(t, true)
 | 
						|
}
 | 
						|
 | 
						|
// TestWebhookAdmissionWithoutWatchCache tests communication between API server and webhook process.
 | 
						|
func TestWebhookAdmissionWithoutWatchCache(t *testing.T) {
 | 
						|
	testWebhookAdmission(t, false)
 | 
						|
}
 | 
						|
 | 
						|
// testWebhookAdmission tests communication between API server and webhook process.
 | 
						|
func testWebhookAdmission(t *testing.T, watchCache bool) {
 | 
						|
	// holder communicates expectations to webhooks, and results from webhooks
 | 
						|
	holder := &holder{
 | 
						|
		t:                 t,
 | 
						|
		warningHandler:    &warningHandler{warnings: map[string]bool{}},
 | 
						|
		gvrToConvertedGVR: map[metav1.GroupVersionResource]metav1.GroupVersionResource{},
 | 
						|
		gvrToConvertedGVK: map[metav1.GroupVersionResource]schema.GroupVersionKind{},
 | 
						|
	}
 | 
						|
 | 
						|
	// set up webhook server
 | 
						|
	roots := x509.NewCertPool()
 | 
						|
	if !roots.AppendCertsFromPEM(localhostCert) {
 | 
						|
		t.Fatal("Failed to append Cert from PEM")
 | 
						|
	}
 | 
						|
	cert, err := tls.X509KeyPair(localhostCert, localhostKey)
 | 
						|
	if err != nil {
 | 
						|
		t.Fatalf("Failed to build cert with error: %+v", err)
 | 
						|
	}
 | 
						|
 | 
						|
	webhookMux := http.NewServeMux()
 | 
						|
	webhookMux.Handle("/v1beta1/"+mutation, newV1beta1WebhookHandler(t, holder, mutation, false))
 | 
						|
	webhookMux.Handle("/v1beta1/convert/"+mutation, newV1beta1WebhookHandler(t, holder, mutation, true))
 | 
						|
	webhookMux.Handle("/v1beta1/"+validation, newV1beta1WebhookHandler(t, holder, validation, false))
 | 
						|
	webhookMux.Handle("/v1beta1/convert/"+validation, newV1beta1WebhookHandler(t, holder, validation, true))
 | 
						|
	webhookMux.Handle("/v1/"+mutation, newV1WebhookHandler(t, holder, mutation, false))
 | 
						|
	webhookMux.Handle("/v1/convert/"+mutation, newV1WebhookHandler(t, holder, mutation, true))
 | 
						|
	webhookMux.Handle("/v1/"+validation, newV1WebhookHandler(t, holder, validation, false))
 | 
						|
	webhookMux.Handle("/v1/convert/"+validation, newV1WebhookHandler(t, holder, validation, true))
 | 
						|
	webhookMux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
 | 
						|
		holder.t.Errorf("unexpected request to %v", req.URL.Path)
 | 
						|
	}))
 | 
						|
	webhookServer := httptest.NewUnstartedServer(webhookMux)
 | 
						|
	webhookServer.TLS = &tls.Config{
 | 
						|
		RootCAs:      roots,
 | 
						|
		Certificates: []tls.Certificate{cert},
 | 
						|
	}
 | 
						|
	webhookServer.StartTLS()
 | 
						|
	defer webhookServer.Close()
 | 
						|
 | 
						|
	// start API server
 | 
						|
	etcdConfig := framework.SharedEtcd()
 | 
						|
	server := kubeapiservertesting.StartTestServerOrDie(t, nil, []string{
 | 
						|
		fmt.Sprintf("--watch-cache=%v", watchCache),
 | 
						|
		// turn off admission plugins that add finalizers
 | 
						|
		"--disable-admission-plugins=ServiceAccount,StorageObjectInUseProtection",
 | 
						|
		// force enable all resources so we can check storage.
 | 
						|
		"--runtime-config=api/all=true",
 | 
						|
	}, etcdConfig)
 | 
						|
	defer server.TearDownFn()
 | 
						|
 | 
						|
	// Configure a client with a distinct user name so that it is easy to distinguish requests
 | 
						|
	// made by the client from requests made by controllers. We use this to filter out requests
 | 
						|
	// before recording them to ensure we don't accidentally mistake requests from controllers
 | 
						|
	// as requests made by the client.
 | 
						|
	clientConfig := rest.CopyConfig(server.ClientConfig)
 | 
						|
	clientConfig.Impersonate.UserName = testClientUsername
 | 
						|
	clientConfig.Impersonate.Groups = []string{"system:masters", "system:authenticated"}
 | 
						|
	clientConfig.WarningHandler = holder.warningHandler
 | 
						|
	client, err := clientset.NewForConfig(clientConfig)
 | 
						|
	if err != nil {
 | 
						|
		t.Fatalf("unexpected error: %v", err)
 | 
						|
	}
 | 
						|
 | 
						|
	// create CRDs
 | 
						|
	etcd.CreateTestCRDs(t, apiextensionsclientset.NewForConfigOrDie(server.ClientConfig), false, etcd.GetCustomResourceDefinitionData()...)
 | 
						|
 | 
						|
	if _, err := client.CoreV1().Namespaces().Create(context.TODO(), &v1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: testNamespace}}, metav1.CreateOptions{}); err != nil {
 | 
						|
		t.Fatal(err)
 | 
						|
	}
 | 
						|
 | 
						|
	// gather resources to test
 | 
						|
	dynamicClient, err := dynamic.NewForConfig(clientConfig)
 | 
						|
	if err != nil {
 | 
						|
		t.Fatal(err)
 | 
						|
	}
 | 
						|
	_, resources, err := client.Discovery().ServerGroupsAndResources()
 | 
						|
	if err != nil {
 | 
						|
		t.Fatalf("Failed to get ServerGroupsAndResources with error: %+v", err)
 | 
						|
	}
 | 
						|
 | 
						|
	gvrsToTest := []schema.GroupVersionResource{}
 | 
						|
	resourcesByGVR := map[schema.GroupVersionResource]metav1.APIResource{}
 | 
						|
 | 
						|
	for _, list := range resources {
 | 
						|
		defaultGroupVersion, err := schema.ParseGroupVersion(list.GroupVersion)
 | 
						|
		if err != nil {
 | 
						|
			t.Errorf("Failed to get GroupVersion for: %+v", list)
 | 
						|
			continue
 | 
						|
		}
 | 
						|
		for _, resource := range list.APIResources {
 | 
						|
			if resource.Group == "" {
 | 
						|
				resource.Group = defaultGroupVersion.Group
 | 
						|
			}
 | 
						|
			if resource.Version == "" {
 | 
						|
				resource.Version = defaultGroupVersion.Version
 | 
						|
			}
 | 
						|
			gvr := defaultGroupVersion.WithResource(resource.Name)
 | 
						|
			resourcesByGVR[gvr] = resource
 | 
						|
			if shouldTestResource(gvr, resource) {
 | 
						|
				gvrsToTest = append(gvrsToTest, gvr)
 | 
						|
			}
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	sort.SliceStable(gvrsToTest, func(i, j int) bool {
 | 
						|
		if gvrsToTest[i].Group < gvrsToTest[j].Group {
 | 
						|
			return true
 | 
						|
		}
 | 
						|
		if gvrsToTest[i].Group > gvrsToTest[j].Group {
 | 
						|
			return false
 | 
						|
		}
 | 
						|
		if gvrsToTest[i].Version < gvrsToTest[j].Version {
 | 
						|
			return true
 | 
						|
		}
 | 
						|
		if gvrsToTest[i].Version > gvrsToTest[j].Version {
 | 
						|
			return false
 | 
						|
		}
 | 
						|
		if gvrsToTest[i].Resource < gvrsToTest[j].Resource {
 | 
						|
			return true
 | 
						|
		}
 | 
						|
		if gvrsToTest[i].Resource > gvrsToTest[j].Resource {
 | 
						|
			return false
 | 
						|
		}
 | 
						|
		return true
 | 
						|
	})
 | 
						|
 | 
						|
	// map unqualified resource names to the fully qualified resource we will expect to be converted to
 | 
						|
	// Note: this only works because there are no overlapping resource names in-process that are not co-located
 | 
						|
	convertedResources := map[string]schema.GroupVersionResource{}
 | 
						|
	// build the webhook rules enumerating the specific group/version/resources we want
 | 
						|
	convertedV1beta1Rules := []admissionv1beta1.RuleWithOperations{}
 | 
						|
	convertedV1Rules := []admissionv1.RuleWithOperations{}
 | 
						|
	for _, gvr := range gvrsToTest {
 | 
						|
		metaGVR := metav1.GroupVersionResource{Group: gvr.Group, Version: gvr.Version, Resource: gvr.Resource}
 | 
						|
 | 
						|
		convertedGVR, ok := convertedResources[gvr.Resource]
 | 
						|
		if !ok {
 | 
						|
			// this is the first time we've seen this resource
 | 
						|
			// record the fully qualified resource we expect
 | 
						|
			convertedGVR = gvr
 | 
						|
			convertedResources[gvr.Resource] = gvr
 | 
						|
			// add an admission rule indicating we can receive this version
 | 
						|
			convertedV1beta1Rules = append(convertedV1beta1Rules, admissionv1beta1.RuleWithOperations{
 | 
						|
				Operations: []admissionv1beta1.OperationType{admissionv1beta1.OperationAll},
 | 
						|
				Rule:       admissionv1beta1.Rule{APIGroups: []string{gvr.Group}, APIVersions: []string{gvr.Version}, Resources: []string{gvr.Resource}},
 | 
						|
			})
 | 
						|
			convertedV1Rules = append(convertedV1Rules, admissionv1.RuleWithOperations{
 | 
						|
				Operations: []admissionv1.OperationType{admissionv1.OperationAll},
 | 
						|
				Rule:       admissionv1.Rule{APIGroups: []string{gvr.Group}, APIVersions: []string{gvr.Version}, Resources: []string{gvr.Resource}},
 | 
						|
			})
 | 
						|
		}
 | 
						|
 | 
						|
		// record the expected resource and kind
 | 
						|
		holder.gvrToConvertedGVR[metaGVR] = metav1.GroupVersionResource{Group: convertedGVR.Group, Version: convertedGVR.Version, Resource: convertedGVR.Resource}
 | 
						|
		holder.gvrToConvertedGVK[metaGVR] = schema.GroupVersionKind{Group: resourcesByGVR[convertedGVR].Group, Version: resourcesByGVR[convertedGVR].Version, Kind: resourcesByGVR[convertedGVR].Kind}
 | 
						|
	}
 | 
						|
 | 
						|
	if err := createV1beta1MutationWebhook(client, webhookServer.URL+"/v1beta1/"+mutation, webhookServer.URL+"/v1beta1/convert/"+mutation, convertedV1beta1Rules); err != nil {
 | 
						|
		t.Fatal(err)
 | 
						|
	}
 | 
						|
	if err := createV1beta1ValidationWebhook(client, webhookServer.URL+"/v1beta1/"+validation, webhookServer.URL+"/v1beta1/convert/"+validation, convertedV1beta1Rules); err != nil {
 | 
						|
		t.Fatal(err)
 | 
						|
	}
 | 
						|
	if err := createV1MutationWebhook(client, webhookServer.URL+"/v1/"+mutation, webhookServer.URL+"/v1/convert/"+mutation, convertedV1Rules); err != nil {
 | 
						|
		t.Fatal(err)
 | 
						|
	}
 | 
						|
	if err := createV1ValidationWebhook(client, webhookServer.URL+"/v1/"+validation, webhookServer.URL+"/v1/convert/"+validation, convertedV1Rules); err != nil {
 | 
						|
		t.Fatal(err)
 | 
						|
	}
 | 
						|
 | 
						|
	// Allow the webhook to establish
 | 
						|
	time.Sleep(time.Second)
 | 
						|
 | 
						|
	start := time.Now()
 | 
						|
	count := 0
 | 
						|
 | 
						|
	// Test admission on all resources, subresources, and verbs
 | 
						|
	for _, gvr := range gvrsToTest {
 | 
						|
		resource := resourcesByGVR[gvr]
 | 
						|
		t.Run(gvr.Group+"."+gvr.Version+"."+strings.ReplaceAll(resource.Name, "/", "."), func(t *testing.T) {
 | 
						|
			for _, verb := range []string{"create", "update", "patch", "connect", "delete", "deletecollection"} {
 | 
						|
				if shouldTestResourceVerb(gvr, resource, verb) {
 | 
						|
					t.Run(verb, func(t *testing.T) {
 | 
						|
						count++
 | 
						|
						holder.reset(t)
 | 
						|
						testFunc := getTestFunc(gvr, verb)
 | 
						|
						testFunc(&testContext{
 | 
						|
							t:               t,
 | 
						|
							admissionHolder: holder,
 | 
						|
							client:          dynamicClient,
 | 
						|
							clientset:       client,
 | 
						|
							verb:            verb,
 | 
						|
							gvr:             gvr,
 | 
						|
							resource:        resource,
 | 
						|
							resources:       resourcesByGVR,
 | 
						|
						})
 | 
						|
						holder.verify(t)
 | 
						|
					})
 | 
						|
				}
 | 
						|
			}
 | 
						|
		})
 | 
						|
	}
 | 
						|
 | 
						|
	duration := time.Since(start)
 | 
						|
	perResourceDuration := time.Duration(int(duration) / count)
 | 
						|
	if perResourceDuration >= 150*time.Millisecond {
 | 
						|
		t.Errorf("expected resources to process in < 150ms, average was %v", perResourceDuration)
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
//
 | 
						|
// generic resource testing
 | 
						|
//
 | 
						|
 | 
						|
func testResourceCreate(c *testContext) {
 | 
						|
	stubObj, err := getStubObj(c.gvr, c.resource)
 | 
						|
	if err != nil {
 | 
						|
		c.t.Error(err)
 | 
						|
		return
 | 
						|
	}
 | 
						|
	ns := ""
 | 
						|
	if c.resource.Namespaced {
 | 
						|
		ns = testNamespace
 | 
						|
	}
 | 
						|
	c.admissionHolder.expect(c.gvr, gvk(c.resource.Group, c.resource.Version, c.resource.Kind), gvkCreateOptions, v1beta1.Create, stubObj.GetName(), ns, true, false, true)
 | 
						|
	_, err = c.client.Resource(c.gvr).Namespace(ns).Create(context.TODO(), stubObj, metav1.CreateOptions{})
 | 
						|
	if err != nil {
 | 
						|
		c.t.Error(err)
 | 
						|
		return
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func testResourceUpdate(c *testContext) {
 | 
						|
	if err := retry.RetryOnConflict(retry.DefaultBackoff, func() error {
 | 
						|
		obj, err := createOrGetResource(c.client, c.gvr, c.resource)
 | 
						|
		if err != nil {
 | 
						|
			return err
 | 
						|
		}
 | 
						|
		obj.SetAnnotations(map[string]string{"update": "true"})
 | 
						|
		c.admissionHolder.expect(c.gvr, gvk(c.resource.Group, c.resource.Version, c.resource.Kind), gvkUpdateOptions, v1beta1.Update, obj.GetName(), obj.GetNamespace(), true, true, true)
 | 
						|
		_, err = c.client.Resource(c.gvr).Namespace(obj.GetNamespace()).Update(context.TODO(), obj, metav1.UpdateOptions{})
 | 
						|
		return err
 | 
						|
	}); err != nil {
 | 
						|
		c.t.Error(err)
 | 
						|
		return
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func testResourcePatch(c *testContext) {
 | 
						|
	obj, err := createOrGetResource(c.client, c.gvr, c.resource)
 | 
						|
	if err != nil {
 | 
						|
		c.t.Error(err)
 | 
						|
		return
 | 
						|
	}
 | 
						|
	c.admissionHolder.expect(c.gvr, gvk(c.resource.Group, c.resource.Version, c.resource.Kind), gvkUpdateOptions, v1beta1.Update, obj.GetName(), obj.GetNamespace(), true, true, true)
 | 
						|
	_, err = c.client.Resource(c.gvr).Namespace(obj.GetNamespace()).Patch(
 | 
						|
		context.TODO(),
 | 
						|
		obj.GetName(),
 | 
						|
		types.MergePatchType,
 | 
						|
		[]byte(`{"metadata":{"annotations":{"patch":"true"}}}`),
 | 
						|
		metav1.PatchOptions{})
 | 
						|
	if err != nil {
 | 
						|
		c.t.Error(err)
 | 
						|
		return
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func testResourceDelete(c *testContext) {
 | 
						|
	// Verify that an immediate delete triggers the webhook and populates the admisssionRequest.oldObject.
 | 
						|
	obj, err := createOrGetResource(c.client, c.gvr, c.resource)
 | 
						|
	if err != nil {
 | 
						|
		c.t.Error(err)
 | 
						|
		return
 | 
						|
	}
 | 
						|
	background := metav1.DeletePropagationBackground
 | 
						|
	zero := int64(0)
 | 
						|
	c.admissionHolder.expect(c.gvr, gvk(c.resource.Group, c.resource.Version, c.resource.Kind), gvkDeleteOptions, v1beta1.Delete, obj.GetName(), obj.GetNamespace(), false, true, true)
 | 
						|
	err = c.client.Resource(c.gvr).Namespace(obj.GetNamespace()).Delete(context.TODO(), obj.GetName(), metav1.DeleteOptions{GracePeriodSeconds: &zero, PropagationPolicy: &background})
 | 
						|
	if err != nil {
 | 
						|
		c.t.Error(err)
 | 
						|
		return
 | 
						|
	}
 | 
						|
	c.admissionHolder.verify(c.t)
 | 
						|
 | 
						|
	// wait for the item to be gone
 | 
						|
	err = wait.PollImmediate(100*time.Millisecond, 10*time.Second, func() (bool, error) {
 | 
						|
		obj, err := c.client.Resource(c.gvr).Namespace(obj.GetNamespace()).Get(context.TODO(), obj.GetName(), metav1.GetOptions{})
 | 
						|
		if apierrors.IsNotFound(err) {
 | 
						|
			return true, nil
 | 
						|
		}
 | 
						|
		if err == nil {
 | 
						|
			c.t.Logf("waiting for %#v to be deleted (name: %s, finalizers: %v)...\n", c.gvr, obj.GetName(), obj.GetFinalizers())
 | 
						|
			return false, nil
 | 
						|
		}
 | 
						|
		return false, err
 | 
						|
	})
 | 
						|
	if err != nil {
 | 
						|
		c.t.Error(err)
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	// Verify that an update-on-delete triggers the webhook and populates the admisssionRequest.oldObject.
 | 
						|
	obj, err = createOrGetResource(c.client, c.gvr, c.resource)
 | 
						|
	if err != nil {
 | 
						|
		c.t.Error(err)
 | 
						|
		return
 | 
						|
	}
 | 
						|
	// Adding finalizer to the object, then deleting it.
 | 
						|
	// We don't add finalizers by setting DeleteOptions.PropagationPolicy
 | 
						|
	// because some resource (e.g., events) do not support garbage
 | 
						|
	// collector finalizers.
 | 
						|
	_, err = c.client.Resource(c.gvr).Namespace(obj.GetNamespace()).Patch(
 | 
						|
		context.TODO(),
 | 
						|
		obj.GetName(),
 | 
						|
		types.MergePatchType,
 | 
						|
		[]byte(`{"metadata":{"finalizers":["test/k8s.io"]}}`),
 | 
						|
		metav1.PatchOptions{})
 | 
						|
	if err != nil {
 | 
						|
		c.t.Error(err)
 | 
						|
		return
 | 
						|
	}
 | 
						|
	c.admissionHolder.expect(c.gvr, gvk(c.resource.Group, c.resource.Version, c.resource.Kind), gvkDeleteOptions, v1beta1.Delete, obj.GetName(), obj.GetNamespace(), false, true, true)
 | 
						|
	err = c.client.Resource(c.gvr).Namespace(obj.GetNamespace()).Delete(context.TODO(), obj.GetName(), metav1.DeleteOptions{GracePeriodSeconds: &zero, PropagationPolicy: &background})
 | 
						|
	if err != nil {
 | 
						|
		c.t.Error(err)
 | 
						|
		return
 | 
						|
	}
 | 
						|
	c.admissionHolder.verify(c.t)
 | 
						|
 | 
						|
	// wait other finalizers (e.g., crd's customresourcecleanup finalizer) to be removed.
 | 
						|
	err = wait.PollImmediate(100*time.Millisecond, 10*time.Second, func() (bool, error) {
 | 
						|
		obj, err := c.client.Resource(c.gvr).Namespace(obj.GetNamespace()).Get(context.TODO(), obj.GetName(), metav1.GetOptions{})
 | 
						|
		if err != nil {
 | 
						|
			return false, err
 | 
						|
		}
 | 
						|
		finalizers := obj.GetFinalizers()
 | 
						|
		if len(finalizers) != 1 {
 | 
						|
			c.t.Logf("waiting for other finalizers on %#v %s to be removed, existing finalizers are %v", c.gvr, obj.GetName(), obj.GetFinalizers())
 | 
						|
			return false, nil
 | 
						|
		}
 | 
						|
		if finalizers[0] != "test/k8s.io" {
 | 
						|
			return false, fmt.Errorf("expected the single finalizer on %#v %s to be test/k8s.io, got %v", c.gvr, obj.GetName(), obj.GetFinalizers())
 | 
						|
		}
 | 
						|
		return true, nil
 | 
						|
	})
 | 
						|
	if err != nil {
 | 
						|
		c.t.Error(err)
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	// remove the finalizer
 | 
						|
	_, err = c.client.Resource(c.gvr).Namespace(obj.GetNamespace()).Patch(
 | 
						|
		context.TODO(),
 | 
						|
		obj.GetName(),
 | 
						|
		types.MergePatchType,
 | 
						|
		[]byte(`{"metadata":{"finalizers":[]}}`),
 | 
						|
		metav1.PatchOptions{})
 | 
						|
	if err != nil {
 | 
						|
		c.t.Error(err)
 | 
						|
		return
 | 
						|
	}
 | 
						|
	// wait for the item to be gone
 | 
						|
	err = wait.PollImmediate(100*time.Millisecond, 10*time.Second, func() (bool, error) {
 | 
						|
		obj, err := c.client.Resource(c.gvr).Namespace(obj.GetNamespace()).Get(context.TODO(), obj.GetName(), metav1.GetOptions{})
 | 
						|
		if apierrors.IsNotFound(err) {
 | 
						|
			return true, nil
 | 
						|
		}
 | 
						|
		if err == nil {
 | 
						|
			c.t.Logf("waiting for %#v to be deleted (name: %s, finalizers: %v)...\n", c.gvr, obj.GetName(), obj.GetFinalizers())
 | 
						|
			return false, nil
 | 
						|
		}
 | 
						|
		return false, err
 | 
						|
	})
 | 
						|
	if err != nil {
 | 
						|
		c.t.Error(err)
 | 
						|
		return
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func testResourceDeletecollection(c *testContext) {
 | 
						|
	obj, err := createOrGetResource(c.client, c.gvr, c.resource)
 | 
						|
	if err != nil {
 | 
						|
		c.t.Error(err)
 | 
						|
		return
 | 
						|
	}
 | 
						|
	background := metav1.DeletePropagationBackground
 | 
						|
	zero := int64(0)
 | 
						|
 | 
						|
	// update the object with a label that matches our selector
 | 
						|
	_, err = c.client.Resource(c.gvr).Namespace(obj.GetNamespace()).Patch(
 | 
						|
		context.TODO(),
 | 
						|
		obj.GetName(),
 | 
						|
		types.MergePatchType,
 | 
						|
		[]byte(`{"metadata":{"labels":{"webhooktest":"true"}}}`),
 | 
						|
		metav1.PatchOptions{})
 | 
						|
	if err != nil {
 | 
						|
		c.t.Error(err)
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	// set expectations
 | 
						|
	c.admissionHolder.expect(c.gvr, gvk(c.resource.Group, c.resource.Version, c.resource.Kind), gvkDeleteOptions, v1beta1.Delete, "", obj.GetNamespace(), false, true, true)
 | 
						|
 | 
						|
	// delete
 | 
						|
	err = c.client.Resource(c.gvr).Namespace(obj.GetNamespace()).DeleteCollection(context.TODO(), metav1.DeleteOptions{GracePeriodSeconds: &zero, PropagationPolicy: &background}, metav1.ListOptions{LabelSelector: "webhooktest=true"})
 | 
						|
	if err != nil {
 | 
						|
		c.t.Error(err)
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	// wait for the item to be gone
 | 
						|
	err = wait.PollImmediate(100*time.Millisecond, 10*time.Second, func() (bool, error) {
 | 
						|
		obj, err := c.client.Resource(c.gvr).Namespace(obj.GetNamespace()).Get(context.TODO(), obj.GetName(), metav1.GetOptions{})
 | 
						|
		if apierrors.IsNotFound(err) {
 | 
						|
			return true, nil
 | 
						|
		}
 | 
						|
		if err == nil {
 | 
						|
			c.t.Logf("waiting for %#v to be deleted (name: %s, finalizers: %v)...\n", c.gvr, obj.GetName(), obj.GetFinalizers())
 | 
						|
			return false, nil
 | 
						|
		}
 | 
						|
		return false, err
 | 
						|
	})
 | 
						|
	if err != nil {
 | 
						|
		c.t.Error(err)
 | 
						|
		return
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func getParentGVR(gvr schema.GroupVersionResource) schema.GroupVersionResource {
 | 
						|
	parentGVR, found := parentResources[gvr]
 | 
						|
	// if no special override is found, just drop the subresource
 | 
						|
	if !found {
 | 
						|
		parentGVR = gvr
 | 
						|
		parentGVR.Resource = strings.Split(parentGVR.Resource, "/")[0]
 | 
						|
	}
 | 
						|
	return parentGVR
 | 
						|
}
 | 
						|
 | 
						|
func testSubresourceUpdate(c *testContext) {
 | 
						|
	if err := retry.RetryOnConflict(retry.DefaultBackoff, func() error {
 | 
						|
		parentGVR := getParentGVR(c.gvr)
 | 
						|
		parentResource := c.resources[parentGVR]
 | 
						|
		obj, err := createOrGetResource(c.client, parentGVR, parentResource)
 | 
						|
		if err != nil {
 | 
						|
			return err
 | 
						|
		}
 | 
						|
 | 
						|
		// Save the parent object as what we submit
 | 
						|
		submitObj := obj
 | 
						|
 | 
						|
		gvrWithoutSubresources := c.gvr
 | 
						|
		gvrWithoutSubresources.Resource = strings.Split(gvrWithoutSubresources.Resource, "/")[0]
 | 
						|
		subresources := strings.Split(c.gvr.Resource, "/")[1:]
 | 
						|
 | 
						|
		// If the subresource supports get, fetch that as the object to submit (namespaces/finalize, */scale, etc)
 | 
						|
		if sets.NewString(c.resource.Verbs...).Has("get") {
 | 
						|
			submitObj, err = c.client.Resource(gvrWithoutSubresources).Namespace(obj.GetNamespace()).Get(context.TODO(), obj.GetName(), metav1.GetOptions{}, subresources...)
 | 
						|
			if err != nil {
 | 
						|
				return err
 | 
						|
			}
 | 
						|
		}
 | 
						|
 | 
						|
		// Modify the object
 | 
						|
		submitObj.SetAnnotations(map[string]string{"subresourceupdate": "true"})
 | 
						|
 | 
						|
		// set expectations
 | 
						|
		c.admissionHolder.expect(c.gvr, gvk(c.resource.Group, c.resource.Version, c.resource.Kind), gvkUpdateOptions, v1beta1.Update, obj.GetName(), obj.GetNamespace(), true, true, true)
 | 
						|
 | 
						|
		_, err = c.client.Resource(gvrWithoutSubresources).Namespace(obj.GetNamespace()).Update(
 | 
						|
			context.TODO(),
 | 
						|
			submitObj,
 | 
						|
			metav1.UpdateOptions{},
 | 
						|
			subresources...,
 | 
						|
		)
 | 
						|
		return err
 | 
						|
	}); err != nil {
 | 
						|
		c.t.Error(err)
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func testSubresourcePatch(c *testContext) {
 | 
						|
	parentGVR := getParentGVR(c.gvr)
 | 
						|
	parentResource := c.resources[parentGVR]
 | 
						|
	obj, err := createOrGetResource(c.client, parentGVR, parentResource)
 | 
						|
	if err != nil {
 | 
						|
		c.t.Error(err)
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	gvrWithoutSubresources := c.gvr
 | 
						|
	gvrWithoutSubresources.Resource = strings.Split(gvrWithoutSubresources.Resource, "/")[0]
 | 
						|
	subresources := strings.Split(c.gvr.Resource, "/")[1:]
 | 
						|
 | 
						|
	// set expectations
 | 
						|
	c.admissionHolder.expect(c.gvr, gvk(c.resource.Group, c.resource.Version, c.resource.Kind), gvkUpdateOptions, v1beta1.Update, obj.GetName(), obj.GetNamespace(), true, true, true)
 | 
						|
 | 
						|
	_, err = c.client.Resource(gvrWithoutSubresources).Namespace(obj.GetNamespace()).Patch(
 | 
						|
		context.TODO(),
 | 
						|
		obj.GetName(),
 | 
						|
		types.MergePatchType,
 | 
						|
		[]byte(`{"metadata":{"annotations":{"subresourcepatch":"true"}}}`),
 | 
						|
		metav1.PatchOptions{},
 | 
						|
		subresources...,
 | 
						|
	)
 | 
						|
	if err != nil {
 | 
						|
		c.t.Error(err)
 | 
						|
		return
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func unimplemented(c *testContext) {
 | 
						|
	c.t.Errorf("Test function for %+v has not been implemented...", c.gvr)
 | 
						|
}
 | 
						|
 | 
						|
//
 | 
						|
// custom methods
 | 
						|
//
 | 
						|
 | 
						|
// testNamespaceDelete verifies namespace-specific delete behavior:
 | 
						|
// - ensures admission is called on first delete (which only sets deletionTimestamp and terminating state)
 | 
						|
// - removes finalizer from namespace
 | 
						|
// - ensures admission is called on final delete once finalizers are removed
 | 
						|
func testNamespaceDelete(c *testContext) {
 | 
						|
	obj, err := createOrGetResource(c.client, c.gvr, c.resource)
 | 
						|
	if err != nil {
 | 
						|
		c.t.Error(err)
 | 
						|
		return
 | 
						|
	}
 | 
						|
	background := metav1.DeletePropagationBackground
 | 
						|
	zero := int64(0)
 | 
						|
 | 
						|
	c.admissionHolder.expect(c.gvr, gvk(c.resource.Group, c.resource.Version, c.resource.Kind), gvkDeleteOptions, v1beta1.Delete, obj.GetName(), obj.GetNamespace(), false, true, true)
 | 
						|
	err = c.client.Resource(c.gvr).Namespace(obj.GetNamespace()).Delete(context.TODO(), obj.GetName(), metav1.DeleteOptions{GracePeriodSeconds: &zero, PropagationPolicy: &background})
 | 
						|
	if err != nil {
 | 
						|
		c.t.Error(err)
 | 
						|
		return
 | 
						|
	}
 | 
						|
	c.admissionHolder.verify(c.t)
 | 
						|
 | 
						|
	// do the finalization so the namespace can be deleted
 | 
						|
	obj, err = c.client.Resource(c.gvr).Namespace(obj.GetNamespace()).Get(context.TODO(), obj.GetName(), metav1.GetOptions{})
 | 
						|
	if err != nil {
 | 
						|
		c.t.Error(err)
 | 
						|
		return
 | 
						|
	}
 | 
						|
	err = unstructured.SetNestedField(obj.Object, nil, "spec", "finalizers")
 | 
						|
	if err != nil {
 | 
						|
		c.t.Error(err)
 | 
						|
		return
 | 
						|
	}
 | 
						|
	_, err = c.client.Resource(c.gvr).Namespace(obj.GetNamespace()).Update(context.TODO(), obj, metav1.UpdateOptions{}, "finalize")
 | 
						|
	if err != nil {
 | 
						|
		c.t.Error(err)
 | 
						|
		return
 | 
						|
	}
 | 
						|
	// verify namespace is gone
 | 
						|
	obj, err = c.client.Resource(c.gvr).Namespace(obj.GetNamespace()).Get(context.TODO(), obj.GetName(), metav1.GetOptions{})
 | 
						|
	if err == nil || !apierrors.IsNotFound(err) {
 | 
						|
		c.t.Errorf("expected namespace to be gone, got %#v, %v", obj, err)
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
// testDeploymentRollback verifies rollback-specific behavior:
 | 
						|
// - creates a parent deployment
 | 
						|
// - creates a rollback object and posts it
 | 
						|
func testDeploymentRollback(c *testContext) {
 | 
						|
	deploymentGVR := gvr("apps", "v1", "deployments")
 | 
						|
	obj, err := createOrGetResource(c.client, deploymentGVR, c.resources[deploymentGVR])
 | 
						|
	if err != nil {
 | 
						|
		c.t.Error(err)
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	gvrWithoutSubresources := c.gvr
 | 
						|
	gvrWithoutSubresources.Resource = strings.Split(gvrWithoutSubresources.Resource, "/")[0]
 | 
						|
	subresources := strings.Split(c.gvr.Resource, "/")[1:]
 | 
						|
 | 
						|
	c.admissionHolder.expect(c.gvr, gvk(c.resource.Group, c.resource.Version, c.resource.Kind), gvkCreateOptions, v1beta1.Create, obj.GetName(), obj.GetNamespace(), true, false, true)
 | 
						|
 | 
						|
	var rollbackObj runtime.Object
 | 
						|
	switch c.gvr {
 | 
						|
	case gvr("apps", "v1beta1", "deployments/rollback"):
 | 
						|
		rollbackObj = &appsv1beta1.DeploymentRollback{
 | 
						|
			TypeMeta:   metav1.TypeMeta{APIVersion: "apps/v1beta1", Kind: "DeploymentRollback"},
 | 
						|
			Name:       obj.GetName(),
 | 
						|
			RollbackTo: appsv1beta1.RollbackConfig{Revision: 0},
 | 
						|
		}
 | 
						|
	case gvr("extensions", "v1beta1", "deployments/rollback"):
 | 
						|
		rollbackObj = &extensionsv1beta1.DeploymentRollback{
 | 
						|
			TypeMeta:   metav1.TypeMeta{APIVersion: "extensions/v1beta1", Kind: "DeploymentRollback"},
 | 
						|
			Name:       obj.GetName(),
 | 
						|
			RollbackTo: extensionsv1beta1.RollbackConfig{Revision: 0},
 | 
						|
		}
 | 
						|
	default:
 | 
						|
		c.t.Errorf("unknown rollback resource %#v", c.gvr)
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	rollbackUnstructuredBody, err := runtime.DefaultUnstructuredConverter.ToUnstructured(rollbackObj)
 | 
						|
	if err != nil {
 | 
						|
		c.t.Errorf("ToUnstructured failed: %v", err)
 | 
						|
		return
 | 
						|
	}
 | 
						|
	rollbackUnstructuredObj := &unstructured.Unstructured{Object: rollbackUnstructuredBody}
 | 
						|
	rollbackUnstructuredObj.SetName(obj.GetName())
 | 
						|
 | 
						|
	_, err = c.client.Resource(gvrWithoutSubresources).Namespace(obj.GetNamespace()).Create(context.TODO(), rollbackUnstructuredObj, metav1.CreateOptions{}, subresources...)
 | 
						|
	if err != nil {
 | 
						|
		c.t.Error(err)
 | 
						|
		return
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
// testPodConnectSubresource verifies connect subresources
 | 
						|
func testPodConnectSubresource(c *testContext) {
 | 
						|
	podGVR := gvr("", "v1", "pods")
 | 
						|
	pod, err := createOrGetResource(c.client, podGVR, c.resources[podGVR])
 | 
						|
	if err != nil {
 | 
						|
		c.t.Error(err)
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	// check all upgradeable verbs
 | 
						|
	for _, httpMethod := range []string{"GET", "POST"} {
 | 
						|
		c.t.Logf("verifying %v", httpMethod)
 | 
						|
 | 
						|
		c.admissionHolder.expect(c.gvr, gvk(c.resource.Group, c.resource.Version, c.resource.Kind), schema.GroupVersionKind{}, v1beta1.Connect, pod.GetName(), pod.GetNamespace(), true, false, false)
 | 
						|
		var err error
 | 
						|
		switch c.gvr {
 | 
						|
		case gvr("", "v1", "pods/exec"):
 | 
						|
			err = c.clientset.CoreV1().RESTClient().Verb(httpMethod).Namespace(pod.GetNamespace()).Resource("pods").Name(pod.GetName()).SubResource("exec").Do(context.TODO()).Error()
 | 
						|
		case gvr("", "v1", "pods/attach"):
 | 
						|
			err = c.clientset.CoreV1().RESTClient().Verb(httpMethod).Namespace(pod.GetNamespace()).Resource("pods").Name(pod.GetName()).SubResource("attach").Do(context.TODO()).Error()
 | 
						|
		case gvr("", "v1", "pods/portforward"):
 | 
						|
			err = c.clientset.CoreV1().RESTClient().Verb(httpMethod).Namespace(pod.GetNamespace()).Resource("pods").Name(pod.GetName()).SubResource("portforward").Do(context.TODO()).Error()
 | 
						|
		default:
 | 
						|
			c.t.Errorf("unknown subresource %#v", c.gvr)
 | 
						|
			return
 | 
						|
		}
 | 
						|
 | 
						|
		if err != nil {
 | 
						|
			c.t.Logf("debug: result of subresource connect: %v", err)
 | 
						|
		}
 | 
						|
		c.admissionHolder.verify(c.t)
 | 
						|
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
// testPodBindingEviction verifies pod binding and eviction admission
 | 
						|
func testPodBindingEviction(c *testContext) {
 | 
						|
	podGVR := gvr("", "v1", "pods")
 | 
						|
	pod, err := createOrGetResource(c.client, podGVR, c.resources[podGVR])
 | 
						|
	if err != nil {
 | 
						|
		c.t.Error(err)
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	background := metav1.DeletePropagationBackground
 | 
						|
	zero := int64(0)
 | 
						|
	forceDelete := metav1.DeleteOptions{GracePeriodSeconds: &zero, PropagationPolicy: &background}
 | 
						|
	defer func() {
 | 
						|
		err := c.clientset.CoreV1().Pods(pod.GetNamespace()).Delete(context.TODO(), pod.GetName(), forceDelete)
 | 
						|
		if err != nil && !apierrors.IsNotFound(err) {
 | 
						|
			c.t.Error(err)
 | 
						|
			return
 | 
						|
		}
 | 
						|
	}()
 | 
						|
 | 
						|
	c.admissionHolder.expect(c.gvr, gvk(c.resource.Group, c.resource.Version, c.resource.Kind), gvkCreateOptions, v1beta1.Create, pod.GetName(), pod.GetNamespace(), true, false, true)
 | 
						|
 | 
						|
	switch c.gvr {
 | 
						|
	case gvr("", "v1", "bindings"):
 | 
						|
		err = c.clientset.CoreV1().RESTClient().Post().Namespace(pod.GetNamespace()).Resource("bindings").Body(&corev1.Binding{
 | 
						|
			ObjectMeta: metav1.ObjectMeta{Name: pod.GetName()},
 | 
						|
			Target:     corev1.ObjectReference{Name: "foo", Kind: "Node", APIVersion: "v1"},
 | 
						|
		}).Do(context.TODO()).Error()
 | 
						|
 | 
						|
	case gvr("", "v1", "pods/binding"):
 | 
						|
		err = c.clientset.CoreV1().RESTClient().Post().Namespace(pod.GetNamespace()).Resource("pods").Name(pod.GetName()).SubResource("binding").Body(&corev1.Binding{
 | 
						|
			ObjectMeta: metav1.ObjectMeta{Name: pod.GetName()},
 | 
						|
			Target:     corev1.ObjectReference{Name: "foo", Kind: "Node", APIVersion: "v1"},
 | 
						|
		}).Do(context.TODO()).Error()
 | 
						|
 | 
						|
	case gvr("", "v1", "pods/eviction"):
 | 
						|
		err = c.clientset.CoreV1().RESTClient().Post().Namespace(pod.GetNamespace()).Resource("pods").Name(pod.GetName()).SubResource("eviction").Body(&policyv1beta1.Eviction{
 | 
						|
			ObjectMeta:    metav1.ObjectMeta{Name: pod.GetName()},
 | 
						|
			DeleteOptions: &forceDelete,
 | 
						|
		}).Do(context.TODO()).Error()
 | 
						|
 | 
						|
	default:
 | 
						|
		c.t.Errorf("unhandled resource %#v", c.gvr)
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	if err != nil {
 | 
						|
		c.t.Error(err)
 | 
						|
		return
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
// testSubresourceProxy verifies proxy subresources
 | 
						|
func testSubresourceProxy(c *testContext) {
 | 
						|
	parentGVR := getParentGVR(c.gvr)
 | 
						|
	parentResource := c.resources[parentGVR]
 | 
						|
	obj, err := createOrGetResource(c.client, parentGVR, parentResource)
 | 
						|
	if err != nil {
 | 
						|
		c.t.Error(err)
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	gvrWithoutSubresources := c.gvr
 | 
						|
	gvrWithoutSubresources.Resource = strings.Split(gvrWithoutSubresources.Resource, "/")[0]
 | 
						|
	subresources := strings.Split(c.gvr.Resource, "/")[1:]
 | 
						|
 | 
						|
	verbToHTTPMethods := map[string][]string{
 | 
						|
		"create": {"POST", "GET", "HEAD", "OPTIONS"}, // also test read-only verbs map to Connect admission
 | 
						|
		"update": {"PUT"},
 | 
						|
		"patch":  {"PATCH"},
 | 
						|
		"delete": {"DELETE"},
 | 
						|
	}
 | 
						|
	httpMethodsToTest, ok := verbToHTTPMethods[c.verb]
 | 
						|
	if !ok {
 | 
						|
		c.t.Errorf("unknown verb %v", c.verb)
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	for _, httpMethod := range httpMethodsToTest {
 | 
						|
		c.t.Logf("testing %v", httpMethod)
 | 
						|
		request := c.clientset.CoreV1().RESTClient().Verb(httpMethod)
 | 
						|
 | 
						|
		// add the namespace if required
 | 
						|
		if len(obj.GetNamespace()) > 0 {
 | 
						|
			request = request.Namespace(obj.GetNamespace())
 | 
						|
		}
 | 
						|
 | 
						|
		// set expectations
 | 
						|
		c.admissionHolder.expect(c.gvr, gvk(c.resource.Group, c.resource.Version, c.resource.Kind), schema.GroupVersionKind{}, v1beta1.Connect, obj.GetName(), obj.GetNamespace(), true, false, false)
 | 
						|
		// run the request. we don't actually care if the request is successful, just that admission gets called as expected
 | 
						|
		err = request.Resource(gvrWithoutSubresources.Resource).Name(obj.GetName()).SubResource(subresources...).Do(context.TODO()).Error()
 | 
						|
		if err != nil {
 | 
						|
			c.t.Logf("debug: result of subresource proxy (error expected): %v", err)
 | 
						|
		}
 | 
						|
		// verify the result
 | 
						|
		c.admissionHolder.verify(c.t)
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func testPruningRandomNumbers(c *testContext) {
 | 
						|
	testResourceCreate(c)
 | 
						|
 | 
						|
	cr2pant, err := c.client.Resource(c.gvr).Get(context.TODO(), "fortytwo", metav1.GetOptions{})
 | 
						|
	if err != nil {
 | 
						|
		c.t.Error(err)
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	foo, found, err := unstructured.NestedString(cr2pant.Object, "foo")
 | 
						|
	if err != nil {
 | 
						|
		c.t.Error(err)
 | 
						|
		return
 | 
						|
	}
 | 
						|
	if found {
 | 
						|
		c.t.Errorf("expected .foo to be pruned, but got: %s", foo)
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func testNoPruningCustomFancy(c *testContext) {
 | 
						|
	testResourceCreate(c)
 | 
						|
 | 
						|
	cr2pant, err := c.client.Resource(c.gvr).Get(context.TODO(), "cr2pant", metav1.GetOptions{})
 | 
						|
	if err != nil {
 | 
						|
		c.t.Error(err)
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	foo, _, err := unstructured.NestedString(cr2pant.Object, "foo")
 | 
						|
	if err != nil {
 | 
						|
		c.t.Error(err)
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	// check that no pruning took place
 | 
						|
	if expected, got := "test", foo; expected != got {
 | 
						|
		c.t.Errorf("expected /foo to be %q, got: %q", expected, got)
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
//
 | 
						|
// utility methods
 | 
						|
//
 | 
						|
 | 
						|
func newV1beta1WebhookHandler(t *testing.T, holder *holder, phase string, converted bool) http.Handler {
 | 
						|
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 | 
						|
		defer r.Body.Close()
 | 
						|
		data, err := ioutil.ReadAll(r.Body)
 | 
						|
		if err != nil {
 | 
						|
			t.Error(err)
 | 
						|
			return
 | 
						|
		}
 | 
						|
 | 
						|
		if contentType := r.Header.Get("Content-Type"); contentType != "application/json" {
 | 
						|
			t.Errorf("contentType=%s, expect application/json", contentType)
 | 
						|
			return
 | 
						|
		}
 | 
						|
 | 
						|
		review := v1beta1.AdmissionReview{}
 | 
						|
		if err := json.Unmarshal(data, &review); err != nil {
 | 
						|
			t.Errorf("Fail to deserialize object: %s with error: %v", string(data), err)
 | 
						|
			http.Error(w, err.Error(), 400)
 | 
						|
			return
 | 
						|
		}
 | 
						|
 | 
						|
		if review.GetObjectKind().GroupVersionKind() != gvk("admission.k8s.io", "v1beta1", "AdmissionReview") {
 | 
						|
			t.Errorf("Invalid admission review kind: %#v", review.GetObjectKind().GroupVersionKind())
 | 
						|
			http.Error(w, err.Error(), 400)
 | 
						|
			return
 | 
						|
		}
 | 
						|
 | 
						|
		if len(review.Request.Object.Raw) > 0 {
 | 
						|
			u := &unstructured.Unstructured{Object: map[string]interface{}{}}
 | 
						|
			if err := json.Unmarshal(review.Request.Object.Raw, u); err != nil {
 | 
						|
				t.Errorf("Fail to deserialize object: %s with error: %v", string(review.Request.Object.Raw), err)
 | 
						|
				http.Error(w, err.Error(), 400)
 | 
						|
				return
 | 
						|
			}
 | 
						|
			review.Request.Object.Object = u
 | 
						|
		}
 | 
						|
		if len(review.Request.OldObject.Raw) > 0 {
 | 
						|
			u := &unstructured.Unstructured{Object: map[string]interface{}{}}
 | 
						|
			if err := json.Unmarshal(review.Request.OldObject.Raw, u); err != nil {
 | 
						|
				t.Errorf("Fail to deserialize object: %s with error: %v", string(review.Request.OldObject.Raw), err)
 | 
						|
				http.Error(w, err.Error(), 400)
 | 
						|
				return
 | 
						|
			}
 | 
						|
			review.Request.OldObject.Object = u
 | 
						|
		}
 | 
						|
 | 
						|
		if len(review.Request.Options.Raw) > 0 {
 | 
						|
			u := &unstructured.Unstructured{Object: map[string]interface{}{}}
 | 
						|
			if err := json.Unmarshal(review.Request.Options.Raw, u); err != nil {
 | 
						|
				t.Errorf("Fail to deserialize options object: %s for admission request %#+v with error: %v", string(review.Request.Options.Raw), review.Request, err)
 | 
						|
				http.Error(w, err.Error(), 400)
 | 
						|
				return
 | 
						|
			}
 | 
						|
			review.Request.Options.Object = u
 | 
						|
		}
 | 
						|
 | 
						|
		if review.Request.UserInfo.Username == testClientUsername {
 | 
						|
			// only record requests originating from this integration test's client
 | 
						|
			reviewRequest := &admissionRequest{
 | 
						|
				Operation:   string(review.Request.Operation),
 | 
						|
				Resource:    review.Request.Resource,
 | 
						|
				SubResource: review.Request.SubResource,
 | 
						|
				Namespace:   review.Request.Namespace,
 | 
						|
				Name:        review.Request.Name,
 | 
						|
				Object:      review.Request.Object,
 | 
						|
				OldObject:   review.Request.OldObject,
 | 
						|
				Options:     review.Request.Options,
 | 
						|
			}
 | 
						|
			holder.record("v1beta1", phase, converted, reviewRequest)
 | 
						|
		}
 | 
						|
 | 
						|
		review.Response = &v1beta1.AdmissionResponse{
 | 
						|
			Allowed: true,
 | 
						|
			Result:  &metav1.Status{Message: "admitted"},
 | 
						|
		}
 | 
						|
 | 
						|
		// v1beta1 webhook handler tolerated these not being set. verify the server continues to accept these as unset.
 | 
						|
		review.APIVersion = ""
 | 
						|
		review.Kind = ""
 | 
						|
		review.Response.UID = ""
 | 
						|
 | 
						|
		// test plumbing warnings back to the client
 | 
						|
		review.Response.Warnings = []string{makeWarning("v1beta1", phase, converted)}
 | 
						|
 | 
						|
		// If we're mutating, and have an object, return a patch to exercise conversion
 | 
						|
		if phase == mutation && len(review.Request.Object.Raw) > 0 {
 | 
						|
			review.Response.Patch = []byte(`[{"op":"add","path":"/foo","value":"test"}]`)
 | 
						|
			jsonPatch := v1beta1.PatchTypeJSONPatch
 | 
						|
			review.Response.PatchType = &jsonPatch
 | 
						|
		}
 | 
						|
 | 
						|
		w.Header().Set("Content-Type", "application/json")
 | 
						|
		if err := json.NewEncoder(w).Encode(review); err != nil {
 | 
						|
			t.Errorf("Marshal of response failed with error: %v", err)
 | 
						|
		}
 | 
						|
	})
 | 
						|
}
 | 
						|
 | 
						|
func newV1WebhookHandler(t *testing.T, holder *holder, phase string, converted bool) http.Handler {
 | 
						|
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 | 
						|
		defer r.Body.Close()
 | 
						|
		data, err := ioutil.ReadAll(r.Body)
 | 
						|
		if err != nil {
 | 
						|
			t.Error(err)
 | 
						|
			return
 | 
						|
		}
 | 
						|
 | 
						|
		if contentType := r.Header.Get("Content-Type"); contentType != "application/json" {
 | 
						|
			t.Errorf("contentType=%s, expect application/json", contentType)
 | 
						|
			return
 | 
						|
		}
 | 
						|
 | 
						|
		review := admissionreviewv1.AdmissionReview{}
 | 
						|
		if err := json.Unmarshal(data, &review); err != nil {
 | 
						|
			t.Errorf("Fail to deserialize object: %s with error: %v", string(data), err)
 | 
						|
			http.Error(w, err.Error(), 400)
 | 
						|
			return
 | 
						|
		}
 | 
						|
 | 
						|
		if review.GetObjectKind().GroupVersionKind() != gvk("admission.k8s.io", "v1", "AdmissionReview") {
 | 
						|
			err := fmt.Errorf("Invalid admission review kind: %#v", review.GetObjectKind().GroupVersionKind())
 | 
						|
			t.Error(err)
 | 
						|
			http.Error(w, err.Error(), 400)
 | 
						|
			return
 | 
						|
		}
 | 
						|
 | 
						|
		if len(review.Request.Object.Raw) > 0 {
 | 
						|
			u := &unstructured.Unstructured{Object: map[string]interface{}{}}
 | 
						|
			if err := json.Unmarshal(review.Request.Object.Raw, u); err != nil {
 | 
						|
				t.Errorf("Fail to deserialize object: %s with error: %v", string(review.Request.Object.Raw), err)
 | 
						|
				http.Error(w, err.Error(), 400)
 | 
						|
				return
 | 
						|
			}
 | 
						|
			review.Request.Object.Object = u
 | 
						|
		}
 | 
						|
		if len(review.Request.OldObject.Raw) > 0 {
 | 
						|
			u := &unstructured.Unstructured{Object: map[string]interface{}{}}
 | 
						|
			if err := json.Unmarshal(review.Request.OldObject.Raw, u); err != nil {
 | 
						|
				t.Errorf("Fail to deserialize object: %s with error: %v", string(review.Request.OldObject.Raw), err)
 | 
						|
				http.Error(w, err.Error(), 400)
 | 
						|
				return
 | 
						|
			}
 | 
						|
			review.Request.OldObject.Object = u
 | 
						|
		}
 | 
						|
 | 
						|
		if len(review.Request.Options.Raw) > 0 {
 | 
						|
			u := &unstructured.Unstructured{Object: map[string]interface{}{}}
 | 
						|
			if err := json.Unmarshal(review.Request.Options.Raw, u); err != nil {
 | 
						|
				t.Errorf("Fail to deserialize options object: %s for admission request %#+v with error: %v", string(review.Request.Options.Raw), review.Request, err)
 | 
						|
				http.Error(w, err.Error(), 400)
 | 
						|
				return
 | 
						|
			}
 | 
						|
			review.Request.Options.Object = u
 | 
						|
		}
 | 
						|
 | 
						|
		if review.Request.UserInfo.Username == testClientUsername {
 | 
						|
			// only record requests originating from this integration test's client
 | 
						|
			reviewRequest := &admissionRequest{
 | 
						|
				Operation:   string(review.Request.Operation),
 | 
						|
				Resource:    review.Request.Resource,
 | 
						|
				SubResource: review.Request.SubResource,
 | 
						|
				Namespace:   review.Request.Namespace,
 | 
						|
				Name:        review.Request.Name,
 | 
						|
				Object:      review.Request.Object,
 | 
						|
				OldObject:   review.Request.OldObject,
 | 
						|
				Options:     review.Request.Options,
 | 
						|
			}
 | 
						|
			holder.record("v1", phase, converted, reviewRequest)
 | 
						|
		}
 | 
						|
 | 
						|
		review.Response = &admissionreviewv1.AdmissionResponse{
 | 
						|
			Allowed: true,
 | 
						|
			UID:     review.Request.UID,
 | 
						|
			Result:  &metav1.Status{Message: "admitted"},
 | 
						|
 | 
						|
			// test plumbing warnings back
 | 
						|
			Warnings: []string{makeWarning("v1", phase, converted)},
 | 
						|
		}
 | 
						|
		// If we're mutating, and have an object, return a patch to exercise conversion
 | 
						|
		if phase == mutation && len(review.Request.Object.Raw) > 0 {
 | 
						|
			review.Response.Patch = []byte(`[{"op":"add","path":"/bar","value":"test"}]`)
 | 
						|
			jsonPatch := admissionreviewv1.PatchTypeJSONPatch
 | 
						|
			review.Response.PatchType = &jsonPatch
 | 
						|
		}
 | 
						|
 | 
						|
		w.Header().Set("Content-Type", "application/json")
 | 
						|
		if err := json.NewEncoder(w).Encode(review); err != nil {
 | 
						|
			t.Errorf("Marshal of response failed with error: %v", err)
 | 
						|
		}
 | 
						|
	})
 | 
						|
}
 | 
						|
 | 
						|
func getTestFunc(gvr schema.GroupVersionResource, verb string) testFunc {
 | 
						|
	if f, found := customTestFuncs[gvr][verb]; found {
 | 
						|
		return f
 | 
						|
	}
 | 
						|
	if f, found := customTestFuncs[gvr]["*"]; found {
 | 
						|
		return f
 | 
						|
	}
 | 
						|
	if strings.Contains(gvr.Resource, "/") {
 | 
						|
		if f, found := defaultSubresourceFuncs[verb]; found {
 | 
						|
			return f
 | 
						|
		}
 | 
						|
		return unimplemented
 | 
						|
	}
 | 
						|
	if f, found := defaultResourceFuncs[verb]; found {
 | 
						|
		return f
 | 
						|
	}
 | 
						|
	return unimplemented
 | 
						|
}
 | 
						|
 | 
						|
func getStubObj(gvr schema.GroupVersionResource, resource metav1.APIResource) (*unstructured.Unstructured, error) {
 | 
						|
	stub := ""
 | 
						|
	if data, ok := etcd.GetEtcdStorageDataForNamespace(testNamespace)[gvr]; ok {
 | 
						|
		stub = data.Stub
 | 
						|
	}
 | 
						|
	if data, ok := stubDataOverrides[gvr]; ok {
 | 
						|
		stub = data
 | 
						|
	}
 | 
						|
	if len(stub) == 0 {
 | 
						|
		return nil, fmt.Errorf("no stub data for %#v", gvr)
 | 
						|
	}
 | 
						|
 | 
						|
	stubObj := &unstructured.Unstructured{Object: map[string]interface{}{}}
 | 
						|
	if err := json.Unmarshal([]byte(stub), &stubObj.Object); err != nil {
 | 
						|
		return nil, fmt.Errorf("error unmarshaling stub for %#v: %v", gvr, err)
 | 
						|
	}
 | 
						|
	return stubObj, nil
 | 
						|
}
 | 
						|
 | 
						|
func createOrGetResource(client dynamic.Interface, gvr schema.GroupVersionResource, resource metav1.APIResource) (*unstructured.Unstructured, error) {
 | 
						|
	stubObj, err := getStubObj(gvr, resource)
 | 
						|
	if err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
	ns := ""
 | 
						|
	if resource.Namespaced {
 | 
						|
		ns = testNamespace
 | 
						|
	}
 | 
						|
	obj, err := client.Resource(gvr).Namespace(ns).Get(context.TODO(), stubObj.GetName(), metav1.GetOptions{})
 | 
						|
	if err == nil {
 | 
						|
		return obj, nil
 | 
						|
	}
 | 
						|
	if !apierrors.IsNotFound(err) {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
	return client.Resource(gvr).Namespace(ns).Create(context.TODO(), stubObj, metav1.CreateOptions{})
 | 
						|
}
 | 
						|
 | 
						|
func gvr(group, version, resource string) schema.GroupVersionResource {
 | 
						|
	return schema.GroupVersionResource{Group: group, Version: version, Resource: resource}
 | 
						|
}
 | 
						|
func gvk(group, version, kind string) schema.GroupVersionKind {
 | 
						|
	return schema.GroupVersionKind{Group: group, Version: version, Kind: kind}
 | 
						|
}
 | 
						|
 | 
						|
var (
 | 
						|
	gvkCreateOptions = metav1.SchemeGroupVersion.WithKind("CreateOptions")
 | 
						|
	gvkUpdateOptions = metav1.SchemeGroupVersion.WithKind("UpdateOptions")
 | 
						|
	gvkDeleteOptions = metav1.SchemeGroupVersion.WithKind("DeleteOptions")
 | 
						|
)
 | 
						|
 | 
						|
func shouldTestResource(gvr schema.GroupVersionResource, resource metav1.APIResource) bool {
 | 
						|
	return sets.NewString(resource.Verbs...).HasAny("create", "update", "patch", "connect", "delete", "deletecollection")
 | 
						|
}
 | 
						|
 | 
						|
func shouldTestResourceVerb(gvr schema.GroupVersionResource, resource metav1.APIResource, verb string) bool {
 | 
						|
	return sets.NewString(resource.Verbs...).Has(verb)
 | 
						|
}
 | 
						|
 | 
						|
//
 | 
						|
// webhook registration helpers
 | 
						|
//
 | 
						|
 | 
						|
func createV1beta1ValidationWebhook(client clientset.Interface, endpoint, convertedEndpoint string, convertedRules []admissionv1beta1.RuleWithOperations) error {
 | 
						|
	fail := admissionv1beta1.Fail
 | 
						|
	equivalent := admissionv1beta1.Equivalent
 | 
						|
	// Attaching Admission webhook to API server
 | 
						|
	_, err := client.AdmissionregistrationV1beta1().ValidatingWebhookConfigurations().Create(context.TODO(), &admissionv1beta1.ValidatingWebhookConfiguration{
 | 
						|
		ObjectMeta: metav1.ObjectMeta{Name: "admission.integration.test"},
 | 
						|
		Webhooks: []admissionv1beta1.ValidatingWebhook{
 | 
						|
			{
 | 
						|
				Name: "admission.integration.test",
 | 
						|
				ClientConfig: admissionv1beta1.WebhookClientConfig{
 | 
						|
					URL:      &endpoint,
 | 
						|
					CABundle: localhostCert,
 | 
						|
				},
 | 
						|
				Rules: []admissionv1beta1.RuleWithOperations{{
 | 
						|
					Operations: []admissionv1beta1.OperationType{admissionv1beta1.OperationAll},
 | 
						|
					Rule:       admissionv1beta1.Rule{APIGroups: []string{"*"}, APIVersions: []string{"*"}, Resources: []string{"*/*"}},
 | 
						|
				}},
 | 
						|
				FailurePolicy:           &fail,
 | 
						|
				AdmissionReviewVersions: []string{"v1beta1"},
 | 
						|
			},
 | 
						|
			{
 | 
						|
				Name: "admission.integration.testconversion",
 | 
						|
				ClientConfig: admissionv1beta1.WebhookClientConfig{
 | 
						|
					URL:      &convertedEndpoint,
 | 
						|
					CABundle: localhostCert,
 | 
						|
				},
 | 
						|
				Rules:                   convertedRules,
 | 
						|
				FailurePolicy:           &fail,
 | 
						|
				MatchPolicy:             &equivalent,
 | 
						|
				AdmissionReviewVersions: []string{"v1beta1"},
 | 
						|
			},
 | 
						|
		},
 | 
						|
	}, metav1.CreateOptions{})
 | 
						|
	return err
 | 
						|
}
 | 
						|
 | 
						|
func createV1beta1MutationWebhook(client clientset.Interface, endpoint, convertedEndpoint string, convertedRules []admissionv1beta1.RuleWithOperations) error {
 | 
						|
	fail := admissionv1beta1.Fail
 | 
						|
	equivalent := admissionv1beta1.Equivalent
 | 
						|
	// Attaching Mutation webhook to API server
 | 
						|
	_, err := client.AdmissionregistrationV1beta1().MutatingWebhookConfigurations().Create(context.TODO(), &admissionv1beta1.MutatingWebhookConfiguration{
 | 
						|
		ObjectMeta: metav1.ObjectMeta{Name: "mutation.integration.test"},
 | 
						|
		Webhooks: []admissionv1beta1.MutatingWebhook{
 | 
						|
			{
 | 
						|
				Name: "mutation.integration.test",
 | 
						|
				ClientConfig: admissionv1beta1.WebhookClientConfig{
 | 
						|
					URL:      &endpoint,
 | 
						|
					CABundle: localhostCert,
 | 
						|
				},
 | 
						|
				Rules: []admissionv1beta1.RuleWithOperations{{
 | 
						|
					Operations: []admissionv1beta1.OperationType{admissionv1beta1.OperationAll},
 | 
						|
					Rule:       admissionv1beta1.Rule{APIGroups: []string{"*"}, APIVersions: []string{"*"}, Resources: []string{"*/*"}},
 | 
						|
				}},
 | 
						|
				FailurePolicy:           &fail,
 | 
						|
				AdmissionReviewVersions: []string{"v1beta1"},
 | 
						|
			},
 | 
						|
			{
 | 
						|
				Name: "mutation.integration.testconversion",
 | 
						|
				ClientConfig: admissionv1beta1.WebhookClientConfig{
 | 
						|
					URL:      &convertedEndpoint,
 | 
						|
					CABundle: localhostCert,
 | 
						|
				},
 | 
						|
				Rules:                   convertedRules,
 | 
						|
				FailurePolicy:           &fail,
 | 
						|
				MatchPolicy:             &equivalent,
 | 
						|
				AdmissionReviewVersions: []string{"v1beta1"},
 | 
						|
			},
 | 
						|
		},
 | 
						|
	}, metav1.CreateOptions{})
 | 
						|
	return err
 | 
						|
}
 | 
						|
 | 
						|
func createV1ValidationWebhook(client clientset.Interface, endpoint, convertedEndpoint string, convertedRules []admissionv1.RuleWithOperations) error {
 | 
						|
	fail := admissionv1.Fail
 | 
						|
	equivalent := admissionv1.Equivalent
 | 
						|
	none := admissionv1.SideEffectClassNone
 | 
						|
	// Attaching Admission webhook to API server
 | 
						|
	_, err := client.AdmissionregistrationV1().ValidatingWebhookConfigurations().Create(context.TODO(), &admissionv1.ValidatingWebhookConfiguration{
 | 
						|
		ObjectMeta: metav1.ObjectMeta{Name: "admissionv1.integration.test"},
 | 
						|
		Webhooks: []admissionv1.ValidatingWebhook{
 | 
						|
			{
 | 
						|
				Name: "admissionv1.integration.test",
 | 
						|
				ClientConfig: admissionv1.WebhookClientConfig{
 | 
						|
					URL:      &endpoint,
 | 
						|
					CABundle: localhostCert,
 | 
						|
				},
 | 
						|
				Rules: []admissionv1.RuleWithOperations{{
 | 
						|
					Operations: []admissionv1.OperationType{admissionv1.OperationAll},
 | 
						|
					Rule:       admissionv1.Rule{APIGroups: []string{"*"}, APIVersions: []string{"*"}, Resources: []string{"*/*"}},
 | 
						|
				}},
 | 
						|
				FailurePolicy:           &fail,
 | 
						|
				AdmissionReviewVersions: []string{"v1", "v1beta1"},
 | 
						|
				SideEffects:             &none,
 | 
						|
			},
 | 
						|
			{
 | 
						|
				Name: "admissionv1.integration.testconversion",
 | 
						|
				ClientConfig: admissionv1.WebhookClientConfig{
 | 
						|
					URL:      &convertedEndpoint,
 | 
						|
					CABundle: localhostCert,
 | 
						|
				},
 | 
						|
				Rules:                   convertedRules,
 | 
						|
				FailurePolicy:           &fail,
 | 
						|
				MatchPolicy:             &equivalent,
 | 
						|
				AdmissionReviewVersions: []string{"v1", "v1beta1"},
 | 
						|
				SideEffects:             &none,
 | 
						|
			},
 | 
						|
		},
 | 
						|
	}, metav1.CreateOptions{})
 | 
						|
	return err
 | 
						|
}
 | 
						|
 | 
						|
func createV1MutationWebhook(client clientset.Interface, endpoint, convertedEndpoint string, convertedRules []admissionv1.RuleWithOperations) error {
 | 
						|
	fail := admissionv1.Fail
 | 
						|
	equivalent := admissionv1.Equivalent
 | 
						|
	none := admissionv1.SideEffectClassNone
 | 
						|
	// Attaching Mutation webhook to API server
 | 
						|
	_, err := client.AdmissionregistrationV1().MutatingWebhookConfigurations().Create(context.TODO(), &admissionv1.MutatingWebhookConfiguration{
 | 
						|
		ObjectMeta: metav1.ObjectMeta{Name: "mutationv1.integration.test"},
 | 
						|
		Webhooks: []admissionv1.MutatingWebhook{
 | 
						|
			{
 | 
						|
				Name: "mutationv1.integration.test",
 | 
						|
				ClientConfig: admissionv1.WebhookClientConfig{
 | 
						|
					URL:      &endpoint,
 | 
						|
					CABundle: localhostCert,
 | 
						|
				},
 | 
						|
				Rules: []admissionv1.RuleWithOperations{{
 | 
						|
					Operations: []admissionv1.OperationType{admissionv1.OperationAll},
 | 
						|
					Rule:       admissionv1.Rule{APIGroups: []string{"*"}, APIVersions: []string{"*"}, Resources: []string{"*/*"}},
 | 
						|
				}},
 | 
						|
				FailurePolicy:           &fail,
 | 
						|
				AdmissionReviewVersions: []string{"v1", "v1beta1"},
 | 
						|
				SideEffects:             &none,
 | 
						|
			},
 | 
						|
			{
 | 
						|
				Name: "mutationv1.integration.testconversion",
 | 
						|
				ClientConfig: admissionv1.WebhookClientConfig{
 | 
						|
					URL:      &convertedEndpoint,
 | 
						|
					CABundle: localhostCert,
 | 
						|
				},
 | 
						|
				Rules:                   convertedRules,
 | 
						|
				FailurePolicy:           &fail,
 | 
						|
				MatchPolicy:             &equivalent,
 | 
						|
				AdmissionReviewVersions: []string{"v1", "v1beta1"},
 | 
						|
				SideEffects:             &none,
 | 
						|
			},
 | 
						|
		},
 | 
						|
	}, metav1.CreateOptions{})
 | 
						|
	return err
 | 
						|
}
 | 
						|
 | 
						|
// localhostCert was generated from crypto/tls/generate_cert.go with the following command:
 | 
						|
//     go run generate_cert.go  --rsa-bits 2048 --host 127.0.0.1,::1,example.com --ca --start-date "Jan 1 00:00:00 1970" --duration=1000000h
 | 
						|
var localhostCert = []byte(`-----BEGIN CERTIFICATE-----
 | 
						|
MIIDGDCCAgCgAwIBAgIQTKCKn99d5HhQVCLln2Q+eTANBgkqhkiG9w0BAQsFADAS
 | 
						|
MRAwDgYDVQQKEwdBY21lIENvMCAXDTcwMDEwMTAwMDAwMFoYDzIwODQwMTI5MTYw
 | 
						|
MDAwWjASMRAwDgYDVQQKEwdBY21lIENvMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
 | 
						|
MIIBCgKCAQEA1Z5/aTwqY706M34tn60l8ZHkanWDl8mM1pYf4Q7qg3zA9XqWLX6S
 | 
						|
4rTYDYCb4stEasC72lQnbEWHbthiQE76zubP8WOFHdvGR3mjAvHWz4FxvLOTheZ+
 | 
						|
3iDUrl6Aj9UIsYqzmpBJAoY4+vGGf+xHvuukHrVcFqR9ZuBdZuJ/HbbjUyuNr3X9
 | 
						|
erNIr5Ha17gVzf17SNbYgNrX9gbCeEB8Z9Ox7dVuJhLDkpF0T/B5Zld3BjyUVY/T
 | 
						|
cukU4dTVp6isbWPvCMRCZCCOpb+qIhxEjJ0n6tnPt8nf9lvDl4SWMl6X1bH+2EFa
 | 
						|
a8R06G0QI+XhwPyjXUyCR8QEOZPCR5wyqQIDAQABo2gwZjAOBgNVHQ8BAf8EBAMC
 | 
						|
AqQwEwYDVR0lBAwwCgYIKwYBBQUHAwEwDwYDVR0TAQH/BAUwAwEB/zAuBgNVHREE
 | 
						|
JzAlggtleGFtcGxlLmNvbYcEfwAAAYcQAAAAAAAAAAAAAAAAAAAAATANBgkqhkiG
 | 
						|
9w0BAQsFAAOCAQEAThqgJ/AFqaANsOp48lojDZfZBFxJQ3A4zfR/MgggUoQ9cP3V
 | 
						|
rxuKAFWQjze1EZc7J9iO1WvH98lOGVNRY/t2VIrVoSsBiALP86Eew9WucP60tbv2
 | 
						|
8/zsBDSfEo9Wl+Q/gwdEh8dgciUKROvCm76EgAwPGicMAgRsxXgwXHhS5e8nnbIE
 | 
						|
Ewaqvb5dY++6kh0Oz+adtNT5OqOwXTIRI67WuEe6/B3Z4LNVPQDIj7ZUJGNw8e6L
 | 
						|
F4nkUthwlKx4yEJHZBRuFPnO7Z81jNKuwL276+mczRH7piI6z9uyMV/JbEsOIxyL
 | 
						|
W6CzB7pZ9Nj1YLpgzc1r6oONHLokMJJIz/IvkQ==
 | 
						|
-----END CERTIFICATE-----`)
 | 
						|
 | 
						|
// localhostKey is the private key for localhostCert.
 | 
						|
var localhostKey = []byte(`-----BEGIN RSA PRIVATE KEY-----
 | 
						|
MIIEowIBAAKCAQEA1Z5/aTwqY706M34tn60l8ZHkanWDl8mM1pYf4Q7qg3zA9XqW
 | 
						|
LX6S4rTYDYCb4stEasC72lQnbEWHbthiQE76zubP8WOFHdvGR3mjAvHWz4FxvLOT
 | 
						|
heZ+3iDUrl6Aj9UIsYqzmpBJAoY4+vGGf+xHvuukHrVcFqR9ZuBdZuJ/HbbjUyuN
 | 
						|
r3X9erNIr5Ha17gVzf17SNbYgNrX9gbCeEB8Z9Ox7dVuJhLDkpF0T/B5Zld3BjyU
 | 
						|
VY/TcukU4dTVp6isbWPvCMRCZCCOpb+qIhxEjJ0n6tnPt8nf9lvDl4SWMl6X1bH+
 | 
						|
2EFaa8R06G0QI+XhwPyjXUyCR8QEOZPCR5wyqQIDAQABAoIBAFAJmb1pMIy8OpFO
 | 
						|
hnOcYWoYepe0vgBiIOXJy9n8R7vKQ1X2f0w+b3SHw6eTd1TLSjAhVIEiJL85cdwD
 | 
						|
MRTdQrXA30qXOioMzUa8eWpCCHUpD99e/TgfO4uoi2dluw+pBx/WUyLnSqOqfLDx
 | 
						|
S66kbeFH0u86jm1hZibki7pfxLbxvu7KQgPe0meO5/13Retztz7/xa/pWIY71Zqd
 | 
						|
YC8UckuQdWUTxfuQf0470lAK34GZlDy9tvdVOG/PmNkG4j6OQjy0Kmz4Uk7rewKo
 | 
						|
ZbdphaLPJ2A4Rdqfn4WCoyDnxlfV861T922/dEDZEbNWiQpB81G8OfLL+FLHxyIT
 | 
						|
LKEu4R0CgYEA4RDj9jatJ/wGkMZBt+UF05mcJlRVMEijqdKgFwR2PP8b924Ka1mj
 | 
						|
9zqWsfbxQbdPdwsCeVBZrSlTEmuFSQLeWtqBxBKBTps/tUP0qZf7HjfSmcVI89WE
 | 
						|
3ab8LFjfh4PtK/LOq2D1GRZZkFliqi0gKwYdDoK6gxXWwrumXq4c2l8CgYEA8vrX
 | 
						|
dMuGCNDjNQkGXx3sr8pyHCDrSNR4Z4FrSlVUkgAW1L7FrCM911BuGh86FcOu9O/1
 | 
						|
Ggo0E8ge7qhQiXhB5vOo7hiVzSp0FxxCtGSlpdp4W6wx6ZWK8+Pc+6Moos03XdG7
 | 
						|
MKsdPGDciUn9VMOP3r8huX/btFTh90C/L50sH/cCgYAd02wyW8qUqux/0RYydZJR
 | 
						|
GWE9Hx3u+SFfRv9aLYgxyyj8oEOXOFjnUYdY7D3KlK1ePEJGq2RG81wD6+XM6Clp
 | 
						|
Zt2di0pBjYdi0S+iLfbkaUdqg1+ImLoz2YY/pkNxJQWQNmw2//FbMsAJxh6yKKrD
 | 
						|
qNq+6oonBwTf55hDodVHBwKBgEHgEBnyM9ygBXmTgM645jqiwF0v75pHQH2PcO8u
 | 
						|
Q0dyDr6PGjiZNWLyw2cBoFXWP9DYXbM5oPTcBMbfizY6DGP5G4uxzqtZHzBE0TDn
 | 
						|
OKHGoWr5PG7/xDRrSrZOfe3lhWVCP2XqfnqoKCJwlOYuPws89n+8UmyJttm6DBt0
 | 
						|
mUnxAoGBAIvbR87ZFXkvqstLs4KrdqTz4TQIcpzB3wENukHODPA6C1gzWTqp+OEe
 | 
						|
GMNltPfGCLO+YmoMQuTpb0kECYV3k4jR3gXO6YvlL9KbY+UOA6P0dDX4ROi2Rklj
 | 
						|
yh+lxFLYa1vlzzi9r8B7nkR9hrOGMvkfXF42X89g7lx4uMtu2I4q
 | 
						|
-----END RSA PRIVATE KEY-----`)
 |