224 lines
8.2 KiB
Go
224 lines
8.2 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)
|
|
}
|
|
go informerFactory.Start(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},
|
|
}
|
|
}
|