You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
tailscale/k8s-operator/reconciler/proxygrouppolicy/proxygrouppolicy_test.go

217 lines
6.6 KiB

// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package proxygrouppolicy_test
import (
"slices"
"strings"
"testing"
admr "k8s.io/api/admissionregistration/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
tsapi "tailscale.com/k8s-operator/apis/v1alpha1"
"tailscale.com/k8s-operator/reconciler/proxygrouppolicy"
)
func TestReconciler_Reconcile(t *testing.T) {
t.Parallel()
tt := []struct {
Name string
Request reconcile.Request
ExpectedPolicyCount int
ExistingResources []client.Object
ExpectsError bool
}{
{
Name: "single-policy-denies-all",
ExpectedPolicyCount: 2,
Request: reconcile.Request{
NamespacedName: types.NamespacedName{
Name: "deny-all",
Namespace: metav1.NamespaceDefault,
},
},
ExistingResources: []client.Object{
&tsapi.ProxyGroupPolicy{
TypeMeta: metav1.TypeMeta{},
ObjectMeta: metav1.ObjectMeta{
Name: "deny-all",
Namespace: metav1.NamespaceDefault,
},
Spec: tsapi.ProxyGroupPolicySpec{
Ingress: []string{},
Egress: []string{},
},
},
},
},
{
Name: "multiple-policies-merged",
ExpectedPolicyCount: 2,
Request: reconcile.Request{
NamespacedName: types.NamespacedName{
Name: "deny-all",
Namespace: metav1.NamespaceDefault,
},
},
ExistingResources: []client.Object{
&tsapi.ProxyGroupPolicy{
TypeMeta: metav1.TypeMeta{},
ObjectMeta: metav1.ObjectMeta{
Name: "deny-all",
Namespace: metav1.NamespaceDefault,
},
Spec: tsapi.ProxyGroupPolicySpec{
Ingress: []string{},
Egress: []string{},
},
},
&tsapi.ProxyGroupPolicy{
TypeMeta: metav1.TypeMeta{},
ObjectMeta: metav1.ObjectMeta{
Name: "allow-one",
Namespace: metav1.NamespaceDefault,
},
Spec: tsapi.ProxyGroupPolicySpec{
Ingress: []string{
"test-ingress",
},
Egress: []string{},
},
},
},
},
{
Name: "no-policies-no-child-resources",
ExpectedPolicyCount: 0,
Request: reconcile.Request{
NamespacedName: types.NamespacedName{
Name: "deny-all",
Namespace: metav1.NamespaceDefault,
},
},
},
}
for _, tc := range tt {
t.Run(tc.Name, func(t *testing.T) {
bldr := fake.NewClientBuilder().WithScheme(tsapi.GlobalScheme)
bldr = bldr.WithObjects(tc.ExistingResources...)
fc := bldr.Build()
opts := proxygrouppolicy.ReconcilerOptions{
Client: fc,
}
reconciler := proxygrouppolicy.NewReconciler(opts)
_, err := reconciler.Reconcile(t.Context(), tc.Request)
if tc.ExpectsError && err == nil {
t.Fatalf("expected error, got none")
}
if !tc.ExpectsError && err != nil {
t.Fatalf("expected no error, got %v", err)
}
var policies admr.ValidatingAdmissionPolicyList
if err = fc.List(t.Context(), &policies); err != nil {
t.Fatal(err)
}
if len(policies.Items) != tc.ExpectedPolicyCount {
t.Fatalf("expected %d ValidatingAdmissionPolicy resources, got %d", tc.ExpectedPolicyCount, len(policies.Items))
}
var bindings admr.ValidatingAdmissionPolicyBindingList
if err = fc.List(t.Context(), &bindings); err != nil {
t.Fatal(err)
}
if len(bindings.Items) != tc.ExpectedPolicyCount {
t.Fatalf("expected %d ValidatingAdmissionPolicyBinding resources, got %d", tc.ExpectedPolicyCount, len(bindings.Items))
}
for _, binding := range bindings.Items {
actual, ok := binding.Spec.MatchResources.NamespaceSelector.MatchLabels["kubernetes.io/metadata.name"]
if !ok || actual != metav1.NamespaceDefault {
t.Fatalf("expected binding to be for default namespace, got %v", actual)
}
if !slices.Contains(binding.Spec.ValidationActions, admr.Deny) {
t.Fatalf("expected binding to be deny, got %v", binding.Spec.ValidationActions)
}
}
for _, policy := range policies.Items {
// Each ValidatingAdmissionPolicy must be set to fail (rejecting resources).
if policy.Spec.FailurePolicy == nil || *policy.Spec.FailurePolicy != admr.Fail {
t.Fatalf("expected fail policy, got %v", *policy.Spec.FailurePolicy)
}
// Each ValidatingAdmissionPolicy must have a matching ValidatingAdmissionPolicyBinding
bound := slices.ContainsFunc(bindings.Items, func(obj admr.ValidatingAdmissionPolicyBinding) bool {
return obj.Spec.PolicyName == policy.Name
})
if !bound {
t.Fatalf("expected policy %s to be bound, but wasn't", policy.Name)
}
// Each ValidatingAdmissionPolicy must be set to evaluate on creation and update of resources.
for _, rule := range policy.Spec.MatchConstraints.ResourceRules {
if !slices.Contains(rule.Operations, admr.Update) {
t.Fatal("expected ingress rule to act on update, but doesn't")
}
if !slices.Contains(rule.Operations, admr.Create) {
t.Fatal("expected ingress rule to act on create, but doesn't")
}
}
// Egress policies should only act on Service resources.
if strings.Contains(policy.Name, "egress") {
if len(policy.Spec.MatchConstraints.ResourceRules) != 1 {
t.Fatalf("expected exactly one matching resource, got %d", len(policy.Spec.MatchConstraints.ResourceRules))
}
rule := policy.Spec.MatchConstraints.ResourceRules[0]
if !slices.Contains(rule.Resources, "services") {
t.Fatal("expected egress rule to act on services, but doesn't")
}
if len(policy.Spec.Validations) != 1 {
t.Fatalf("expected exactly one validation, got %d", len(policy.Spec.Validations))
}
}
// Ingress policies should act on both Ingress and Service resources.
if strings.Contains(policy.Name, "ingress") {
if len(policy.Spec.MatchConstraints.ResourceRules) != 2 {
t.Fatalf("expected exactly two matching resources, got %d", len(policy.Spec.MatchConstraints.ResourceRules))
}
ingressRule := policy.Spec.MatchConstraints.ResourceRules[0]
if !slices.Contains(ingressRule.Resources, "ingresses") {
t.Fatal("expected ingress rule to act on ingresses, but doesn't")
}
serviceRule := policy.Spec.MatchConstraints.ResourceRules[1]
if !slices.Contains(serviceRule.Resources, "services") {
t.Fatal("expected ingress rule to act on services, but doesn't")
}
if len(policy.Spec.Validations) != 2 {
t.Fatalf("expected exactly two validations, got %d", len(policy.Spec.Validations))
}
}
}
})
}
}