Skip to content

Commit

Permalink
Add validation unit tests
Browse files Browse the repository at this point in the history
currently fails with bazel visibility error, see issue
google/cel-go#947

in order to run test with visibility check excluded:
```
hack/dockerized bazel test --test_output=errors --cache_test_results=no
--check_visibility=false //pkg/virt-operator/...
```

Signed-off-by: Ram Lavi <[email protected]>
  • Loading branch information
RamLavi committed Nov 5, 2024
1 parent d1445a9 commit 83f07d4
Show file tree
Hide file tree
Showing 2 changed files with 248 additions and 4 deletions.
5 changes: 5 additions & 0 deletions pkg/virt-operator/resource/generate/components/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ go_test(
"//staging/src/kubevirt.io/api/core/v1:go_default_library",
"//staging/src/kubevirt.io/client-go/testutils:go_default_library",
"//vendor/github.com/google/cel-go/cel:go_default_library",
"//vendor/github.com/google/cel-go/common/types:go_default_library",
"//vendor/github.com/onsi/ginkgo/v2:go_default_library",
"//vendor/github.com/onsi/gomega:go_default_library",
"//vendor/github.com/openshift/api/route/v1:go_default_library",
Expand All @@ -95,7 +96,11 @@ go_test(
"//vendor/k8s.io/api/core/v1:go_default_library",
"//vendor/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/types:go_default_library",
"//vendor/k8s.io/apiserver/pkg/admission:go_default_library",
"//vendor/k8s.io/apiserver/pkg/admission/plugin/cel:go_default_library",
"//vendor/k8s.io/apiserver/pkg/apis/cel:go_default_library",
"//vendor/k8s.io/apiserver/pkg/cel/environment:go_default_library",
],
)
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,25 @@
package components_test

import (
"context"
"fmt"
"strings"

celgo "github.com/google/cel-go/cel"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"k8s.io/apiserver/pkg/cel/environment"

celgo "github.com/google/cel-go/cel"
celtypes "github.com/google/cel-go/common/types"

admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/admission/plugin/cel"
celconfig "k8s.io/apiserver/pkg/apis/cel"
"k8s.io/apiserver/pkg/cel/environment"

"kubevirt.io/kubevirt/pkg/virt-operator/resource/generate/components"
)
Expand All @@ -55,7 +64,6 @@ var _ = Describe("Validation Admission Policy", func() {
Expect(validatingAdmissionPolicy.Spec.MatchConditions[0].Expression).To(Equal(expectedMatchConditionExpression))
Expect(validatingAdmissionPolicy.Kind).ToNot(BeEmpty())
})

Context("Validation Compile test", func() {
var celCompiler *cel.CompositedCompiler
BeforeEach(func() {
Expand All @@ -64,7 +72,7 @@ var _ = Describe("Validation Admission Policy", func() {
celCompiler = cel.NewCompositedCompilerFromTemplate(compositionEnvTemplateWithoutStrictCost)
})

It("succeed compiling all the policy validations", func() {
It("succeed compiling all the policy validations with variables", func() {
const userName = "system:serviceaccount:kubevirt-ns:kubevirt-handler"
validatingAdmissionPolicy := components.NewHandlerV1ValidatingAdmissionPolicy(userName)

Expand All @@ -82,6 +90,130 @@ var _ = Describe("Validation Admission Policy", func() {
}
})
})
Context("Validation Filter test", func() {
var celCompiler *cel.CompositedCompiler
const nodeName = "node01"
BeforeEach(func() {
compositionEnvTemplateWithoutStrictCost, err := cel.NewCompositionEnv(cel.VariablesTypeName, environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), false))
Expect(err).ToNot(HaveOccurred())
celCompiler = cel.NewCompositedCompilerFromTemplate(compositionEnvTemplateWithoutStrictCost)
})
DescribeTable("should succeed patching the node with allowed actions", func(oldNode, newNode *corev1.Node) {
const userName = "system:serviceaccount:kubevirt-ns:kubevirt-handler"
validatingAdmissionPolicy := components.NewHandlerV1ValidatingAdmissionPolicy(userName)

// currently variables are not calculated when running the filter.
// to work around it - replacing variables args in validations' expression.
injectVariablesToValidations(validatingAdmissionPolicy.Spec.Validations, validatingAdmissionPolicy.Spec.Variables)

filterResults := compileValidations(validatingAdmissionPolicy.Spec.Validations, celCompiler)
Expect(filterResults.CompilationErrors()).To(HaveLen(0))

versionedAttr, err := setNodeUpdateAttribute(oldNode, newNode)
Expect(err).ToNot(HaveOccurred())

evalResults, _, err := filterResults.ForInput(
context.TODO(),
versionedAttr,
cel.CreateAdmissionRequest(versionedAttr.Attributes, metav1.GroupVersionResource(versionedAttr.GetResource()), metav1.GroupVersionKind(versionedAttr.VersionedKind)),
cel.OptionalVariableBindings{},
nil,
celconfig.RuntimeCELCostBudget)
Expect(err).ToNot(HaveOccurred())

for resultIdx := range evalResults {
result := evalResults[resultIdx]
validation := validatingAdmissionPolicy.Spec.Validations[resultIdx]
Expect(result.Error).To(BeNil(), fmt.Sprintf("validation policy expression %q failed", result.ExpressionAccessor.GetExpression()))
Expect(result.EvalResult).To(Equal(celtypes.True), fmt.Sprintf("validation policy expression %q returned false. reason given: %q", result.ExpressionAccessor.GetExpression(), validation.Message))
}
},
Entry("when adding a kubevirt-owned annotation",
newNode(nodeName),
newNode(nodeName, withAnnotations(map[string]string{"kubevirt.io/permittedAnnotation": ""}))),
Entry("when adding a kubevirt-owned label",
newNode(nodeName),
newNode(nodeName, withLabels(map[string]string{"kubevirt.io/permittedLabel": "", "cpumanager": "true"}))),
)

DescribeTable("should fail patching the node with not allowed actions", func(oldNode, newNode *corev1.Node, expectedErrMessage string) {
const userName = "system:serviceaccount:kubevirt-ns:kubevirt-handler"
validatingAdmissionPolicy := components.NewHandlerV1ValidatingAdmissionPolicy(userName)

// currently variables are not calculated when running the filter.
// to work around it - replacing variables args in validations' expression.
injectVariablesToValidations(validatingAdmissionPolicy.Spec.Validations, validatingAdmissionPolicy.Spec.Variables)

filterResults := compileValidations(validatingAdmissionPolicy.Spec.Validations, celCompiler)
Expect(filterResults.CompilationErrors()).To(HaveLen(0))

versionedAttr, err := setNodeUpdateAttribute(oldNode, newNode)
Expect(err).ToNot(HaveOccurred())

evalResults, _, err := filterResults.ForInput(
context.TODO(),
versionedAttr,
cel.CreateAdmissionRequest(versionedAttr.Attributes, metav1.GroupVersionResource(versionedAttr.GetResource()), metav1.GroupVersionKind(versionedAttr.VersionedKind)),
cel.OptionalVariableBindings{},
nil,
celconfig.RuntimeCELCostBudget)
Expect(err).ToNot(HaveOccurred())

var resultIdxFailures []int
for resultIdx := range evalResults {
result := evalResults[resultIdx]
Expect(result.Error).To(BeNil(), fmt.Sprintf("validation policy expression %q failed", result.ExpressionAccessor.GetExpression()))
if result.EvalResult == celtypes.False {
resultIdxFailures = append(resultIdxFailures, resultIdx)
}
}

getErrMessage := func(resultIdx int) string { return validatingAdmissionPolicy.Spec.Validations[resultIdx].Message }
Expect(resultIdxFailures).To(ContainElement(
WithTransform(getErrMessage, Equal(expectedErrMessage))), fmt.Sprintf("validation did not fail with expected error message %q", expectedErrMessage))
},
Entry("when changing node spec",
newNode(nodeName),
newNode(nodeName, withUnschedulable(true)),
components.NodeRestrictionErrModifySpec,
),
Entry("when changing not allowed metadata field",
newNode(nodeName),
newNode(nodeName, withOwnerReference("user", "1234")),
components.NodeRestrictionErrChangeMetadataFields,
),
Entry("when adding a non kubevirt-owned label",
newNode(nodeName),
newNode(nodeName, withLabels(map[string]string{"other.io/notPermittedLabel": ""})),
components.NodeRestrictionErrAddDeleteLabels,
),
Entry("when updating a non kubevirt-owned label",
newNode(nodeName, withLabels(map[string]string{"other.io/notPermittedLabel": "old-value"})),
newNode(nodeName, withLabels(map[string]string{"other.io/notPermittedLabel": "new-value"})),
components.NodeRestrictionErrUpdateLabels,
),
Entry("when removing a non kubevirt-owned label",
newNode(nodeName, withLabels(map[string]string{"other.io/notPermittedLabel": "old-value"})),
newNode(nodeName),
components.NodeRestrictionErrAddDeleteLabels,
),
Entry("when adding a non kubevirt-owned annotation",
newNode(nodeName),
newNode(nodeName, withAnnotations(map[string]string{"other.io/notPermittedAnnotation": ""})),
components.NodeRestrictionErrAddDeleteAnnotations,
),
Entry("when updating a non kubevirt-owned annotation",
newNode(nodeName, withAnnotations(map[string]string{"other.io/notPermittedAnnotation": "old-value"})),
newNode(nodeName, withAnnotations(map[string]string{"other.io/notPermittedAnnotation": "new-value"})),
components.NodeRestrictionErrUpdateAnnotations,
),
Entry("when removing a non kubevirt-owned annotation",
newNode(nodeName, withAnnotations(map[string]string{"other.io/notPermittedAnnotation": "old-value"})),
newNode(nodeName),
components.NodeRestrictionErrAddDeleteAnnotations,
),
)
})
})

})
Expand Down Expand Up @@ -134,3 +266,110 @@ func convertV1Validation(validation admissionregistrationv1.Validation) cel.Expr
Reason: validation.Reason,
}
}

// newObjectInterfacesForTest returns an ObjectInterfaces appropriate for test cases in this file.
func newObjectInterfacesForTest() admission.ObjectInterfaces {
scheme := runtime.NewScheme()
corev1.AddToScheme(scheme)
return admission.NewObjectInterfacesFromScheme(scheme)
}

func injectVariablesToValidations(validations []admissionregistrationv1.Validation, variables []admissionregistrationv1.Variable) {
for idx, _ := range validations {
for _, variable := range variables {
validations[idx].Expression = strings.ReplaceAll(validations[idx].Expression, "variables."+variable.Name, variable.Expression)
}
}
}

func compileValidations(validations []admissionregistrationv1.Validation, celCompiler *cel.CompositedCompiler) cel.Filter {
var expressions []cel.ExpressionAccessor
for _, validation := range validations {
expressions = append(expressions, convertV1Validation(validation))
}
options := cel.OptionalVariableDeclarations{
HasParams: false,
HasAuthorizer: false,
}
mode := environment.NewExpressions
return celCompiler.FilterCompiler.Compile(expressions, options, mode)
}

func setNodeUpdateAttribute(oldNode, newNode *corev1.Node) (*admission.VersionedAttributes, error) {
nodeAttribiute := admission.NewAttributesRecord(
oldNode,
newNode,
corev1.SchemeGroupVersion.WithKind("Node"),
corev1.NamespaceAll,
oldNode.Name,
corev1.SchemeGroupVersion.WithResource("nodes"),
"",
admission.Update,
&metav1.CreateOptions{},
false,
nil,
)
return admission.NewVersionedAttributes(nodeAttribiute, nodeAttribiute.GetKind(), newObjectInterfacesForTest())
}

type Option func(node *corev1.Node)

func newNode(nodeName string, options ...Option) *corev1.Node {
node := &corev1.Node{
ObjectMeta: metav1.ObjectMeta{
Name: nodeName,
Labels: map[string]string{"label1": "val1"},
Annotations: map[string]string{"annotations1": "val1"},
},
Spec: corev1.NodeSpec{},
}

for _, f := range options {
f(node)
}

return node
}

func withOwnerReference(ownerName, ownerUID string) Option {
return func(node *corev1.Node) {
if ownerUID != "" && ownerName != "" {
node.ObjectMeta.OwnerReferences = append(node.ObjectMeta.OwnerReferences, metav1.OwnerReference{
APIVersion: "v1",
Kind: "Pod",
Name: ownerName,
UID: types.UID(ownerUID),
})
}
}
}

func withAnnotations(annotations map[string]string) Option {
return func(node *corev1.Node) {
if node.Annotations == nil {
node.ObjectMeta.Annotations = annotations
} else {
for annotation, value := range annotations {
node.ObjectMeta.Annotations[annotation] = value
}
}
}
}

func withLabels(labels map[string]string) Option {
return func(node *corev1.Node) {
if node.Labels == nil {
node.ObjectMeta.Labels = labels
} else {
for label, value := range labels {
node.ObjectMeta.Labels[label] = value
}
}
}
}

func withUnschedulable(unschedulable bool) Option {
return func(node *corev1.Node) {
node.Spec.Unschedulable = unschedulable
}
}

0 comments on commit 83f07d4

Please sign in to comment.