...

Source file src/k8s.io/apiextensions-apiserver/test/integration/conversion/webhook.go

Documentation: k8s.io/apiextensions-apiserver/test/integration/conversion

     1  /*
     2  Copyright 2019 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 conversion
    18  
    19  import (
    20  	"crypto/tls"
    21  	"crypto/x509"
    22  	"encoding/json"
    23  	"fmt"
    24  	"io"
    25  	"net/http"
    26  	"net/http/httptest"
    27  	"strings"
    28  	"testing"
    29  	"time"
    30  
    31  	"k8s.io/apimachinery/pkg/util/wait"
    32  
    33  	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
    34  	apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
    35  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    36  	"k8s.io/apimachinery/pkg/runtime"
    37  )
    38  
    39  // StartConversionWebhookServer starts an http server with the provided handler and returns the WebhookClientConfig
    40  // needed to configure a CRD to use this conversion webhook as its converter.
    41  func StartConversionWebhookServer(handler http.Handler) (func(), *apiextensionsv1.WebhookClientConfig, error) {
    42  	roots := x509.NewCertPool()
    43  	if !roots.AppendCertsFromPEM(localhostCert) {
    44  		return nil, nil, fmt.Errorf("failed to append Cert from PEM")
    45  	}
    46  	cert, err := tls.X509KeyPair(localhostCert, localhostKey)
    47  	if err != nil {
    48  		return nil, nil, fmt.Errorf("failed to build cert with error: %+v", err)
    49  	}
    50  
    51  	webhookMux := http.NewServeMux()
    52  	webhookMux.Handle("/convert", handler)
    53  	webhookServer := httptest.NewUnstartedServer(webhookMux)
    54  	webhookServer.TLS = &tls.Config{
    55  		RootCAs:      roots,
    56  		Certificates: []tls.Certificate{cert},
    57  	}
    58  	webhookServer.StartTLS()
    59  	endpoint := webhookServer.URL + "/convert"
    60  	webhookConfig := &apiextensionsv1.WebhookClientConfig{
    61  		CABundle: localhostCert,
    62  		URL:      &endpoint,
    63  	}
    64  
    65  	// StartTLS returns immediately, there is a small chance of a race to avoid.
    66  	if err := wait.PollImmediate(time.Millisecond*100, wait.ForeverTestTimeout, func() (bool, error) {
    67  		_, err := webhookServer.Client().Get(webhookServer.URL) // even a 404 is fine
    68  		return err == nil, nil
    69  	}); err != nil {
    70  		webhookServer.Close()
    71  		return nil, nil, err
    72  	}
    73  
    74  	return webhookServer.Close, webhookConfig, nil
    75  }
    76  
    77  // V1Beta1ReviewConverterFunc converts an entire ConversionReview.
    78  type V1Beta1ReviewConverterFunc func(review *apiextensionsv1beta1.ConversionReview) (*apiextensionsv1beta1.ConversionReview, error)
    79  
    80  // V1ReviewConverterFunc converts an entire ConversionReview.
    81  type V1ReviewConverterFunc func(review *apiextensionsv1.ConversionReview) (*apiextensionsv1.ConversionReview, error)
    82  
    83  // NewReviewWebhookHandler creates a handler that delegates the review conversion to the provided ReviewConverterFunc.
    84  func NewReviewWebhookHandler(t *testing.T, v1beta1ConverterFunc V1Beta1ReviewConverterFunc, v1ConverterFunc V1ReviewConverterFunc) http.Handler {
    85  	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    86  		defer r.Body.Close()
    87  		data, err := io.ReadAll(r.Body)
    88  		if err != nil {
    89  			t.Error(err)
    90  			return
    91  		}
    92  		if contentType := r.Header.Get("Content-Type"); contentType != "application/json" {
    93  			t.Errorf("contentType=%s, expect application/json", contentType)
    94  			return
    95  		}
    96  
    97  		typeMeta := &metav1.TypeMeta{}
    98  		if err := json.Unmarshal(data, typeMeta); err != nil {
    99  			t.Errorf("Fail to deserialize object: %s with error: %v", string(data), err)
   100  			http.Error(w, err.Error(), 400)
   101  			return
   102  		}
   103  
   104  		var response runtime.Object
   105  
   106  		switch typeMeta.GroupVersionKind() {
   107  		case apiextensionsv1.SchemeGroupVersion.WithKind("ConversionReview"):
   108  			review := &apiextensionsv1.ConversionReview{}
   109  			if err := json.Unmarshal(data, review); err != nil {
   110  				t.Errorf("Fail to deserialize object: %s with error: %v", string(data), err)
   111  				http.Error(w, err.Error(), 400)
   112  				return
   113  			}
   114  
   115  			if v1ConverterFunc == nil {
   116  				http.Error(w, "Cannot handle v1 ConversionReview", 422)
   117  				return
   118  			}
   119  			response, err = v1ConverterFunc(review)
   120  			if err != nil {
   121  				t.Errorf("Error converting review: %v", err)
   122  				http.Error(w, err.Error(), 500)
   123  				return
   124  			}
   125  
   126  		case apiextensionsv1beta1.SchemeGroupVersion.WithKind("ConversionReview"):
   127  			review := &apiextensionsv1beta1.ConversionReview{}
   128  			if err := json.Unmarshal(data, review); err != nil {
   129  				t.Errorf("Fail to deserialize object: %s with error: %v", string(data), err)
   130  				http.Error(w, err.Error(), 400)
   131  				return
   132  			}
   133  
   134  			if v1beta1ConverterFunc == nil {
   135  				http.Error(w, "Cannot handle v1beta1 ConversionReview", 422)
   136  				return
   137  			}
   138  			response, err = v1beta1ConverterFunc(review)
   139  			if err != nil {
   140  				t.Errorf("Error converting review: %v", err)
   141  				http.Error(w, err.Error(), 500)
   142  				return
   143  			}
   144  
   145  		default:
   146  			err := fmt.Errorf("unrecognized request kind: %v", typeMeta.GroupVersionKind())
   147  			t.Error(err)
   148  			http.Error(w, err.Error(), 400)
   149  			return
   150  		}
   151  
   152  		w.Header().Set("Content-Type", "application/json")
   153  		if err := json.NewEncoder(w).Encode(response); err != nil {
   154  			t.Errorf("Marshal of response failed with error: %v", err)
   155  		}
   156  	})
   157  }
   158  
   159  // ObjectConverterFunc converts a single custom resource to the desiredAPIVersion and returns it or returns an error.
   160  type ObjectConverterFunc func(desiredAPIVersion string, customResource runtime.RawExtension) (runtime.RawExtension, error)
   161  
   162  // NewObjectConverterWebhookHandler creates a handler that delegates custom resource conversion to the provided ConverterFunc.
   163  func NewObjectConverterWebhookHandler(t *testing.T, converterFunc ObjectConverterFunc) http.Handler {
   164  	return NewReviewWebhookHandler(t, func(review *apiextensionsv1beta1.ConversionReview) (*apiextensionsv1beta1.ConversionReview, error) {
   165  		converted := []runtime.RawExtension{}
   166  		errMsgs := []string{}
   167  		for _, obj := range review.Request.Objects {
   168  			convertedObj, err := converterFunc(review.Request.DesiredAPIVersion, obj)
   169  			if err != nil {
   170  				errMsgs = append(errMsgs, err.Error())
   171  			}
   172  
   173  			converted = append(converted, convertedObj)
   174  		}
   175  
   176  		review.Response = &apiextensionsv1beta1.ConversionResponse{
   177  			UID:              review.Request.UID,
   178  			ConvertedObjects: converted,
   179  		}
   180  		if len(errMsgs) == 0 {
   181  			review.Response.Result = metav1.Status{Status: "Success"}
   182  		} else {
   183  			review.Response.Result = metav1.Status{Status: "Failure", Message: strings.Join(errMsgs, ", ")}
   184  		}
   185  		return review, nil
   186  	}, func(review *apiextensionsv1.ConversionReview) (*apiextensionsv1.ConversionReview, error) {
   187  		converted := []runtime.RawExtension{}
   188  		errMsgs := []string{}
   189  		for _, obj := range review.Request.Objects {
   190  			convertedObj, err := converterFunc(review.Request.DesiredAPIVersion, obj)
   191  			if err != nil {
   192  				errMsgs = append(errMsgs, err.Error())
   193  			}
   194  
   195  			converted = append(converted, convertedObj)
   196  		}
   197  
   198  		review.Response = &apiextensionsv1.ConversionResponse{
   199  			UID:              review.Request.UID,
   200  			ConvertedObjects: converted,
   201  		}
   202  		if len(errMsgs) == 0 {
   203  			review.Response.Result = metav1.Status{Status: "Success"}
   204  		} else {
   205  			review.Response.Result = metav1.Status{Status: "Failure", Message: strings.Join(errMsgs, ", ")}
   206  		}
   207  		return review, nil
   208  	})
   209  }
   210  
   211  // localhostCert was generated from crypto/tls/generate_cert.go with the following command:
   212  //
   213  //	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
   214  var localhostCert = []byte(`-----BEGIN CERTIFICATE-----
   215  MIIDGDCCAgCgAwIBAgIQTKCKn99d5HhQVCLln2Q+eTANBgkqhkiG9w0BAQsFADAS
   216  MRAwDgYDVQQKEwdBY21lIENvMCAXDTcwMDEwMTAwMDAwMFoYDzIwODQwMTI5MTYw
   217  MDAwWjASMRAwDgYDVQQKEwdBY21lIENvMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
   218  MIIBCgKCAQEA1Z5/aTwqY706M34tn60l8ZHkanWDl8mM1pYf4Q7qg3zA9XqWLX6S
   219  4rTYDYCb4stEasC72lQnbEWHbthiQE76zubP8WOFHdvGR3mjAvHWz4FxvLOTheZ+
   220  3iDUrl6Aj9UIsYqzmpBJAoY4+vGGf+xHvuukHrVcFqR9ZuBdZuJ/HbbjUyuNr3X9
   221  erNIr5Ha17gVzf17SNbYgNrX9gbCeEB8Z9Ox7dVuJhLDkpF0T/B5Zld3BjyUVY/T
   222  cukU4dTVp6isbWPvCMRCZCCOpb+qIhxEjJ0n6tnPt8nf9lvDl4SWMl6X1bH+2EFa
   223  a8R06G0QI+XhwPyjXUyCR8QEOZPCR5wyqQIDAQABo2gwZjAOBgNVHQ8BAf8EBAMC
   224  AqQwEwYDVR0lBAwwCgYIKwYBBQUHAwEwDwYDVR0TAQH/BAUwAwEB/zAuBgNVHREE
   225  JzAlggtleGFtcGxlLmNvbYcEfwAAAYcQAAAAAAAAAAAAAAAAAAAAATANBgkqhkiG
   226  9w0BAQsFAAOCAQEAThqgJ/AFqaANsOp48lojDZfZBFxJQ3A4zfR/MgggUoQ9cP3V
   227  rxuKAFWQjze1EZc7J9iO1WvH98lOGVNRY/t2VIrVoSsBiALP86Eew9WucP60tbv2
   228  8/zsBDSfEo9Wl+Q/gwdEh8dgciUKROvCm76EgAwPGicMAgRsxXgwXHhS5e8nnbIE
   229  Ewaqvb5dY++6kh0Oz+adtNT5OqOwXTIRI67WuEe6/B3Z4LNVPQDIj7ZUJGNw8e6L
   230  F4nkUthwlKx4yEJHZBRuFPnO7Z81jNKuwL276+mczRH7piI6z9uyMV/JbEsOIxyL
   231  W6CzB7pZ9Nj1YLpgzc1r6oONHLokMJJIz/IvkQ==
   232  -----END CERTIFICATE-----`)
   233  
   234  // localhostKey is the private key for localhostCert.
   235  var localhostKey = []byte(`-----BEGIN RSA PRIVATE KEY-----
   236  MIIEowIBAAKCAQEA1Z5/aTwqY706M34tn60l8ZHkanWDl8mM1pYf4Q7qg3zA9XqW
   237  LX6S4rTYDYCb4stEasC72lQnbEWHbthiQE76zubP8WOFHdvGR3mjAvHWz4FxvLOT
   238  heZ+3iDUrl6Aj9UIsYqzmpBJAoY4+vGGf+xHvuukHrVcFqR9ZuBdZuJ/HbbjUyuN
   239  r3X9erNIr5Ha17gVzf17SNbYgNrX9gbCeEB8Z9Ox7dVuJhLDkpF0T/B5Zld3BjyU
   240  VY/TcukU4dTVp6isbWPvCMRCZCCOpb+qIhxEjJ0n6tnPt8nf9lvDl4SWMl6X1bH+
   241  2EFaa8R06G0QI+XhwPyjXUyCR8QEOZPCR5wyqQIDAQABAoIBAFAJmb1pMIy8OpFO
   242  hnOcYWoYepe0vgBiIOXJy9n8R7vKQ1X2f0w+b3SHw6eTd1TLSjAhVIEiJL85cdwD
   243  MRTdQrXA30qXOioMzUa8eWpCCHUpD99e/TgfO4uoi2dluw+pBx/WUyLnSqOqfLDx
   244  S66kbeFH0u86jm1hZibki7pfxLbxvu7KQgPe0meO5/13Retztz7/xa/pWIY71Zqd
   245  YC8UckuQdWUTxfuQf0470lAK34GZlDy9tvdVOG/PmNkG4j6OQjy0Kmz4Uk7rewKo
   246  ZbdphaLPJ2A4Rdqfn4WCoyDnxlfV861T922/dEDZEbNWiQpB81G8OfLL+FLHxyIT
   247  LKEu4R0CgYEA4RDj9jatJ/wGkMZBt+UF05mcJlRVMEijqdKgFwR2PP8b924Ka1mj
   248  9zqWsfbxQbdPdwsCeVBZrSlTEmuFSQLeWtqBxBKBTps/tUP0qZf7HjfSmcVI89WE
   249  3ab8LFjfh4PtK/LOq2D1GRZZkFliqi0gKwYdDoK6gxXWwrumXq4c2l8CgYEA8vrX
   250  dMuGCNDjNQkGXx3sr8pyHCDrSNR4Z4FrSlVUkgAW1L7FrCM911BuGh86FcOu9O/1
   251  Ggo0E8ge7qhQiXhB5vOo7hiVzSp0FxxCtGSlpdp4W6wx6ZWK8+Pc+6Moos03XdG7
   252  MKsdPGDciUn9VMOP3r8huX/btFTh90C/L50sH/cCgYAd02wyW8qUqux/0RYydZJR
   253  GWE9Hx3u+SFfRv9aLYgxyyj8oEOXOFjnUYdY7D3KlK1ePEJGq2RG81wD6+XM6Clp
   254  Zt2di0pBjYdi0S+iLfbkaUdqg1+ImLoz2YY/pkNxJQWQNmw2//FbMsAJxh6yKKrD
   255  qNq+6oonBwTf55hDodVHBwKBgEHgEBnyM9ygBXmTgM645jqiwF0v75pHQH2PcO8u
   256  Q0dyDr6PGjiZNWLyw2cBoFXWP9DYXbM5oPTcBMbfizY6DGP5G4uxzqtZHzBE0TDn
   257  OKHGoWr5PG7/xDRrSrZOfe3lhWVCP2XqfnqoKCJwlOYuPws89n+8UmyJttm6DBt0
   258  mUnxAoGBAIvbR87ZFXkvqstLs4KrdqTz4TQIcpzB3wENukHODPA6C1gzWTqp+OEe
   259  GMNltPfGCLO+YmoMQuTpb0kECYV3k4jR3gXO6YvlL9KbY+UOA6P0dDX4ROi2Rklj
   260  yh+lxFLYa1vlzzi9r8B7nkR9hrOGMvkfXF42X89g7lx4uMtu2I4q
   261  -----END RSA PRIVATE KEY-----`)
   262  

View as plain text