knative.dev/pkg@v0.0.0-20260602142205-ac97e43f6622/webhook/conversion_integration_test.go (about) 1 /* 2 Copyright 2020 The Knative 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 webhook 18 19 import ( 20 "bytes" 21 "context" 22 "encoding/json" 23 "fmt" 24 "io" 25 "net/http" 26 "strings" 27 "testing" 28 29 "github.com/google/go-cmp/cmp" 30 "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" 31 "golang.org/x/sync/errgroup" 32 33 apixv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" 34 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 35 "k8s.io/apimachinery/pkg/runtime" 36 "k8s.io/apimachinery/pkg/types" 37 38 kubeclient "knative.dev/pkg/client/injection/kube/client/fake" 39 "knative.dev/pkg/observability/metrics/metricstest" 40 ) 41 42 type fixedConversionController struct { 43 path string 44 response *apixv1.ConversionResponse 45 } 46 47 var _ ConversionController = (*fixedConversionController)(nil) 48 49 func (fcc *fixedConversionController) Path() string { 50 return fcc.path 51 } 52 53 func (fcc *fixedConversionController) Convert(context.Context, *apixv1.ConversionRequest) *apixv1.ConversionResponse { 54 return fcc.response 55 } 56 57 func TestConversionEmptyRequestBody(t *testing.T) { 58 c := &fixedConversionController{ 59 path: "/bazinga", 60 response: &apixv1.ConversionResponse{}, 61 } 62 63 testEmptyRequestBody(t, c) 64 } 65 66 func TestConversionValidResponse(t *testing.T) { 67 cc := &fixedConversionController{ 68 path: "/bazinga", 69 response: &apixv1.ConversionResponse{ 70 UID: types.UID("some-uid"), 71 Result: metav1.Status{ 72 Status: metav1.StatusSuccess, 73 }, 74 }, 75 } 76 test := testSetup(t, withController(cc)) 77 78 eg, _ := errgroup.WithContext(test.ctx) 79 eg.Go(func() error { return test.webhook.Run(test.ctx.Done()) }) 80 defer func() { 81 test.cancel() 82 if err := eg.Wait(); err != nil { 83 t.Error("Unable to run controller:", err) 84 } 85 }() 86 87 if err := waitForServerAvailable(t, test.addr, testTimeout); err != nil { 88 t.Fatal("waitForServerAvailable() =", err) 89 } 90 tlsClient, err := createSecureTLSClient(t, kubeclient.Get(test.ctx), &test.webhook.Options) 91 if err != nil { 92 t.Fatal("createSecureTLSClient() =", err) 93 } 94 95 review := apixv1.ConversionReview{ 96 TypeMeta: metav1.TypeMeta{ 97 APIVersion: "apiextensions.k8s.io/v1", 98 Kind: "ConversionReview", 99 }, 100 Request: &apixv1.ConversionRequest{ 101 UID: types.UID("some-uid"), 102 DesiredAPIVersion: "example.com/v1", 103 Objects: []runtime.RawExtension{}, 104 }, 105 } 106 107 reqBuf := new(bytes.Buffer) 108 err = json.NewEncoder(reqBuf).Encode(&review) 109 if err != nil { 110 t.Fatal("Failed to marshal conversion review:", err) 111 } 112 113 req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("https://%s%s", test.addr, cc.Path()), reqBuf) 114 if err != nil { 115 t.Fatal("http.NewRequest() =", err) 116 } 117 req.Header.Add("Content-Type", "application/json") 118 119 response, err := tlsClient.Do(req) 120 if err != nil { 121 t.Fatal("Failed to get response", err) 122 } 123 124 defer response.Body.Close() 125 126 if got, want := response.StatusCode, http.StatusOK; got != want { 127 t.Errorf("Response status code = %v, wanted %v", got, want) 128 } 129 130 responseBody, err := io.ReadAll(response.Body) 131 if err != nil { 132 t.Fatal("Failed to read response body", err) 133 } 134 135 reviewResponse := apixv1.ConversionReview{} 136 137 err = json.NewDecoder(bytes.NewReader(responseBody)).Decode(&reviewResponse) 138 if err != nil { 139 t.Fatal("Failed to decode response:", err) 140 } 141 142 if reviewResponse.Response.UID != "some-uid" { 143 t.Errorf("expected the response uid to be the stubbed version") 144 } 145 146 if diff := cmp.Diff(review.TypeMeta, reviewResponse.TypeMeta); diff != "" { 147 t.Errorf("expected the response typeMeta to be the same as the request (-want, +got)\n%s", diff) 148 } 149 150 assertConversionMetrics(t, test, cc.response.Result.Status) 151 } 152 153 func TestConversionInvalidResponse(t *testing.T) { 154 cc := &fixedConversionController{ 155 path: "/bazinga", 156 response: &apixv1.ConversionResponse{ 157 UID: types.UID("some-uid"), 158 Result: metav1.Status{ 159 Status: metav1.StatusFailure, 160 }, 161 }, 162 } 163 test := testSetup(t, withController(cc)) 164 165 eg, _ := errgroup.WithContext(test.ctx) 166 eg.Go(func() error { return test.webhook.Run(test.ctx.Done()) }) 167 defer func() { 168 test.cancel() 169 if err := eg.Wait(); err != nil { 170 t.Error("Unable to run controller:", err) 171 } 172 }() 173 174 if err := waitForServerAvailable(t, test.addr, testTimeout); err != nil { 175 t.Fatal("waitForServerAvailable() =", err) 176 } 177 tlsClient, err := createSecureTLSClient(t, kubeclient.Get(test.ctx), &test.webhook.Options) 178 if err != nil { 179 t.Fatal("createSecureTLSClient() =", err) 180 } 181 182 review := apixv1.ConversionReview{ 183 TypeMeta: metav1.TypeMeta{ 184 APIVersion: "apiextensions.k8s.io/v1", 185 Kind: "ConversionReview", 186 }, 187 Request: &apixv1.ConversionRequest{ 188 UID: types.UID("some-uid"), 189 DesiredAPIVersion: "example.com/v1", 190 Objects: []runtime.RawExtension{}, 191 }, 192 } 193 194 reqBuf := new(bytes.Buffer) 195 err = json.NewEncoder(reqBuf).Encode(&review) 196 if err != nil { 197 t.Fatal("Failed to marshal conversion review:", err) 198 } 199 200 req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("https://%s%s", test.addr, cc.Path()), reqBuf) 201 if err != nil { 202 t.Fatal("http.NewRequest() =", err) 203 } 204 req.Header.Add("Content-Type", "application/json") 205 206 response, err := tlsClient.Do(req) 207 if err != nil { 208 t.Fatal("Failed to get response", err) 209 } 210 211 defer response.Body.Close() 212 213 if got, want := response.StatusCode, http.StatusOK; got != want { 214 t.Errorf("Response status code = %v, wanted %v", got, want) 215 } 216 217 responseBody, err := io.ReadAll(response.Body) 218 if err != nil { 219 t.Fatal("Failed to read response body", err) 220 } 221 222 reviewResponse := apixv1.ConversionReview{} 223 224 err = json.NewDecoder(bytes.NewReader(responseBody)).Decode(&reviewResponse) 225 if err != nil { 226 t.Fatal("Failed to decode response:", err) 227 } 228 229 if reviewResponse.Response.UID != "some-uid" { 230 t.Errorf("expected the response uid to be the stubbed version") 231 } 232 233 if reviewResponse.Response.Result.Status != metav1.StatusFailure { 234 t.Errorf("expected the response uid to be the stubbed version") 235 } 236 237 assertConversionMetrics(t, test, cc.response.Result.Status) 238 } 239 240 func assertConversionMetrics(t *testing.T, tc testContext, status string) { 241 metricstest.AssertMetrics(t, tc.metricReader, 242 metricstest.MetricsPresent( 243 otelhttp.ScopeName, 244 "http.server.request.body.size", 245 "http.server.response.body.size", 246 "http.server.request.duration", 247 ), 248 metricstest.MetricsPresent( 249 scopeName, 250 "kn.webhook.handler.duration", 251 ), 252 metricstest.HasAttributes( 253 "", // any scope 254 "", // any metric 255 WebhookTypeAttr.With(WebhookTypeConversion), 256 GroupAttr.With("example.com"), 257 VersionAttr.With("v1"), 258 StatusAttr.With(strings.ToLower(status)), 259 ), 260 ) 261 }