k8s.io/kubernetes@v1.29.3/pkg/controller/validatingadmissionpolicystatus/controller_test.go (about)

     1  /*
     2  Copyright 2023 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package validatingadmissionpolicystatus
    18  
    19  import (
    20  	"context"
    21  	"strings"
    22  	"testing"
    23  	"time"
    24  
    25  	admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
    26  	admissionregistrationv1beta1 "k8s.io/api/admissionregistration/v1beta1"
    27  	"k8s.io/apimachinery/pkg/api/meta/testrestmapper"
    28  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    29  	"k8s.io/apimachinery/pkg/util/wait"
    30  	"k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy"
    31  	"k8s.io/apiserver/pkg/cel/openapi/resolver"
    32  	"k8s.io/client-go/informers"
    33  	"k8s.io/client-go/kubernetes/fake"
    34  	"k8s.io/client-go/kubernetes/scheme"
    35  	"k8s.io/kubernetes/pkg/generated/openapi"
    36  )
    37  
    38  func TestTypeChecking(t *testing.T) {
    39  	for _, tc := range []struct {
    40  		name           string
    41  		policy         *admissionregistrationv1beta1.ValidatingAdmissionPolicy
    42  		assertFieldRef func(warnings []admissionregistrationv1beta1.ExpressionWarning, t *testing.T) // warning.fieldRef
    43  		assertWarnings func(warnings []admissionregistrationv1beta1.ExpressionWarning, t *testing.T) // warning.warning
    44  	}{
    45  		{
    46  			name: "deployment with correct expression",
    47  			policy: withGVRMatch([]string{"apps"}, []string{"v1"}, []string{"deployments"}, withValidations([]admissionregistrationv1beta1.Validation{
    48  				{
    49  					Expression: "object.spec.replicas > 1",
    50  				},
    51  			}, makePolicy("replicated-deployment"))),
    52  			assertFieldRef: toHaveLengthOf(0),
    53  			assertWarnings: toHaveLengthOf(0),
    54  		},
    55  		{
    56  			name: "deployment with type confusion",
    57  			policy: withGVRMatch([]string{"apps"}, []string{"v1"}, []string{"deployments"}, withValidations([]admissionregistrationv1beta1.Validation{
    58  				{
    59  					Expression: "object.spec.replicas < 100", // this one passes
    60  				},
    61  				{
    62  					Expression: "object.spec.replicas > '1'", // '1' should be int
    63  				},
    64  			}, makePolicy("confused-deployment"))),
    65  			assertFieldRef: toBe("spec.validations[1].expression"),
    66  			assertWarnings: toHaveSubstring(`found no matching overload for '_>_' applied to '(int, string)'`),
    67  		},
    68  		{
    69  			name: "two expressions different type checking errors",
    70  			policy: withGVRMatch([]string{"apps"}, []string{"v1"}, []string{"deployments"}, withValidations([]admissionregistrationv1beta1.Validation{
    71  				{
    72  					Expression: "object.spec.nonExistingFirst > 1",
    73  				},
    74  				{
    75  					Expression: "object.spec.replicas > '1'", // '1' should be int
    76  				},
    77  			}, makePolicy("confused-deployment"))),
    78  			assertFieldRef: toBe("spec.validations[0].expression", "spec.validations[1].expression"),
    79  			assertWarnings: toHaveSubstring(
    80  				"undefined field 'nonExistingFirst'",
    81  				`found no matching overload for '_>_' applied to '(int, string)'`,
    82  			),
    83  		},
    84  		{
    85  			name: "one expression, two warnings",
    86  			policy: withGVRMatch([]string{"apps"}, []string{"v1"}, []string{"deployments"}, withValidations([]admissionregistrationv1beta1.Validation{
    87  				{
    88  					Expression: "object.spec.replicas < 100", // this one passes
    89  				},
    90  				{
    91  					Expression: "object.spec.replicas > '1' && object.spec.nonExisting == 1",
    92  				},
    93  			}, makePolicy("confused-deployment"))),
    94  			assertFieldRef: toBe("spec.validations[1].expression"),
    95  			assertWarnings: toHaveMultipleSubstrings([]string{"undefined field 'nonExisting'", `found no matching overload for '_>_' applied to '(int, string)'`}),
    96  		},
    97  	} {
    98  		t.Run(tc.name, func(t *testing.T) {
    99  			ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
   100  			defer cancel()
   101  			policy := tc.policy.DeepCopy()
   102  			policy.ObjectMeta.Generation = 1 // fake storage does not do this automatically
   103  			client := fake.NewSimpleClientset(policy)
   104  			informerFactory := informers.NewSharedInformerFactory(client, 0)
   105  			typeChecker := &validatingadmissionpolicy.TypeChecker{
   106  				SchemaResolver: resolver.NewDefinitionsSchemaResolver(openapi.GetOpenAPIDefinitions, scheme.Scheme),
   107  				RestMapper:     testrestmapper.TestOnlyStaticRESTMapper(scheme.Scheme),
   108  			}
   109  			controller, err := NewController(
   110  				informerFactory.Admissionregistration().V1beta1().ValidatingAdmissionPolicies(),
   111  				client.AdmissionregistrationV1beta1().ValidatingAdmissionPolicies(),
   112  				typeChecker,
   113  			)
   114  			if err != nil {
   115  				t.Fatalf("cannot create controller: %v", err)
   116  			}
   117  			go informerFactory.Start(ctx.Done())
   118  			go controller.Run(ctx, 1)
   119  			err = wait.PollUntilContextCancel(ctx, time.Second, false, func(ctx context.Context) (done bool, err error) {
   120  				name := policy.Name
   121  				// wait until the typeChecking is set, which means the type checking
   122  				// is complete.
   123  				updated, err := client.AdmissionregistrationV1beta1().ValidatingAdmissionPolicies().Get(ctx, name, metav1.GetOptions{})
   124  				if err != nil {
   125  					return false, err
   126  				}
   127  				if updated.Status.TypeChecking != nil {
   128  					policy = updated
   129  					return true, nil
   130  				}
   131  				return false, nil
   132  			})
   133  			if err != nil {
   134  				t.Fatal(err)
   135  			}
   136  			tc.assertFieldRef(policy.Status.TypeChecking.ExpressionWarnings, t)
   137  			tc.assertWarnings(policy.Status.TypeChecking.ExpressionWarnings, t)
   138  			if err != nil {
   139  				t.Fatalf("failed to initialize controller: %v", err)
   140  			}
   141  		})
   142  	}
   143  
   144  }
   145  
   146  func toBe(expected ...string) func(warnings []admissionregistrationv1beta1.ExpressionWarning, t *testing.T) {
   147  	return func(warnings []admissionregistrationv1beta1.ExpressionWarning, t *testing.T) {
   148  		if len(expected) != len(warnings) {
   149  			t.Fatalf("mismatched length, expect %d, got %d", len(expected), len(warnings))
   150  		}
   151  		for i := range expected {
   152  			if expected[i] != warnings[i].FieldRef {
   153  				t.Errorf("expected %q but got %q", expected[i], warnings[i].FieldRef)
   154  			}
   155  		}
   156  	}
   157  }
   158  
   159  func toHaveSubstring(substrings ...string) func(warnings []admissionregistrationv1beta1.ExpressionWarning, t *testing.T) {
   160  	return func(warnings []admissionregistrationv1beta1.ExpressionWarning, t *testing.T) {
   161  		if len(substrings) != len(warnings) {
   162  			t.Fatalf("mismatched length, expect %d, got %d", len(substrings), len(warnings))
   163  		}
   164  		for i := range substrings {
   165  			if !strings.Contains(warnings[i].Warning, substrings[i]) {
   166  				t.Errorf("missing expected substring %q in %v", substrings[i], warnings[i])
   167  			}
   168  		}
   169  	}
   170  }
   171  
   172  func toHaveMultipleSubstrings(substrings ...[]string) func(warnings []admissionregistrationv1beta1.ExpressionWarning, t *testing.T) {
   173  	return func(warnings []admissionregistrationv1beta1.ExpressionWarning, t *testing.T) {
   174  		if len(substrings) != len(warnings) {
   175  			t.Fatalf("mismatched length, expect %d, got %d", len(substrings), len(warnings))
   176  		}
   177  		for i, expectedSubstrings := range substrings {
   178  			for _, s := range expectedSubstrings {
   179  				if !strings.Contains(warnings[i].Warning, s) {
   180  					t.Errorf("missing expected substring %q in %v", substrings[i], warnings[i])
   181  				}
   182  			}
   183  		}
   184  	}
   185  }
   186  
   187  func toHaveLengthOf(n int) func(warnings []admissionregistrationv1beta1.ExpressionWarning, t *testing.T) {
   188  	return func(warnings []admissionregistrationv1beta1.ExpressionWarning, t *testing.T) {
   189  		if n != len(warnings) {
   190  			t.Fatalf("mismatched length, expect %d, got %d", n, len(warnings))
   191  		}
   192  	}
   193  }
   194  
   195  func withGVRMatch(groups []string, versions []string, resources []string, policy *admissionregistrationv1beta1.ValidatingAdmissionPolicy) *admissionregistrationv1beta1.ValidatingAdmissionPolicy {
   196  	policy.Spec.MatchConstraints = &admissionregistrationv1beta1.MatchResources{
   197  		ResourceRules: []admissionregistrationv1beta1.NamedRuleWithOperations{
   198  			{
   199  				RuleWithOperations: admissionregistrationv1beta1.RuleWithOperations{
   200  					Operations: []admissionregistrationv1.OperationType{
   201  						"*",
   202  					},
   203  					Rule: admissionregistrationv1.Rule{
   204  						APIGroups:   groups,
   205  						APIVersions: versions,
   206  						Resources:   resources,
   207  					},
   208  				},
   209  			},
   210  		},
   211  	}
   212  	return policy
   213  }
   214  
   215  func withValidations(validations []admissionregistrationv1beta1.Validation, policy *admissionregistrationv1beta1.ValidatingAdmissionPolicy) *admissionregistrationv1beta1.ValidatingAdmissionPolicy {
   216  	policy.Spec.Validations = validations
   217  	return policy
   218  }
   219  
   220  func makePolicy(name string) *admissionregistrationv1beta1.ValidatingAdmissionPolicy {
   221  	return &admissionregistrationv1beta1.ValidatingAdmissionPolicy{
   222  		ObjectMeta: metav1.ObjectMeta{Name: name},
   223  	}
   224  }