1
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
40
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
66 if err := wait.PollImmediate(time.Millisecond*100, wait.ForeverTestTimeout, func() (bool, error) {
67 _, err := webhookServer.Client().Get(webhookServer.URL)
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
78 type V1Beta1ReviewConverterFunc func(review *apiextensionsv1beta1.ConversionReview) (*apiextensionsv1beta1.ConversionReview, error)
79
80
81 type V1ReviewConverterFunc func(review *apiextensionsv1.ConversionReview) (*apiextensionsv1.ConversionReview, error)
82
83
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
160 type ObjectConverterFunc func(desiredAPIVersion string, customResource runtime.RawExtension) (runtime.RawExtension, error)
161
162
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
212
213
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
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