istio.io/istio@v0.0.0-20240520182934-d79c90f27776/istioctl/pkg/tag/generate_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 tag
    16  
    17  import (
    18  	"context"
    19  	"fmt"
    20  	"path/filepath"
    21  	"testing"
    22  
    23  	admitv1 "k8s.io/api/admissionregistration/v1"
    24  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    25  	"k8s.io/apimachinery/pkg/runtime"
    26  	"k8s.io/apimachinery/pkg/runtime/serializer"
    27  
    28  	"istio.io/api/label"
    29  	"istio.io/istio/pkg/kube"
    30  	"istio.io/istio/pkg/test/env"
    31  	"istio.io/istio/pkg/test/util/assert"
    32  )
    33  
    34  var (
    35  	defaultRevisionCanonicalWebhook = admitv1.MutatingWebhookConfiguration{
    36  		ObjectMeta: metav1.ObjectMeta{
    37  			Name:   "istio-sidecar-injector",
    38  			Labels: map[string]string{label.IoIstioRev.Name: "default"},
    39  		},
    40  		Webhooks: []admitv1.MutatingWebhook{
    41  			{
    42  				Name: fmt.Sprintf("namespace.%s", istioInjectionWebhookSuffix),
    43  				ClientConfig: admitv1.WebhookClientConfig{
    44  					Service: &admitv1.ServiceReference{
    45  						Namespace: "default",
    46  						Name:      "istiod",
    47  					},
    48  					CABundle: []byte("ca"),
    49  				},
    50  			},
    51  			{
    52  				Name: fmt.Sprintf("object.%s", istioInjectionWebhookSuffix),
    53  				ClientConfig: admitv1.WebhookClientConfig{
    54  					Service: &admitv1.ServiceReference{
    55  						Namespace: "default",
    56  						Name:      "istiod",
    57  					},
    58  					CABundle: []byte("ca"),
    59  				},
    60  			},
    61  		},
    62  	}
    63  	samplePath               = "/sample/path"
    64  	operatorManaged          = operatorNamespace + "/managed"
    65  	revisionCanonicalWebhook = admitv1.MutatingWebhookConfiguration{
    66  		ObjectMeta: metav1.ObjectMeta{
    67  			Name: "istio-sidecar-injector-revision",
    68  			Labels: map[string]string{
    69  				label.IoIstioRev.Name: "revision",
    70  				operatorManaged:       "Reconcile",
    71  			},
    72  		},
    73  		Webhooks: []admitv1.MutatingWebhook{
    74  			{
    75  				Name: fmt.Sprintf("namespace.%s", istioInjectionWebhookSuffix),
    76  				ClientConfig: admitv1.WebhookClientConfig{
    77  					Service: &admitv1.ServiceReference{
    78  						Namespace: "default",
    79  						Name:      "istiod-revision",
    80  						Path:      &samplePath,
    81  					},
    82  					CABundle: []byte("ca"),
    83  				},
    84  			},
    85  			{
    86  				Name: fmt.Sprintf("object.%s", istioInjectionWebhookSuffix),
    87  				ClientConfig: admitv1.WebhookClientConfig{
    88  					Service: &admitv1.ServiceReference{
    89  						Namespace: "default",
    90  						Name:      "istiod-revision",
    91  					},
    92  					CABundle: []byte("ca"),
    93  				},
    94  			},
    95  		},
    96  	}
    97  	remoteInjectionURL             = "https://random.host.com/inject/cluster/cluster1/net/net1"
    98  	revisionCanonicalWebhookRemote = admitv1.MutatingWebhookConfiguration{
    99  		ObjectMeta: metav1.ObjectMeta{
   100  			Name:   "istio-sidecar-injector-revision",
   101  			Labels: map[string]string{label.IoIstioRev.Name: "revision"},
   102  		},
   103  		Webhooks: []admitv1.MutatingWebhook{
   104  			{
   105  				Name: fmt.Sprintf("namespace.%s", istioInjectionWebhookSuffix),
   106  				ClientConfig: admitv1.WebhookClientConfig{
   107  					URL:      &remoteInjectionURL,
   108  					CABundle: []byte("ca"),
   109  				},
   110  			},
   111  			{
   112  				Name: fmt.Sprintf("object.%s", istioInjectionWebhookSuffix),
   113  				ClientConfig: admitv1.WebhookClientConfig{
   114  					URL:      &remoteInjectionURL,
   115  					CABundle: []byte("ca"),
   116  				},
   117  			},
   118  		},
   119  	}
   120  	remoteValidationURL = "https://random.host.com/validate"
   121  )
   122  
   123  func TestGenerateValidatingWebhook(t *testing.T) {
   124  	tcs := []struct {
   125  		name           string
   126  		istioNamespace string
   127  		webhook        admitv1.MutatingWebhookConfiguration
   128  		whURL          string
   129  		whSVC          string
   130  		whCA           string
   131  		userManaged    bool
   132  	}{
   133  		{
   134  			name:           "webhook-pointing-to-service",
   135  			istioNamespace: "istio-system",
   136  			webhook:        revisionCanonicalWebhook,
   137  			whURL:          "",
   138  			whSVC:          "istiod-revision",
   139  			whCA:           "ca",
   140  		},
   141  		{
   142  			name:           "webhook-custom-istio-namespace",
   143  			istioNamespace: "istio-system-blue",
   144  			webhook:        revisionCanonicalWebhook,
   145  			whURL:          "",
   146  			whSVC:          "istiod-revision",
   147  			whCA:           "ca",
   148  		},
   149  		{
   150  			name:           "webhook-pointing-to-url",
   151  			istioNamespace: "istio-system",
   152  			webhook:        revisionCanonicalWebhookRemote,
   153  			whURL:          remoteValidationURL,
   154  			whSVC:          "",
   155  			whCA:           "ca",
   156  		},
   157  		{
   158  			name:           "webhook-process-failure-policy",
   159  			istioNamespace: "istio-system",
   160  			webhook:        revisionCanonicalWebhook,
   161  			whURL:          "",
   162  			whSVC:          "istiod-revision",
   163  			whCA:           "ca",
   164  			userManaged:    true,
   165  		},
   166  	}
   167  	scheme := runtime.NewScheme()
   168  	codecFactory := serializer.NewCodecFactory(scheme)
   169  	deserializer := codecFactory.UniversalDeserializer()
   170  
   171  	fail := admitv1.Fail
   172  	fakeClient := kube.NewFakeClient(&admitv1.ValidatingWebhookConfiguration{
   173  		TypeMeta: metav1.TypeMeta{},
   174  		ObjectMeta: metav1.ObjectMeta{
   175  			Name: "istiod-default-validator",
   176  		},
   177  		Webhooks: []admitv1.ValidatingWebhook{
   178  			{
   179  				Name: "random",
   180  			},
   181  			{
   182  				FailurePolicy: &fail,
   183  				Name:          "validation.istio.io",
   184  			},
   185  		},
   186  	})
   187  	for _, tc := range tcs {
   188  		t.Run(tc.name, func(t *testing.T) {
   189  			webhookConfig, err := tagWebhookConfigFromCanonicalWebhook(tc.webhook, "default", tc.istioNamespace)
   190  			if err != nil {
   191  				t.Fatalf("webhook parsing failed with error: %v", err)
   192  			}
   193  			webhookConfig, err = fixWhConfig(fakeClient, webhookConfig)
   194  			if err != nil {
   195  				t.Fatalf("webhook fixing failed with error: %v", err)
   196  			}
   197  			opts := &GenerateOptions{
   198  				ManifestsPath: filepath.Join(env.IstioSrc, "manifests"),
   199  			}
   200  			if tc.userManaged {
   201  				opts.UserManaged = true
   202  			}
   203  			webhookYAML, err := generateValidatingWebhook(webhookConfig, opts)
   204  			if err != nil {
   205  				t.Fatalf("tag webhook YAML generation failed with error: %v", err)
   206  			}
   207  
   208  			vwhObject, _, err := deserializer.Decode([]byte(webhookYAML), nil, &admitv1.ValidatingWebhookConfiguration{})
   209  			if err != nil {
   210  				t.Fatalf("could not parse webhook from generated YAML: %s", vwhObject)
   211  			}
   212  			wh := vwhObject.(*admitv1.ValidatingWebhookConfiguration)
   213  
   214  			if tc.userManaged {
   215  				// User created webhooks should not have operator labels, otherwise will be pruned.
   216  				_, ok := wh.GetLabels()[operatorManaged]
   217  				assert.Equal(t, ok, false)
   218  			}
   219  
   220  			for _, webhook := range wh.Webhooks {
   221  				validationWhConf := webhook.ClientConfig
   222  
   223  				// this is nil since we've already have one with failed FailurePolicy in the fake client
   224  				if webhook.FailurePolicy != nil {
   225  					t.Fatalf("expected FailurePolicy to be nil, got %v", *webhook.FailurePolicy)
   226  				}
   227  
   228  				if tc.whSVC != "" {
   229  					if validationWhConf.Service == nil {
   230  						t.Fatalf("expected validation service %s, got nil", tc.whSVC)
   231  					}
   232  					if validationWhConf.Service.Name != tc.whSVC {
   233  						t.Fatalf("expected validation service %s, got %s", tc.whSVC, validationWhConf.Service.Name)
   234  					}
   235  					if validationWhConf.Service.Namespace != tc.istioNamespace {
   236  						t.Fatalf("expected validation service namespace %s, got %s", tc.istioNamespace, validationWhConf.Service.Namespace)
   237  					}
   238  				}
   239  				if tc.whURL != "" {
   240  					if validationWhConf.URL == nil {
   241  						t.Fatalf("expected validation URL %s, got nil", tc.whURL)
   242  					}
   243  					if *validationWhConf.URL != tc.whURL {
   244  						t.Fatalf("expected validation URL %s, got %s", tc.whURL, *validationWhConf.URL)
   245  					}
   246  				}
   247  				if tc.whCA != "" {
   248  					if string(validationWhConf.CABundle) != tc.whCA {
   249  						t.Fatalf("expected CA bundle %q, got %q", tc.whCA, validationWhConf.CABundle)
   250  					}
   251  				}
   252  			}
   253  		})
   254  	}
   255  }
   256  
   257  func TestGenerateMutatingWebhook(t *testing.T) {
   258  	tcs := []struct {
   259  		name        string
   260  		webhook     admitv1.MutatingWebhookConfiguration
   261  		tagName     string
   262  		whURL       string
   263  		whSVC       string
   264  		whCA        string
   265  		numWebhooks int
   266  	}{
   267  		{
   268  			name:        "webhook-pointing-to-service",
   269  			webhook:     revisionCanonicalWebhook,
   270  			tagName:     "canary",
   271  			whURL:       "",
   272  			whSVC:       "istiod-revision",
   273  			whCA:        "ca",
   274  			numWebhooks: 2,
   275  		},
   276  		{
   277  			name:        "webhook-pointing-to-url",
   278  			webhook:     revisionCanonicalWebhookRemote,
   279  			tagName:     "canary",
   280  			whURL:       remoteInjectionURL,
   281  			whSVC:       "",
   282  			whCA:        "ca",
   283  			numWebhooks: 2,
   284  		},
   285  		{
   286  			name:        "webhook-pointing-to-default-revision",
   287  			webhook:     defaultRevisionCanonicalWebhook,
   288  			tagName:     "canary",
   289  			whURL:       "",
   290  			whSVC:       "istiod",
   291  			whCA:        "ca",
   292  			numWebhooks: 2,
   293  		},
   294  		{
   295  			name:        "webhook-pointing-to-default-revision",
   296  			webhook:     defaultRevisionCanonicalWebhook,
   297  			tagName:     "default",
   298  			whURL:       "",
   299  			whSVC:       "istiod",
   300  			whCA:        "ca",
   301  			numWebhooks: 4,
   302  		},
   303  	}
   304  	scheme := runtime.NewScheme()
   305  	codecFactory := serializer.NewCodecFactory(scheme)
   306  	deserializer := codecFactory.UniversalDeserializer()
   307  
   308  	for _, tc := range tcs {
   309  		webhookConfig, err := tagWebhookConfigFromCanonicalWebhook(tc.webhook, tc.tagName, "istio-system")
   310  		if err != nil {
   311  			t.Fatalf("webhook parsing failed with error: %v", err)
   312  		}
   313  		webhookYAML, err := generateMutatingWebhook(webhookConfig, &GenerateOptions{
   314  			WebhookName:          "",
   315  			ManifestsPath:        filepath.Join(env.IstioSrc, "manifests"),
   316  			AutoInjectNamespaces: false,
   317  			CustomLabels:         nil,
   318  		})
   319  		if err != nil {
   320  			t.Fatalf("tag webhook YAML generation failed with error: %v", err)
   321  		}
   322  
   323  		whObject, _, err := deserializer.Decode([]byte(webhookYAML), nil, &admitv1.MutatingWebhookConfiguration{})
   324  		if err != nil {
   325  			t.Fatalf("could not parse webhook from generated YAML: %s", webhookYAML)
   326  		}
   327  		wh := whObject.(*admitv1.MutatingWebhookConfiguration)
   328  
   329  		// expect both namespace.sidecar-injector.istio.io and object.sidecar-injector.istio.io webhooks
   330  		if len(wh.Webhooks) != tc.numWebhooks {
   331  			t.Errorf("expected %d webhook(s) in MutatingWebhookConfiguration, found %d",
   332  				tc.numWebhooks, len(wh.Webhooks))
   333  		}
   334  		tag, exists := wh.ObjectMeta.Labels[IstioTagLabel]
   335  		if !exists {
   336  			t.Errorf("expected tag webhook to have %s label, did not find", IstioTagLabel)
   337  		}
   338  		if tag != tc.tagName {
   339  			t.Errorf("expected tag webhook to have istio.io/tag=%s, found %s instead", tc.tagName, tag)
   340  		}
   341  
   342  		// ensure all webhooks have the correct client config
   343  		for _, webhook := range wh.Webhooks {
   344  			injectionWhConf := webhook.ClientConfig
   345  			if tc.whSVC != "" {
   346  				if injectionWhConf.Service == nil {
   347  					t.Fatalf("expected injection service %s, got nil", tc.whSVC)
   348  				}
   349  				if injectionWhConf.Service.Name != tc.whSVC {
   350  					t.Fatalf("expected injection service %s, got %s", tc.whSVC, injectionWhConf.Service.Name)
   351  				}
   352  			}
   353  			if tc.whURL != "" {
   354  				if injectionWhConf.URL == nil {
   355  					t.Fatalf("expected injection URL %s, got nil", tc.whURL)
   356  				}
   357  				if *injectionWhConf.URL != tc.whURL {
   358  					t.Fatalf("expected injection URL %s, got %s", tc.whURL, *injectionWhConf.URL)
   359  				}
   360  			}
   361  			if tc.whCA != "" {
   362  				if string(injectionWhConf.CABundle) != tc.whCA {
   363  					t.Fatalf("expected CA bundle %q, got %q", tc.whCA, injectionWhConf.CABundle)
   364  				}
   365  			}
   366  		}
   367  	}
   368  }
   369  
   370  func testGenerateOption(t *testing.T, generate bool, assertFunc func(*testing.T, []admitv1.MutatingWebhook, []admitv1.MutatingWebhook)) {
   371  	defaultWh := defaultRevisionCanonicalWebhook.DeepCopy()
   372  	fakeClient := kube.NewFakeClient(defaultWh)
   373  
   374  	opts := &GenerateOptions{
   375  		Generate: generate,
   376  		Tag:      "default",
   377  		Revision: "default",
   378  	}
   379  
   380  	_, err := Generate(context.TODO(), fakeClient, opts, "istio-system")
   381  	assert.NoError(t, err)
   382  
   383  	wh, err := fakeClient.Kube().AdmissionregistrationV1().MutatingWebhookConfigurations().
   384  		Get(context.Background(), "istio-sidecar-injector", metav1.GetOptions{})
   385  	assert.NoError(t, err)
   386  
   387  	assertFunc(t, wh.Webhooks, defaultWh.Webhooks)
   388  }
   389  
   390  func TestGenerateOptions(t *testing.T) {
   391  	// Test generate option 'true', should not modify webhooks
   392  	testGenerateOption(t, true, func(t *testing.T, actual, expected []admitv1.MutatingWebhook) {
   393  		assert.Equal(t, actual, expected)
   394  	})
   395  
   396  	// Test generate option 'false', should modify webhooks
   397  	testGenerateOption(t, false, func(t *testing.T, actual, expected []admitv1.MutatingWebhook) {
   398  		if err := assert.Compare(actual, expected); err == nil {
   399  			t.Errorf("expected diff between webhooks, got none")
   400  		}
   401  	})
   402  }