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  }