istio.io/istio@v0.0.0-20240520182934-d79c90f27776/tests/integration/pilot/validation_test.go (about)

     1  //go:build integ
     2  // +build integ
     3  
     4  // Copyright Istio Authors
     5  //
     6  // Licensed under the Apache License, Version 2.0 (the "License");
     7  // you may not use this file except in compliance with the License.
     8  // You may obtain a copy of the License at
     9  //
    10  //     http://www.apache.org/licenses/LICENSE-2.0
    11  //
    12  // Unless required by applicable law or agreed to in writing, software
    13  // distributed under the License is distributed on an "AS IS" BASIS,
    14  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    15  // See the License for the specific language governing permissions and
    16  // limitations under the License.
    17  
    18  package pilot
    19  
    20  import (
    21  	"fmt"
    22  	"path"
    23  	"strings"
    24  	"testing"
    25  
    26  	json "github.com/go-jose/go-jose/v3/json"
    27  	"sigs.k8s.io/yaml"
    28  
    29  	"istio.io/istio/pkg/config/constants"
    30  	"istio.io/istio/pkg/config/schema/collections"
    31  	"istio.io/istio/pkg/test/datasets/validation"
    32  	"istio.io/istio/pkg/test/framework"
    33  	"istio.io/istio/pkg/test/framework/components/namespace"
    34  	"istio.io/istio/pkg/test/util/yml"
    35  	"istio.io/istio/pkg/util/sets"
    36  )
    37  
    38  type testData string
    39  
    40  func (t testData) load() (string, error) {
    41  	by, err := validation.FS.ReadFile(path.Join("dataset", string(t)))
    42  	if err != nil {
    43  		return "", err
    44  	}
    45  
    46  	return string(by), nil
    47  }
    48  
    49  func loadTestData(t framework.TestContext) []testData {
    50  	entries, err := validation.FS.ReadDir("dataset")
    51  	if err != nil {
    52  		t.Fatalf("Error loading test data: %v", err)
    53  	}
    54  
    55  	var result []testData
    56  	for _, e := range entries {
    57  		result = append(result, testData(e.Name()))
    58  	}
    59  
    60  	return result
    61  }
    62  
    63  func TestValidation(t *testing.T) {
    64  	framework.NewTest(t).
    65  		// Limit to Kube environment as we're testing integration of webhook with K8s.
    66  
    67  		RunParallel(func(t framework.TestContext) {
    68  			dataset := loadTestData(t)
    69  
    70  			denied := func(err error) bool {
    71  				if err == nil {
    72  					return false
    73  				}
    74  				// We are only checking the string literals of the rejection reasons
    75  				// from the webhook and the k8s api server as the returned errors are not
    76  				// k8s typed errors.
    77  				// Note: this explicitly does NOT catch OpenAPI schema rejections - only validating webhook rejections.
    78  				return strings.Contains(err.Error(), "denied the request")
    79  			}
    80  
    81  			for _, cluster := range t.Clusters().Configs() {
    82  				for i := range dataset {
    83  					d := dataset[i]
    84  					for _, valid := range []bool{true, false} {
    85  						t.NewSubTest(string(d) + "-" + fmt.Sprint(valid)).Run(func(t framework.TestContext) {
    86  							ym, err := d.load()
    87  							if err != nil {
    88  								t.Fatalf("Unable to load test data: %v", err)
    89  							}
    90  
    91  							if !valid {
    92  								ym, err = yml.ApplyAnnotation(ym, constants.AlwaysReject, "true")
    93  								if err != nil {
    94  									t.Fatal(err)
    95  								}
    96  							}
    97  
    98  							ns := namespace.NewOrFail(t, t, namespace.Config{
    99  								Prefix: "validation",
   100  							})
   101  
   102  							applyFiles := t.WriteYAMLOrFail(t, "apply", ym)
   103  							dryRunErr := cluster.ApplyYAMLFilesDryRun(ns.Name(), applyFiles...)
   104  
   105  							switch {
   106  							case dryRunErr != nil && valid:
   107  								if denied(dryRunErr) {
   108  									t.Fatalf("got unexpected for valid config: %v", dryRunErr)
   109  								} else {
   110  									t.Fatalf("got unexpected unknown error for valid config: %v", dryRunErr)
   111  								}
   112  							case dryRunErr == nil && !valid:
   113  								t.Fatalf("got unexpected success for invalid config")
   114  							case dryRunErr != nil && !valid:
   115  								if !denied(dryRunErr) {
   116  									t.Fatalf("config request denied for wrong reason: %v", dryRunErr)
   117  								}
   118  							}
   119  
   120  							wetRunErr := cluster.ApplyYAMLFiles(ns.Name(), applyFiles...)
   121  							t.CleanupConditionally(func() {
   122  								cluster.DeleteYAMLFiles(ns.Name(), applyFiles...)
   123  							})
   124  
   125  							if dryRunErr != nil && wetRunErr == nil {
   126  								t.Fatalf("dry run returned error, but wet run returned none: %v", dryRunErr)
   127  							}
   128  							if dryRunErr == nil && wetRunErr != nil {
   129  								t.Fatalf("wet run returned errors, but dry run returned none: %v", wetRunErr)
   130  							}
   131  						})
   132  					}
   133  				}
   134  			}
   135  		})
   136  }
   137  
   138  var ignoredCRDs = []string{
   139  	// We don't validate K8s resources
   140  	"/v1/Endpoints",
   141  	"/v1/Namespace",
   142  	"/v1/Node",
   143  	"/v1/Pod",
   144  	"/v1/Secret",
   145  	"/v1/Service",
   146  	"/v1/ConfigMap",
   147  	"apiextensions.k8s.io/v1/CustomResourceDefinition",
   148  	"admissionregistration.k8s.io/v1/MutatingWebhookConfiguration",
   149  	"apps/v1/Deployment",
   150  	"extensions/v1beta1/Ingress",
   151  }
   152  
   153  func TestEnsureNoMissingCRDs(t *testing.T) {
   154  	// This test ensures that we have necessary tests for all known CRDs. If you're breaking this test, it is likely
   155  	// that you need to update validation tests by either adding new/missing test cases, or removing test cases for
   156  	// types that are no longer supported.
   157  	framework.NewTest(t).
   158  		Run(func(t framework.TestContext) {
   159  			ignored := sets.New(ignoredCRDs...)
   160  			recognized := sets.New[string]()
   161  
   162  			// TODO(jasonwzm) remove this after multi-version APIs are supported.
   163  			for _, r := range collections.Pilot.All() {
   164  				s := strings.Join([]string{r.Group(), r.Version(), r.Kind()}, "/")
   165  				recognized.Insert(s)
   166  				for _, alias := range r.GroupVersionAliasKinds() {
   167  					s = strings.Join([]string{alias.Group, alias.Version, alias.Kind}, "/")
   168  					recognized.Insert(s)
   169  				}
   170  			}
   171  			// These CRDs are validated outside of Istio
   172  			for _, gvk := range []string{
   173  				"gateway.networking.k8s.io/v1/Gateway",
   174  				"gateway.networking.k8s.io/v1beta1/Gateway",
   175  				"gateway.networking.k8s.io/v1/GatewayClass",
   176  				"gateway.networking.k8s.io/v1beta1/GatewayClass",
   177  				"gateway.networking.k8s.io/v1/HTTPRoute",
   178  				"gateway.networking.k8s.io/v1beta1/HTTPRoute",
   179  				"gateway.networking.k8s.io/v1alpha2/TCPRoute",
   180  				"gateway.networking.k8s.io/v1alpha2/TLSRoute",
   181  				"gateway.networking.k8s.io/v1beta1/ReferenceGrant",
   182  				"gateway.networking.k8s.io/v1alpha2/ReferenceGrant",
   183  			} {
   184  				recognized.Delete(gvk)
   185  			}
   186  
   187  			tested := sets.New[string]()
   188  			for _, te := range loadTestData(t) {
   189  				yamlBatch, err := te.load()
   190  				yamlParts := yml.SplitString(yamlBatch)
   191  				for _, yamlPart := range yamlParts {
   192  					if err != nil {
   193  						t.Fatalf("error loading test data: %v", err)
   194  					}
   195  
   196  					m := make(map[string]any)
   197  					by, er := yaml.YAMLToJSON([]byte(yamlPart))
   198  					if er != nil {
   199  						t.Fatalf("error loading test data: %v", er)
   200  					}
   201  					if err = json.Unmarshal(by, &m); err != nil {
   202  						t.Fatalf("error parsing JSON: %v", err)
   203  					}
   204  
   205  					apiVersion := m["apiVersion"].(string)
   206  					kind := m["kind"].(string)
   207  
   208  					key := strings.Join([]string{apiVersion, kind}, "/")
   209  					tested.Insert(key)
   210  				}
   211  			}
   212  
   213  			for rec := range recognized {
   214  				if ignored.Contains(rec) {
   215  					continue
   216  				}
   217  				if !tested.Contains(rec) {
   218  					t.Errorf("CRD does not have a validation test: %v", rec)
   219  				}
   220  			}
   221  
   222  			for tst := range tested {
   223  				if _, found := recognized[tst]; !found {
   224  					t.Errorf("Unrecognized validation test data found: %v", tst)
   225  				}
   226  			}
   227  		})
   228  }