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.
217 lines
6.6 KiB
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))
|
|
}
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|