istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pkg/webhooks/validation/server/server_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 server
    16  
    17  import (
    18  	"bytes"
    19  	"encoding/json"
    20  	"fmt"
    21  	"io"
    22  	"net/http"
    23  	"net/http/httptest"
    24  	"os"
    25  	"path/filepath"
    26  	"strconv"
    27  	"strings"
    28  	"testing"
    29  
    30  	admissionv1 "k8s.io/api/admission/v1"
    31  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    32  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    33  	"k8s.io/apimachinery/pkg/runtime"
    34  
    35  	"istio.io/istio/pkg/config/schema/collections"
    36  	"istio.io/istio/pkg/kube"
    37  	"istio.io/istio/pkg/test/config"
    38  	"istio.io/istio/pkg/testcerts"
    39  )
    40  
    41  const (
    42  	// testDomainSuffix is the default DNS domain suffix for Istio
    43  	// CRD resources.
    44  	testDomainSuffix = "local.cluster"
    45  )
    46  
    47  func TestArgs_String(t *testing.T) {
    48  	p := DefaultArgs()
    49  	// Should not crash
    50  	_ = p.String()
    51  }
    52  
    53  func createTestWebhook(t testing.TB) *Webhook {
    54  	t.Helper()
    55  	dir := t.TempDir()
    56  
    57  	var (
    58  		certFile = filepath.Join(dir, "cert-file.yaml")
    59  		keyFile  = filepath.Join(dir, "key-file.yaml")
    60  		port     = uint(0)
    61  	)
    62  
    63  	// cert
    64  	if err := os.WriteFile(certFile, testcerts.ServerCert, 0o644); err != nil { // nolint: vetshadow
    65  		t.Fatalf("WriteFile(%v) failed: %v", certFile, err)
    66  	}
    67  	// key
    68  	if err := os.WriteFile(keyFile, testcerts.ServerKey, 0o644); err != nil { // nolint: vetshadow
    69  		t.Fatalf("WriteFile(%v) failed: %v", keyFile, err)
    70  	}
    71  
    72  	options := Options{
    73  		Port:         port,
    74  		DomainSuffix: testDomainSuffix,
    75  		Schemas:      collections.Mocks,
    76  		Mux:          http.NewServeMux(),
    77  	}
    78  	wh, err := New(options)
    79  	if err != nil {
    80  		t.Fatalf("New() failed: %v", err)
    81  	}
    82  
    83  	return wh
    84  }
    85  
    86  func makePilotConfig(t *testing.T, i int, validConfig bool, includeBogusKey bool) []byte { // nolint: unparam
    87  	t.Helper()
    88  
    89  	var key string
    90  	if validConfig {
    91  		key = "key"
    92  	}
    93  
    94  	name := fmt.Sprintf("%s%d", "mock-config", i)
    95  
    96  	r := collections.Mock
    97  	var un unstructured.Unstructured
    98  	un.SetGroupVersionKind(r.GroupVersionKind().Kubernetes())
    99  	un.SetName(name)
   100  	un.SetLabels(map[string]string{"key": name})
   101  	un.SetAnnotations(map[string]string{"annotationKey": name})
   102  	un.Object["spec"] = &config.MockConfig{
   103  		Key: key,
   104  		Pairs: []*config.ConfigPair{{
   105  			Key:   key,
   106  			Value: strconv.Itoa(i),
   107  		}},
   108  	}
   109  	raw, err := json.Marshal(&un)
   110  	if err != nil {
   111  		t.Fatalf("Marshal(%v) failed: %v", name, err)
   112  	}
   113  	if includeBogusKey {
   114  		trial := make(map[string]any)
   115  		if err := json.Unmarshal(raw, &trial); err != nil {
   116  			t.Fatalf("Unmarshal(%v) failed: %v", name, err)
   117  		}
   118  		trial["unexpected_key"] = "any value"
   119  		if raw, err = json.Marshal(&trial); err != nil {
   120  			t.Fatalf("re-Marshal(%v) failed: %v", name, err)
   121  		}
   122  	}
   123  	return raw
   124  }
   125  
   126  func TestAdmitPilot(t *testing.T) {
   127  	valid := makePilotConfig(t, 0, true, false)
   128  	invalidConfig := makePilotConfig(t, 0, false, false)
   129  	extraKeyConfig := makePilotConfig(t, 0, true, true)
   130  
   131  	wh := createTestWebhook(t)
   132  
   133  	cases := []struct {
   134  		name    string
   135  		admit   admitFunc
   136  		in      *kube.AdmissionRequest
   137  		allowed bool
   138  	}{
   139  		{
   140  			name:  "valid create",
   141  			admit: wh.validate,
   142  			in: &kube.AdmissionRequest{
   143  				Kind:      metav1.GroupVersionKind{Kind: collections.Mock.Kind()},
   144  				Object:    runtime.RawExtension{Raw: valid},
   145  				Operation: kube.Create,
   146  			},
   147  			allowed: true,
   148  		},
   149  		{
   150  			name:  "valid update",
   151  			admit: wh.validate,
   152  			in: &kube.AdmissionRequest{
   153  				Kind:      metav1.GroupVersionKind{Kind: collections.Mock.Kind()},
   154  				Object:    runtime.RawExtension{Raw: valid},
   155  				Operation: kube.Update,
   156  			},
   157  			allowed: true,
   158  		},
   159  		{
   160  			name:  "unsupported operation",
   161  			admit: wh.validate,
   162  			in: &kube.AdmissionRequest{
   163  				Kind:      metav1.GroupVersionKind{Kind: collections.Mock.Kind()},
   164  				Object:    runtime.RawExtension{Raw: valid},
   165  				Operation: kube.Delete,
   166  			},
   167  			allowed: true,
   168  		},
   169  		{
   170  			name:  "invalid spec",
   171  			admit: wh.validate,
   172  			in: &kube.AdmissionRequest{
   173  				Kind:      metav1.GroupVersionKind{Kind: collections.Mock.Kind()},
   174  				Object:    runtime.RawExtension{Raw: invalidConfig},
   175  				Operation: kube.Create,
   176  			},
   177  			allowed: false,
   178  		},
   179  		{
   180  			name:  "corrupt object",
   181  			admit: wh.validate,
   182  			in: &kube.AdmissionRequest{
   183  				Kind:      metav1.GroupVersionKind{Kind: collections.Mock.Kind()},
   184  				Object:    runtime.RawExtension{Raw: append([]byte("---"), valid...)},
   185  				Operation: kube.Create,
   186  			},
   187  			allowed: false,
   188  		},
   189  		{
   190  			name:  "invalid extra key create",
   191  			admit: wh.validate,
   192  			in: &kube.AdmissionRequest{
   193  				Kind:      metav1.GroupVersionKind{Kind: collections.Mock.Kind()},
   194  				Object:    runtime.RawExtension{Raw: extraKeyConfig},
   195  				Operation: kube.Create,
   196  			},
   197  			allowed: false,
   198  		},
   199  	}
   200  
   201  	for i, c := range cases {
   202  		t.Run(fmt.Sprintf("[%d] %s", i, c.name), func(t *testing.T) {
   203  			got := wh.validate(c.in)
   204  			if got.Allowed != c.allowed {
   205  				t.Fatalf("got %v want %v", got.Allowed, c.allowed)
   206  			}
   207  		})
   208  	}
   209  }
   210  
   211  func makeTestReview(t *testing.T, valid bool, apiVersion string) []byte {
   212  	t.Helper()
   213  	review := admissionv1.AdmissionReview{
   214  		TypeMeta: metav1.TypeMeta{
   215  			Kind:       "AdmissionReview",
   216  			APIVersion: fmt.Sprintf("admission.k8s.io/%s", apiVersion),
   217  		},
   218  		Request: &admissionv1.AdmissionRequest{
   219  			Kind: metav1.GroupVersionKind{
   220  				Group:   admissionv1.GroupName,
   221  				Version: apiVersion,
   222  				Kind:    "AdmissionRequest",
   223  			},
   224  			Object: runtime.RawExtension{
   225  				Raw: makePilotConfig(t, 0, valid, false),
   226  			},
   227  			Operation: admissionv1.Create,
   228  		},
   229  	}
   230  	reviewJSON, err := json.Marshal(review)
   231  	if err != nil {
   232  		t.Fatalf("Failed to create AdmissionReview: %v", err)
   233  	}
   234  	return reviewJSON
   235  }
   236  
   237  func TestServe(t *testing.T) {
   238  	_ = createTestWebhook(t)
   239  	stop := make(chan struct{})
   240  	defer func() {
   241  		close(stop)
   242  	}()
   243  
   244  	validReview := makeTestReview(t, true, "v1beta1")
   245  	validReviewV1 := makeTestReview(t, true, "v1")
   246  	invalidReview := makeTestReview(t, false, "v1beta1")
   247  
   248  	cases := []struct {
   249  		name            string
   250  		body            []byte
   251  		contentType     string
   252  		wantStatusCode  int
   253  		wantAllowed     bool
   254  		allowedResponse bool
   255  	}{
   256  		{
   257  			name:            "valid",
   258  			body:            validReview,
   259  			contentType:     "application/json",
   260  			wantAllowed:     true,
   261  			wantStatusCode:  http.StatusOK,
   262  			allowedResponse: true,
   263  		},
   264  		{
   265  			name:            "valid(v1 version)",
   266  			body:            validReviewV1,
   267  			contentType:     "application/json",
   268  			wantAllowed:     true,
   269  			wantStatusCode:  http.StatusOK,
   270  			allowedResponse: true,
   271  		},
   272  		{
   273  			name:           "invalid",
   274  			body:           invalidReview,
   275  			contentType:    "application/json",
   276  			wantAllowed:    false,
   277  			wantStatusCode: http.StatusOK,
   278  		},
   279  		{
   280  			name:           "wrong content-type",
   281  			body:           validReview,
   282  			contentType:    "application/yaml",
   283  			wantAllowed:    false,
   284  			wantStatusCode: http.StatusUnsupportedMediaType,
   285  		},
   286  		{
   287  			name:           "bad content",
   288  			body:           []byte{0, 1, 2, 3, 4, 5}, // random data
   289  			contentType:    "application/json",
   290  			wantAllowed:    false,
   291  			wantStatusCode: http.StatusOK,
   292  		},
   293  		{
   294  			name:           "no content",
   295  			body:           []byte{},
   296  			contentType:    "application/json",
   297  			wantAllowed:    false,
   298  			wantStatusCode: http.StatusBadRequest,
   299  		},
   300  	}
   301  
   302  	for i, c := range cases {
   303  		t.Run(fmt.Sprintf("[%d] %s", i, c.name), func(t *testing.T) {
   304  			req := httptest.NewRequest("POST", "http://validator", bytes.NewReader(c.body))
   305  			req.Header.Add("Content-Type", c.contentType)
   306  			w := httptest.NewRecorder()
   307  
   308  			serve(w, req, func(*kube.AdmissionRequest) *kube.AdmissionResponse {
   309  				return &kube.AdmissionResponse{Allowed: c.allowedResponse}
   310  			})
   311  
   312  			res := w.Result()
   313  
   314  			if res.StatusCode != c.wantStatusCode {
   315  				t.Fatalf("%v: wrong status code: \ngot %v \nwant %v", c.name, res.StatusCode, c.wantStatusCode)
   316  			}
   317  
   318  			if res.StatusCode != http.StatusOK {
   319  				return
   320  			}
   321  
   322  			gotBody, err := io.ReadAll(res.Body)
   323  			if err != nil {
   324  				t.Fatalf("%v: could not read body: %v", c.name, err)
   325  			}
   326  			var gotReview admissionv1.AdmissionReview
   327  			if err := json.Unmarshal(gotBody, &gotReview); err != nil {
   328  				t.Fatalf("%v: could not decode response body: %v", c.name, err)
   329  			}
   330  			if gotReview.Response.Allowed != c.wantAllowed {
   331  				t.Fatalf("%v: AdmissionReview.Response.Allowed is wrong : got %v want %v",
   332  					c.name, gotReview.Response.Allowed, c.wantAllowed)
   333  			}
   334  		})
   335  	}
   336  }
   337  
   338  // scenario is a common struct used by many tests in this context.
   339  type scenario struct {
   340  	wrapFunc      func(*Options)
   341  	expectedError string
   342  }
   343  
   344  func TestValidate(t *testing.T) {
   345  	scenarios := map[string]scenario{
   346  		"valid": {
   347  			wrapFunc:      func(args *Options) {},
   348  			expectedError: "",
   349  		},
   350  		"invalid port": {
   351  			wrapFunc:      func(args *Options) { args.Port = 100000 },
   352  			expectedError: "port number 100000 must be in the range 1..65535",
   353  		},
   354  	}
   355  
   356  	for name, scenario := range scenarios {
   357  		t.Run(name, func(tt *testing.T) {
   358  			runTestCode(name, tt, scenario)
   359  		})
   360  	}
   361  }
   362  
   363  func runTestCode(name string, t *testing.T, test scenario) {
   364  	args := DefaultArgs()
   365  
   366  	test.wrapFunc(&args)
   367  	err := args.Validate()
   368  	if err == nil && test.expectedError != "" {
   369  		t.Errorf("Test %q failed: expected error: %q, got nil", name, test.expectedError)
   370  	}
   371  	if err != nil {
   372  		if test.expectedError == "" {
   373  			t.Errorf("Test %q failed: expected nil error, got %v", name, err)
   374  		}
   375  		if !strings.Contains(err.Error(), test.expectedError) {
   376  			t.Errorf("Test %q failed: expected error: %q, got %q", name, test.expectedError, err.Error())
   377  		}
   378  	}
   379  }