k8s.io/apiserver@v0.31.1/pkg/admission/plugin/webhook/testing/webhook_server.go (about)

     1  /*
     2  Copyright 2018 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package testing
    18  
    19  import (
    20  	"crypto/tls"
    21  	"crypto/x509"
    22  	"encoding/json"
    23  	"net/http"
    24  	"net/http/httptest"
    25  	"regexp"
    26  	"strconv"
    27  	"strings"
    28  	"testing"
    29  	"time"
    30  
    31  	"k8s.io/api/admission/v1beta1"
    32  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    33  	"k8s.io/apiserver/pkg/admission/plugin/webhook/testcerts"
    34  	testingclock "k8s.io/utils/clock/testing"
    35  )
    36  
    37  // NewTestServerWithHandler returns a webhook test HTTPS server
    38  // which uses given handler function to handle requests
    39  func NewTestServerWithHandler(t testing.TB, handler func(http.ResponseWriter, *http.Request)) *httptest.Server {
    40  	// Create the test webhook server
    41  	sCert, err := tls.X509KeyPair(testcerts.ServerCert, testcerts.ServerKey)
    42  	if err != nil {
    43  		t.Error(err)
    44  		t.FailNow()
    45  	}
    46  	rootCAs := x509.NewCertPool()
    47  	rootCAs.AppendCertsFromPEM(testcerts.CACert)
    48  	testServer := httptest.NewUnstartedServer(http.HandlerFunc(handler))
    49  	testServer.TLS = &tls.Config{
    50  		Certificates: []tls.Certificate{sCert},
    51  		ClientCAs:    rootCAs,
    52  		ClientAuth:   tls.RequireAndVerifyClientCert,
    53  	}
    54  	return testServer
    55  }
    56  
    57  // NewTestServer returns a webhook test HTTPS server with fixed webhook test certs.
    58  func NewTestServer(t testing.TB) *httptest.Server {
    59  	return NewTestServerWithHandler(t, webhookHandler)
    60  }
    61  
    62  func webhookHandler(w http.ResponseWriter, r *http.Request) {
    63  	// fmt.Printf("got req: %v\n", r.URL.Path)
    64  	switch r.URL.Path {
    65  	case "/internalErr":
    66  		http.Error(w, "webhook internal server error", http.StatusInternalServerError)
    67  		return
    68  	case "/invalidReq":
    69  		w.WriteHeader(http.StatusSwitchingProtocols)
    70  		w.Write([]byte("webhook invalid request"))
    71  		return
    72  	case "/invalidResp":
    73  		w.Header().Set("Content-Type", "application/json")
    74  		w.Write([]byte("webhook invalid response"))
    75  	case "/disallow":
    76  		w.Header().Set("Content-Type", "application/json")
    77  		json.NewEncoder(w).Encode(&v1beta1.AdmissionReview{
    78  			Response: &v1beta1.AdmissionResponse{
    79  				Allowed: false,
    80  				Result: &metav1.Status{
    81  					Code: http.StatusForbidden,
    82  				},
    83  			},
    84  		})
    85  	case "/disallowReason":
    86  		w.Header().Set("Content-Type", "application/json")
    87  		json.NewEncoder(w).Encode(&v1beta1.AdmissionReview{
    88  			Response: &v1beta1.AdmissionResponse{
    89  				Allowed: false,
    90  				Result: &metav1.Status{
    91  					Message: "you shall not pass",
    92  					Code:    http.StatusForbidden,
    93  				},
    94  			},
    95  		})
    96  	case "/shouldNotBeCalled":
    97  		w.Header().Set("Content-Type", "application/json")
    98  		json.NewEncoder(w).Encode(&v1beta1.AdmissionReview{
    99  			Response: &v1beta1.AdmissionResponse{
   100  				Allowed: false,
   101  				Result: &metav1.Status{
   102  					Message: "doesn't expect labels to match object selector",
   103  					Code:    http.StatusForbidden,
   104  				},
   105  			},
   106  		})
   107  	case "/allow":
   108  		w.Header().Set("Content-Type", "application/json")
   109  		json.NewEncoder(w).Encode(&v1beta1.AdmissionReview{
   110  			Response: &v1beta1.AdmissionResponse{
   111  				Allowed: true,
   112  				AuditAnnotations: map[string]string{
   113  					"key1": "value1",
   114  				},
   115  			},
   116  		})
   117  	case "/removeLabel":
   118  		w.Header().Set("Content-Type", "application/json")
   119  		pt := v1beta1.PatchTypeJSONPatch
   120  		json.NewEncoder(w).Encode(&v1beta1.AdmissionReview{
   121  			Response: &v1beta1.AdmissionResponse{
   122  				Allowed:   true,
   123  				PatchType: &pt,
   124  				Patch:     []byte(`[{"op": "remove", "path": "/metadata/labels/remove"}]`),
   125  				AuditAnnotations: map[string]string{
   126  					"key1": "value1",
   127  				},
   128  			},
   129  		})
   130  	case "/addLabel":
   131  		w.Header().Set("Content-Type", "application/json")
   132  		pt := v1beta1.PatchTypeJSONPatch
   133  		json.NewEncoder(w).Encode(&v1beta1.AdmissionReview{
   134  			Response: &v1beta1.AdmissionResponse{
   135  				Allowed:   true,
   136  				PatchType: &pt,
   137  				Patch:     []byte(`[{"op": "add", "path": "/metadata/labels/added", "value": "test"}]`),
   138  			},
   139  		})
   140  	case "/invalidMutation":
   141  		w.Header().Set("Content-Type", "application/json")
   142  		pt := v1beta1.PatchTypeJSONPatch
   143  		json.NewEncoder(w).Encode(&v1beta1.AdmissionReview{
   144  			Response: &v1beta1.AdmissionResponse{
   145  				Allowed:   true,
   146  				PatchType: &pt,
   147  				Patch:     []byte(`[{"op": "add", "CORRUPTED_KEY":}]`),
   148  			},
   149  		})
   150  	case "/nilResponse":
   151  		w.Header().Set("Content-Type", "application/json")
   152  		json.NewEncoder(w).Encode(&v1beta1.AdmissionReview{})
   153  	case "/invalidAnnotation":
   154  		w.Header().Set("Content-Type", "application/json")
   155  		json.NewEncoder(w).Encode(&v1beta1.AdmissionReview{
   156  			Response: &v1beta1.AdmissionResponse{
   157  				Allowed: true,
   158  				AuditAnnotations: map[string]string{
   159  					"invalid*key": "value1",
   160  				},
   161  			},
   162  		})
   163  	case "/noop":
   164  		w.Header().Set("Content-Type", "application/json")
   165  		json.NewEncoder(w).Encode(&v1beta1.AdmissionReview{
   166  			Response: &v1beta1.AdmissionResponse{
   167  				Allowed: true,
   168  			},
   169  		})
   170  	default:
   171  		http.NotFound(w, r)
   172  	}
   173  }
   174  
   175  // ClockSteppingWebhookHandler given a fakeClock returns a request handler
   176  // that moves time in given clock by an amount specified in the webhook request
   177  func ClockSteppingWebhookHandler(t testing.TB, fakeClock *testingclock.FakeClock) func(http.ResponseWriter, *http.Request) {
   178  	return func(w http.ResponseWriter, r *http.Request) {
   179  		path := r.URL.Path
   180  		validPath := regexp.MustCompile(`^/(?:allow|disallow)/(\d{1,10})$`)
   181  
   182  		if !validPath.MatchString(path) {
   183  			t.Errorf("error in test case, wrong webhook url path: '%q' expected to match: '%q'", path, validPath.String())
   184  			t.FailNow()
   185  		}
   186  
   187  		delay, _ := strconv.ParseInt(validPath.FindStringSubmatch(path)[1], 0, 64)
   188  		fakeClock.Step(time.Duration(delay))
   189  		w.Header().Set("Content-Type", "application/json")
   190  
   191  		if strings.HasPrefix(path, "/allow/") {
   192  			json.NewEncoder(w).Encode(&v1beta1.AdmissionReview{
   193  				Response: &v1beta1.AdmissionResponse{
   194  					Allowed: true,
   195  					AuditAnnotations: map[string]string{
   196  						"key1": "value1",
   197  					},
   198  				},
   199  			})
   200  			return
   201  		}
   202  		json.NewEncoder(w).Encode(&v1beta1.AdmissionReview{
   203  			Response: &v1beta1.AdmissionResponse{
   204  				Allowed: false,
   205  				Result: &metav1.Status{
   206  					Code: http.StatusForbidden,
   207  				},
   208  			},
   209  		})
   210  	}
   211  }