istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pkg/webhooks/validation/controller/controller_test.go (about) 1 // Copyright Istio Authors 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package controller 16 17 import ( 18 "errors" 19 "fmt" 20 "testing" 21 "time" 22 23 "go.uber.org/atomic" 24 admission "k8s.io/api/admissionregistration/v1" 25 kerrors "k8s.io/apimachinery/pkg/api/errors" 26 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 27 "k8s.io/apimachinery/pkg/runtime" 28 ktesting "k8s.io/client-go/testing" 29 30 "istio.io/api/label" 31 "istio.io/client-go/pkg/apis/networking/v1alpha3" 32 istiofake "istio.io/client-go/pkg/clientset/versioned/fake" 33 "istio.io/istio/pilot/pkg/keycertbundle" 34 "istio.io/istio/pkg/kube" 35 "istio.io/istio/pkg/kube/kclient/clienttest" 36 "istio.io/istio/pkg/ptr" 37 "istio.io/istio/pkg/test" 38 "istio.io/istio/pkg/test/util/assert" 39 "istio.io/istio/pkg/test/util/retry" 40 "istio.io/istio/pkg/testcerts" 41 "istio.io/istio/pkg/webhooks/util" 42 ) 43 44 var ( 45 unpatchedWebhookConfig = &admission.ValidatingWebhookConfiguration{ 46 TypeMeta: metav1.TypeMeta{ 47 APIVersion: admission.SchemeGroupVersion.String(), 48 Kind: "ValidatingWebhookConfiguration", 49 }, 50 ObjectMeta: metav1.ObjectMeta{ 51 Name: webhookName, 52 Labels: map[string]string{ 53 label.IoIstioRev.Name: revision, 54 }, 55 }, 56 Webhooks: []admission.ValidatingWebhook{{ 57 Name: "hook0", 58 ClientConfig: admission.WebhookClientConfig{Service: &admission.ServiceReference{ 59 Namespace: namespace, 60 Name: istiod, 61 Path: &[]string{"/hook0"}[0], 62 }}, 63 Rules: []admission.RuleWithOperations{{ 64 Operations: []admission.OperationType{admission.Create, admission.Update}, 65 Rule: admission.Rule{ 66 APIGroups: []string{"group0"}, 67 APIVersions: []string{"*"}, 68 Resources: []string{"*"}, 69 }, 70 }}, 71 FailurePolicy: ptr.Of(admission.Ignore), 72 }, { 73 Name: "hook1", 74 ClientConfig: admission.WebhookClientConfig{Service: &admission.ServiceReference{ 75 Namespace: namespace, 76 Name: istiod, 77 Path: &[]string{"/hook1"}[0], 78 }}, 79 Rules: []admission.RuleWithOperations{{ 80 Operations: []admission.OperationType{admission.Create, admission.Update}, 81 Rule: admission.Rule{ 82 APIGroups: []string{"group1"}, 83 APIVersions: []string{"*"}, 84 Resources: []string{"*"}, 85 }, 86 }}, 87 FailurePolicy: ptr.Of(admission.Ignore), 88 }}, 89 } 90 91 webhookConfigWithCABundleFail *admission.ValidatingWebhookConfiguration 92 webhookConfigWithCABundleIgnore *admission.ValidatingWebhookConfiguration 93 94 caBundle0 = []byte(`-----BEGIN CERTIFICATE----- 95 MIIC9DCCAdygAwIBAgIJAIFe3lWPaalKMA0GCSqGSIb3DQEBCwUAMA4xDDAKBgNV 96 BAMMA19jYTAgFw0xNzEyMjIxODA0MjRaGA8yMjkxMTAwNzE4MDQyNFowDjEMMAoG 97 A1UEAwwDX2NhMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuBdxj+Hi 98 8h0TkId1f64TprLydwgzzLwXAs3wpmXz+BfnW1oMQPNyN7vojW6VzqJGGYLsc1OB 99 MgwObU/VeFNc6YUCmu6mfFJwoPfXMPnhmGuSwf/kjXomlejAYjxClU3UFVWQht54 100 xNLjTi2M1ZOnwNbECOhXC3Tw3G8mCtfanMAO0UXM5yObbPa8yauUpJKkpoxWA7Ed 101 qiuUD9qRxluFPqqw/z86V8ikmvnyjQE9960j+8StlAbRs82ArtnrhRgkDO0Smtf7 102 4QZsb/hA1KNMm73bOGS6+SVU+eH8FgVOzcTQYFRpRT3Mhi6dKZe9twIO8mpZK4wk 103 uygRxBM32Ag9QQIDAQABo1MwUTAdBgNVHQ4EFgQUc8tvoNNBHyIkoVV8XCXy63Ya 104 BEQwHwYDVR0jBBgwFoAUc8tvoNNBHyIkoVV8XCXy63YaBEQwDwYDVR0TAQH/BAUw 105 AwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAVmaUkkYESfcfgnuPeZ4sTNs2nk2Y+Xpd 106 lxkMJhChb8YQtlCe4uiLvVe7er1sXcBLNCm/+2K9AT71gnxBSeS5mEOzWmCPErhy 107 RmYtSxeRyXAaUWVYLs/zMlBQ0Iz4dpY+FVVbMjIurelVwHF0NBk3VtU5U3lHyKdZ 108 j4C2rMjvTxmkyIcR1uBEeVvuGU8R70nZ1yfo3vDwmNGMcLwW+4QK+WcfwfjLXhLs 109 5550arfEYdTzYFMxY60HJT/LvbGrjxY0PQUWWDbPiRfsdRjOFduAbM0/EVRda/Oo 110 Fg72WnHeojDUhqEz4UyFZbnRJ4x6leQhnrIcVjWX4FFFktiO9rqqfw== 111 -----END CERTIFICATE-----`) 112 113 caBundle1 = []byte(`-----BEGIN CERTIFICATE----- 114 MIIDCzCCAfOgAwIBAgIQbfOzhcKTldFipQ1X2WXpHDANBgkqhkiG9w0BAQsFADAv 115 MS0wKwYDVQQDEyRhNzU5YzcyZC1lNjcyLTQwMzYtYWMzYy1kYzAxMDBmMTVkNWUw 116 HhcNMTkwNTE2MjIxMTI2WhcNMjQwNTE0MjMxMTI2WjAvMS0wKwYDVQQDEyRhNzU5 117 YzcyZC1lNjcyLTQwMzYtYWMzYy1kYzAxMDBmMTVkNWUwggEiMA0GCSqGSIb3DQEB 118 AQUAA4IBDwAwggEKAoIBAQC6sSAN80Ci0DYFpNDumGYoejMQai42g6nSKYS+ekvs 119 E7uT+eepO74wj8o6nFMNDu58+XgIsvPbWnn+3WtUjJfyiQXxmmTg8om4uY1C7R1H 120 gMsrL26pUaXZ/lTE8ZV5CnQJ9XilagY4iZKeptuZkxrWgkFBD7tr652EA3hmj+3h 121 4sTCQ+pBJKG8BJZDNRrCoiABYBMcFLJsaKuGZkJ6KtxhQEO9QxJVaDoSvlCRGa8R 122 fcVyYQyXOZ+0VHZJQgaLtqGpiQmlFttpCwDiLfMkk3UAd79ovkhN1MCq+O5N7YVt 123 eVQWaTUqUV2tKUFvVq21Zdl4dRaq+CF5U8uOqLY/4Kg9AgMBAAGjIzAhMA4GA1Ud 124 DwEB/wQEAwICBDAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQCg 125 oF71Ey2b1QY22C6BXcANF1+wPzxJovFeKYAnUqwh3rF7pIYCS/adZXOKlgDBsbcS 126 MxAGnCRi1s+A7hMYj3sQAbBXttc31557lRoJrx58IeN5DyshT53t7q4VwCzuCXFT 127 3zRHVRHQnO6LHgZx1FuKfwtkhfSXDyYU2fQYw2Hcb9krYU/alViVZdE0rENXCClq 128 xO7AQk5MJcGg6cfE5wWAKU1ATjpK4CN+RTn8v8ODLoI2SW3pfsnXxm93O+pp9HN4 129 +O+1PQtNUWhCfh+g6BN2mYo2OEZ8qGSxDlMZej4YOdVkW8PHmFZTK0w9iJKqM5o1 130 V6g5gZlqSoRhICK09tpc 131 -----END CERTIFICATE-----`) 132 ) 133 134 // patch the caBundle into the final istiod and galley configs. 135 func init() { 136 webhookConfigWithCABundleIgnore = unpatchedWebhookConfig.DeepCopyObject().(*admission.ValidatingWebhookConfiguration) 137 webhookConfigWithCABundleIgnore.Webhooks[0].ClientConfig.CABundle = caBundle0 138 webhookConfigWithCABundleIgnore.Webhooks[1].ClientConfig.CABundle = caBundle0 139 140 webhookConfigWithCABundleFail = webhookConfigWithCABundleIgnore.DeepCopyObject().(*admission.ValidatingWebhookConfiguration) 141 webhookConfigWithCABundleFail.Webhooks[0].FailurePolicy = ptr.Of(admission.Fail) 142 webhookConfigWithCABundleFail.Webhooks[1].FailurePolicy = ptr.Of(admission.Fail) 143 } 144 145 const ( 146 istiod = "istio-revision" 147 namespace = "istio-system" 148 revision = "revision" 149 ) 150 151 var webhookName = fmt.Sprintf("istio-validator-revision-%s", namespace) 152 153 func createTestController(t *testing.T) (*Controller, *atomic.Pointer[error]) { 154 c := kube.NewFakeClient() 155 revision := "default" 156 ns := "default" 157 watcher := keycertbundle.NewWatcher() 158 watcher.SetAndNotify(nil, nil, caBundle0) 159 control := newController(Options{ 160 WatchedNamespace: ns, 161 CABundleWatcher: watcher, 162 Revision: revision, 163 ServiceName: "istiod", 164 }, c) 165 stop := test.NewStop(t) 166 c.RunAndWait(stop) 167 go control.Run(stop) 168 kube.WaitForCacheSync("test", stop, control.queue.HasSynced) 169 170 gatewayError := setupGatewayError(c) 171 172 return control, gatewayError 173 } 174 175 func unstartedTestController(c kube.Client) *Controller { 176 revision := "default" 177 ns := "default" 178 watcher := keycertbundle.NewWatcher() 179 watcher.SetAndNotify(nil, nil, caBundle0) 180 control := newController(Options{ 181 WatchedNamespace: ns, 182 CABundleWatcher: watcher, 183 Revision: revision, 184 ServiceName: "istiod", 185 }, c) 186 return control 187 } 188 189 func setupGatewayError(c kube.Client) *atomic.Pointer[error] { 190 gatewayError := atomic.NewPointer[error](nil) 191 c.Istio().(*istiofake.Clientset).PrependReactor("*", "gateways", func(action ktesting.Action) (bool, runtime.Object, error) { 192 e := gatewayError.Load() 193 if e == nil { 194 return true, &v1alpha3.Gateway{}, nil 195 } 196 return true, &v1alpha3.Gateway{}, *e 197 }) 198 return gatewayError 199 } 200 201 func TestGreenfield(t *testing.T) { 202 controllerStop := make(chan struct{}) 203 clientStop := test.NewStop(t) 204 kc := kube.NewFakeClient() 205 gatewayError := setupGatewayError(kc) 206 c := unstartedTestController(kc) 207 kc.RunAndWait(clientStop) 208 go c.Run(controllerStop) 209 webhooks := clienttest.Wrap(t, c.webhooks) 210 fetch := func(name string) func() *admission.ValidatingWebhookConfiguration { 211 return func() *admission.ValidatingWebhookConfiguration { 212 return webhooks.Get(name, "") 213 } 214 } 215 // install adds the webhook config with fail open policy 216 webhooks.Create(unpatchedWebhookConfig) 217 // verify the webhook isn't updated if invalid config is accepted. 218 assert.EventuallyEqual( 219 t, 220 fetch(unpatchedWebhookConfig.Name), 221 webhookConfigWithCABundleIgnore, 222 retry.Message("no config update when endpoint not present"), 223 LongRetry, 224 ) 225 webhooks.Delete(unpatchedWebhookConfig.Name, "") 226 227 // verify the webhook is updated after the controller can confirm invalid config is rejected. 228 gatewayError.Store(ptr.Of[error](kerrors.NewInternalError(errors.New("unknown error")))) 229 webhooks.Create(unpatchedWebhookConfig) 230 assert.EventuallyEqual( 231 t, 232 fetch(unpatchedWebhookConfig.Name), 233 webhookConfigWithCABundleIgnore, 234 retry.Message("no config update when endpoint invalid config is rejected for an unknown reason"), 235 LongRetry, 236 ) 237 238 // verify the webhook is updated after the controller can confirm invalid config is rejected. 239 gatewayError.Store(ptr.Of[error](kerrors.NewInternalError(errors.New(deniedRequestMessageFragment)))) 240 c.syncAll() 241 assert.EventuallyEqual( 242 t, 243 fetch(unpatchedWebhookConfig.Name), 244 webhookConfigWithCABundleFail, 245 retry.Message("istiod config created when endpoint is ready and invalid config is denied"), 246 LongRetry, 247 ) 248 249 // If we start having issues, we should not flip back to Ignore 250 gatewayError.Store(ptr.Of[error](kerrors.NewInternalError(errors.New("unknown error")))) 251 c.syncAll() 252 assert.EventuallyEqual( 253 t, 254 fetch(unpatchedWebhookConfig.Name), 255 webhookConfigWithCABundleFail, 256 retry.Message("should not go from Fail -> Ignore"), 257 LongRetry, 258 ) 259 260 // We also should not flip back to Ignore in a new instance 261 close(controllerStop) 262 _ = c.queue.WaitForClose(time.Second) 263 controllerStop2 := test.NewStop(t) 264 c2 := unstartedTestController(kc) 265 go c2.Run(controllerStop2) 266 assert.EventuallyEqual( 267 t, 268 fetch(unpatchedWebhookConfig.Name), 269 webhookConfigWithCABundleFail, 270 retry.Message("should not go from Fail -> Ignore"), 271 LongRetry, 272 ) 273 _ = c2 274 } 275 276 // TestCABundleChange ensures that we create request to update all webhooks when CA bundle changes. 277 func TestCABundleChange(t *testing.T) { 278 c, gatewayError := createTestController(t) 279 gatewayError.Store(ptr.Of[error](kerrors.NewInternalError(errors.New(deniedRequestMessageFragment)))) 280 webhooks := clienttest.Wrap(t, c.webhooks) 281 fetch := func(name string) func() *admission.ValidatingWebhookConfiguration { 282 return func() *admission.ValidatingWebhookConfiguration { 283 return webhooks.Get(name, "") 284 } 285 } 286 webhooks.Create(unpatchedWebhookConfig) 287 assert.EventuallyEqual( 288 t, 289 fetch(unpatchedWebhookConfig.Name), 290 webhookConfigWithCABundleFail, 291 retry.Message("istiod config created when endpoint is ready"), 292 LongRetry, 293 ) 294 295 c.o.CABundleWatcher.SetAndNotify(nil, nil, caBundle1) 296 webhookConfigAfterCAUpdate := webhookConfigWithCABundleFail.DeepCopyObject().(*admission.ValidatingWebhookConfiguration) 297 webhookConfigAfterCAUpdate.Webhooks[0].ClientConfig.CABundle = caBundle1 298 webhookConfigAfterCAUpdate.Webhooks[1].ClientConfig.CABundle = caBundle1 299 assert.EventuallyEqual( 300 t, 301 fetch(unpatchedWebhookConfig.Name), 302 webhookConfigAfterCAUpdate, 303 retry.Message("webhook should change after cert change"), 304 LongRetry, 305 ) 306 } 307 308 // LongRetry is used when comparing webhook values. Apparently the values are so large that with -race 309 // on the comparison can take a few seconds, meaning we never retry with the default settings. 310 var LongRetry = retry.Timeout(time.Second * 20) 311 312 func TestLoadCaCertPem(t *testing.T) { 313 cases := []struct { 314 name string 315 cert []byte 316 wantError bool 317 }{ 318 { 319 name: "valid pem", 320 cert: testcerts.CACert, 321 wantError: false, 322 }, 323 { 324 name: "pem decode error", 325 cert: append([]byte("-----codec"), testcerts.CACert...), 326 wantError: true, 327 }, 328 { 329 name: "pem wrong type", 330 wantError: true, 331 }, 332 { 333 name: "invalid x509", 334 cert: testcerts.BadCert, 335 wantError: true, 336 }, 337 } 338 339 for i, c := range cases { 340 t.Run(fmt.Sprintf("[%v] %s", i, c.name), func(tt *testing.T) { 341 err := util.VerifyCABundle(c.cert) 342 if err != nil { 343 if !c.wantError { 344 tt.Fatalf("unexpected error: got error %q", err) 345 } 346 } else { 347 if c.wantError { 348 tt.Fatal("expected error") 349 } 350 } 351 }) 352 } 353 }