mirror of
				https://github.com/optim-enterprises-bv/kubernetes.git
				synced 2025-11-04 04:08:16 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			225 lines
		
	
	
		
			8.3 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			225 lines
		
	
	
		
			8.3 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
/*
 | 
						|
Copyright 2023 The Kubernetes Authors.
 | 
						|
 | 
						|
Licensed under the Apache License, Version 2.0 (the "License");
 | 
						|
you may not use this file except in compliance with the License.
 | 
						|
You may obtain a copy of the License at
 | 
						|
 | 
						|
    http://www.apache.org/licenses/LICENSE-2.0
 | 
						|
 | 
						|
Unless required by applicable law or agreed to in writing, software
 | 
						|
distributed under the License is distributed on an "AS IS" BASIS,
 | 
						|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
						|
See the License for the specific language governing permissions and
 | 
						|
limitations under the License.
 | 
						|
*/
 | 
						|
 | 
						|
package validatingadmissionpolicystatus
 | 
						|
 | 
						|
import (
 | 
						|
	"context"
 | 
						|
	"strings"
 | 
						|
	"testing"
 | 
						|
	"time"
 | 
						|
 | 
						|
	admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
 | 
						|
	"k8s.io/apimachinery/pkg/api/meta/testrestmapper"
 | 
						|
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 | 
						|
	"k8s.io/apimachinery/pkg/util/wait"
 | 
						|
	validatingadmissionpolicy "k8s.io/apiserver/pkg/admission/plugin/policy/validating"
 | 
						|
	"k8s.io/apiserver/pkg/cel/openapi/resolver"
 | 
						|
	"k8s.io/client-go/informers"
 | 
						|
	"k8s.io/client-go/kubernetes/fake"
 | 
						|
	"k8s.io/client-go/kubernetes/scheme"
 | 
						|
	"k8s.io/kubernetes/pkg/generated/openapi"
 | 
						|
)
 | 
						|
 | 
						|
func TestTypeChecking(t *testing.T) {
 | 
						|
	for _, tc := range []struct {
 | 
						|
		name           string
 | 
						|
		policy         *admissionregistrationv1.ValidatingAdmissionPolicy
 | 
						|
		assertFieldRef func(warnings []admissionregistrationv1.ExpressionWarning, t *testing.T) // warning.fieldRef
 | 
						|
		assertWarnings func(warnings []admissionregistrationv1.ExpressionWarning, t *testing.T) // warning.warning
 | 
						|
	}{
 | 
						|
		{
 | 
						|
			name: "deployment with correct expression",
 | 
						|
			policy: withGVRMatch([]string{"apps"}, []string{"v1"}, []string{"deployments"}, withValidations([]admissionregistrationv1.Validation{
 | 
						|
				{
 | 
						|
					Expression: "object.spec.replicas > 1",
 | 
						|
				},
 | 
						|
			}, makePolicy("replicated-deployment"))),
 | 
						|
			assertFieldRef: toHaveLengthOf(0),
 | 
						|
			assertWarnings: toHaveLengthOf(0),
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name: "deployment with type confusion",
 | 
						|
			policy: withGVRMatch([]string{"apps"}, []string{"v1"}, []string{"deployments"}, withValidations([]admissionregistrationv1.Validation{
 | 
						|
				{
 | 
						|
					Expression: "object.spec.replicas < 100", // this one passes
 | 
						|
				},
 | 
						|
				{
 | 
						|
					Expression: "object.spec.replicas > '1'", // '1' should be int
 | 
						|
				},
 | 
						|
			}, makePolicy("confused-deployment"))),
 | 
						|
			assertFieldRef: toBe("spec.validations[1].expression"),
 | 
						|
			assertWarnings: toHaveSubstring(`found no matching overload for '_>_' applied to '(int, string)'`),
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name: "two expressions different type checking errors",
 | 
						|
			policy: withGVRMatch([]string{"apps"}, []string{"v1"}, []string{"deployments"}, withValidations([]admissionregistrationv1.Validation{
 | 
						|
				{
 | 
						|
					Expression: "object.spec.nonExistingFirst > 1",
 | 
						|
				},
 | 
						|
				{
 | 
						|
					Expression: "object.spec.replicas > '1'", // '1' should be int
 | 
						|
				},
 | 
						|
			}, makePolicy("confused-deployment"))),
 | 
						|
			assertFieldRef: toBe("spec.validations[0].expression", "spec.validations[1].expression"),
 | 
						|
			assertWarnings: toHaveSubstring(
 | 
						|
				"undefined field 'nonExistingFirst'",
 | 
						|
				`found no matching overload for '_>_' applied to '(int, string)'`,
 | 
						|
			),
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name: "one expression, two warnings",
 | 
						|
			policy: withGVRMatch([]string{"apps"}, []string{"v1"}, []string{"deployments"}, withValidations([]admissionregistrationv1.Validation{
 | 
						|
				{
 | 
						|
					Expression: "object.spec.replicas < 100", // this one passes
 | 
						|
				},
 | 
						|
				{
 | 
						|
					Expression: "object.spec.replicas > '1' && object.spec.nonExisting == 1",
 | 
						|
				},
 | 
						|
			}, makePolicy("confused-deployment"))),
 | 
						|
			assertFieldRef: toBe("spec.validations[1].expression"),
 | 
						|
			assertWarnings: toHaveMultipleSubstrings([]string{"undefined field 'nonExisting'", `found no matching overload for '_>_' applied to '(int, string)'`}),
 | 
						|
		},
 | 
						|
	} {
 | 
						|
		t.Run(tc.name, func(t *testing.T) {
 | 
						|
			ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
 | 
						|
			defer cancel()
 | 
						|
			policy := tc.policy.DeepCopy()
 | 
						|
			policy.ObjectMeta.Generation = 1 // fake storage does not do this automatically
 | 
						|
			client := fake.NewSimpleClientset(policy)
 | 
						|
			informerFactory := informers.NewSharedInformerFactory(client, 0)
 | 
						|
			typeChecker := &validatingadmissionpolicy.TypeChecker{
 | 
						|
				SchemaResolver: resolver.NewDefinitionsSchemaResolver(openapi.GetOpenAPIDefinitions, scheme.Scheme),
 | 
						|
				RestMapper:     testrestmapper.TestOnlyStaticRESTMapper(scheme.Scheme),
 | 
						|
			}
 | 
						|
			controller, err := NewController(
 | 
						|
				informerFactory.Admissionregistration().V1().ValidatingAdmissionPolicies(),
 | 
						|
				client.AdmissionregistrationV1().ValidatingAdmissionPolicies(),
 | 
						|
				typeChecker,
 | 
						|
			)
 | 
						|
			if err != nil {
 | 
						|
				t.Fatalf("cannot create controller: %v", err)
 | 
						|
			}
 | 
						|
			informerFactory.Start(ctx.Done())
 | 
						|
			informerFactory.WaitForCacheSync(ctx.Done())
 | 
						|
			go controller.Run(ctx, 1)
 | 
						|
			err = wait.PollUntilContextCancel(ctx, time.Second, false, func(ctx context.Context) (done bool, err error) {
 | 
						|
				name := policy.Name
 | 
						|
				// wait until the typeChecking is set, which means the type checking
 | 
						|
				// is complete.
 | 
						|
				updated, err := client.AdmissionregistrationV1().ValidatingAdmissionPolicies().Get(ctx, name, metav1.GetOptions{})
 | 
						|
				if err != nil {
 | 
						|
					return false, err
 | 
						|
				}
 | 
						|
				if updated.Status.TypeChecking != nil {
 | 
						|
					policy = updated
 | 
						|
					return true, nil
 | 
						|
				}
 | 
						|
				return false, nil
 | 
						|
			})
 | 
						|
			if err != nil {
 | 
						|
				t.Fatal(err)
 | 
						|
			}
 | 
						|
			tc.assertFieldRef(policy.Status.TypeChecking.ExpressionWarnings, t)
 | 
						|
			tc.assertWarnings(policy.Status.TypeChecking.ExpressionWarnings, t)
 | 
						|
			if err != nil {
 | 
						|
				t.Fatalf("failed to initialize controller: %v", err)
 | 
						|
			}
 | 
						|
		})
 | 
						|
	}
 | 
						|
 | 
						|
}
 | 
						|
 | 
						|
func toBe(expected ...string) func(warnings []admissionregistrationv1.ExpressionWarning, t *testing.T) {
 | 
						|
	return func(warnings []admissionregistrationv1.ExpressionWarning, t *testing.T) {
 | 
						|
		if len(expected) != len(warnings) {
 | 
						|
			t.Fatalf("mismatched length, expect %d, got %d", len(expected), len(warnings))
 | 
						|
		}
 | 
						|
		for i := range expected {
 | 
						|
			if expected[i] != warnings[i].FieldRef {
 | 
						|
				t.Errorf("expected %q but got %q", expected[i], warnings[i].FieldRef)
 | 
						|
			}
 | 
						|
		}
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func toHaveSubstring(substrings ...string) func(warnings []admissionregistrationv1.ExpressionWarning, t *testing.T) {
 | 
						|
	return func(warnings []admissionregistrationv1.ExpressionWarning, t *testing.T) {
 | 
						|
		if len(substrings) != len(warnings) {
 | 
						|
			t.Fatalf("mismatched length, expect %d, got %d", len(substrings), len(warnings))
 | 
						|
		}
 | 
						|
		for i := range substrings {
 | 
						|
			if !strings.Contains(warnings[i].Warning, substrings[i]) {
 | 
						|
				t.Errorf("missing expected substring %q in %v", substrings[i], warnings[i])
 | 
						|
			}
 | 
						|
		}
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func toHaveMultipleSubstrings(substrings ...[]string) func(warnings []admissionregistrationv1.ExpressionWarning, t *testing.T) {
 | 
						|
	return func(warnings []admissionregistrationv1.ExpressionWarning, t *testing.T) {
 | 
						|
		if len(substrings) != len(warnings) {
 | 
						|
			t.Fatalf("mismatched length, expect %d, got %d", len(substrings), len(warnings))
 | 
						|
		}
 | 
						|
		for i, expectedSubstrings := range substrings {
 | 
						|
			for _, s := range expectedSubstrings {
 | 
						|
				if !strings.Contains(warnings[i].Warning, s) {
 | 
						|
					t.Errorf("missing expected substring %q in %v", substrings[i], warnings[i])
 | 
						|
				}
 | 
						|
			}
 | 
						|
		}
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func toHaveLengthOf(n int) func(warnings []admissionregistrationv1.ExpressionWarning, t *testing.T) {
 | 
						|
	return func(warnings []admissionregistrationv1.ExpressionWarning, t *testing.T) {
 | 
						|
		if n != len(warnings) {
 | 
						|
			t.Fatalf("mismatched length, expect %d, got %d", n, len(warnings))
 | 
						|
		}
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func withGVRMatch(groups []string, versions []string, resources []string, policy *admissionregistrationv1.ValidatingAdmissionPolicy) *admissionregistrationv1.ValidatingAdmissionPolicy {
 | 
						|
	policy.Spec.MatchConstraints = &admissionregistrationv1.MatchResources{
 | 
						|
		ResourceRules: []admissionregistrationv1.NamedRuleWithOperations{
 | 
						|
			{
 | 
						|
				RuleWithOperations: admissionregistrationv1.RuleWithOperations{
 | 
						|
					Operations: []admissionregistrationv1.OperationType{
 | 
						|
						"*",
 | 
						|
					},
 | 
						|
					Rule: admissionregistrationv1.Rule{
 | 
						|
						APIGroups:   groups,
 | 
						|
						APIVersions: versions,
 | 
						|
						Resources:   resources,
 | 
						|
					},
 | 
						|
				},
 | 
						|
			},
 | 
						|
		},
 | 
						|
	}
 | 
						|
	return policy
 | 
						|
}
 | 
						|
 | 
						|
func withValidations(validations []admissionregistrationv1.Validation, policy *admissionregistrationv1.ValidatingAdmissionPolicy) *admissionregistrationv1.ValidatingAdmissionPolicy {
 | 
						|
	policy.Spec.Validations = validations
 | 
						|
	return policy
 | 
						|
}
 | 
						|
 | 
						|
func makePolicy(name string) *admissionregistrationv1.ValidatingAdmissionPolicy {
 | 
						|
	return &admissionregistrationv1.ValidatingAdmissionPolicy{
 | 
						|
		ObjectMeta: metav1.ObjectMeta{Name: name},
 | 
						|
	}
 | 
						|
}
 |