Files
feedc0de fd5f5c454f
Validate and publish / Test image and chart (push) Successful in 2m33s
Validate and publish / Publish image and OCI chart (push) Successful in 22s
Support multiple service port forwardings
2026-08-31 14:13:56 +02:00

154 lines
5.9 KiB
Go

package controller
import (
"testing"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
mikrotikv1alpha1 "code.brunner.ninja/feedc0de/mikrotik-operator/api/v1alpha1"
"code.brunner.ninja/feedc0de/mikrotik-operator/internal/routeros"
)
func TestDesiredRules(t *testing.T) {
router := &mikrotikv1alpha1.MikroTikRouter{
ObjectMeta: metav1.ObjectMeta{Name: "edge"},
Spec: mikrotikv1alpha1.MikroTikRouterSpec{AllowedNamespaces: []string{"games"}},
}
service := corev1.Service{
ObjectMeta: metav1.ObjectMeta{
Namespace: "games", Name: "factorio",
Annotations: map[string]string{
AnnotationRouter: "edge", AnnotationServicePort: "game",
AnnotationDestinationPort: "40000", AnnotationInInterfaceList: "WAN",
},
},
Spec: corev1.ServiceSpec{
Type: corev1.ServiceTypeLoadBalancer,
Ports: []corev1.ServicePort{
{Name: "game", Port: 34197, Protocol: corev1.ProtocolUDP},
{Name: "rcon", Port: 27015, Protocol: corev1.ProtocolTCP},
},
},
Status: corev1.ServiceStatus{LoadBalancer: corev1.LoadBalancerStatus{Ingress: []corev1.LoadBalancerIngress{
{IP: "2001:db8::1"}, {IP: "192.168.0.17"},
}}},
}
rules, preserve, issues := DesiredRules(router, []corev1.Service{service})
if len(issues) != 0 {
t.Fatalf("unexpected issues: %#v", issues)
}
if len(preserve) != 0 {
t.Fatalf("unexpected preserve set: %#v", preserve)
}
if len(rules) != 1 {
t.Fatalf("got %d rules, want 1", len(rules))
}
want := routeros.Rule{
Comment: routeros.ManagedComment("games", "factorio"), Protocol: "udp",
DestinationPort: "40000", InInterfaceList: "WAN",
ToAddress: "192.168.0.17", ToPort: "34197",
}
if rules[0] != want {
t.Fatalf("rule = %#v, want %#v", rules[0], want)
}
}
func TestDesiredRulesPreservesRuleWhileLoadBalancerIsPending(t *testing.T) {
router := &mikrotikv1alpha1.MikroTikRouter{
ObjectMeta: metav1.ObjectMeta{Name: "edge"},
Spec: mikrotikv1alpha1.MikroTikRouterSpec{AllowedNamespaces: []string{"games"}},
}
service := corev1.Service{
ObjectMeta: metav1.ObjectMeta{Namespace: "games", Name: "factorio", Annotations: map[string]string{AnnotationRouter: "edge"}},
Spec: corev1.ServiceSpec{Type: corev1.ServiceTypeLoadBalancer, Ports: []corev1.ServicePort{{Port: 34197, Protocol: corev1.ProtocolUDP}}},
}
rules, preserve, issues := DesiredRules(router, []corev1.Service{service})
if len(rules) != 0 || len(issues) != 1 {
t.Fatalf("rules=%#v issues=%#v", rules, issues)
}
if _, ok := preserve[routeros.ManagedComment("games", "factorio")]; !ok {
t.Fatal("pending rule was not preserved")
}
}
func TestDesiredRulesDoesNotHonorUnlistedNamespace(t *testing.T) {
router := &mikrotikv1alpha1.MikroTikRouter{
ObjectMeta: metav1.ObjectMeta{Name: "edge"},
Spec: mikrotikv1alpha1.MikroTikRouterSpec{AllowedNamespaces: []string{"trusted"}},
}
service := corev1.Service{ObjectMeta: metav1.ObjectMeta{
Namespace: "untrusted", Name: "bad", Annotations: map[string]string{AnnotationRouter: "edge"},
}}
rules, preserve, issues := DesiredRules(router, []corev1.Service{service})
if len(rules) != 0 || len(preserve) != 0 || len(issues) != 0 {
t.Fatalf("unlisted namespace affected reconciliation: rules=%#v preserve=%#v issues=%#v", rules, preserve, issues)
}
}
func TestDesiredRulesSupportsNamedForwardingsAndRanges(t *testing.T) {
router := &mikrotikv1alpha1.MikroTikRouter{
ObjectMeta: metav1.ObjectMeta{Name: "edge"},
Spec: mikrotikv1alpha1.MikroTikRouterSpec{AllowedNamespaces: []string{"*"}},
}
service := corev1.Service{
ObjectMeta: metav1.ObjectMeta{
Namespace: "coturn", Name: "coturn",
Annotations: map[string]string{
AnnotationRouter: "edge",
AnnotationForwardings: `[
{"name":"turn","servicePort":"turn","dstAddressList":"WANs"},
{"name":"relay","servicePortRange":"49160-49162","dstAddressList":"WANs"}
]`,
},
},
Spec: corev1.ServiceSpec{
Type: corev1.ServiceTypeLoadBalancer,
Ports: []corev1.ServicePort{
{Name: "turn", Port: 3478, Protocol: corev1.ProtocolUDP},
{Name: "relay-49160", Port: 49160, Protocol: corev1.ProtocolUDP},
{Name: "relay-49161", Port: 49161, Protocol: corev1.ProtocolUDP},
{Name: "relay-49162", Port: 49162, Protocol: corev1.ProtocolUDP},
},
},
Status: corev1.ServiceStatus{LoadBalancer: corev1.LoadBalancerStatus{Ingress: []corev1.LoadBalancerIngress{{IP: "192.168.0.19"}}}},
}
rules, preserve, issues := DesiredRules(router, []corev1.Service{service})
if len(issues) != 0 || len(preserve) != 0 {
t.Fatalf("issues=%#v preserve=%#v", issues, preserve)
}
if len(rules) != 2 {
t.Fatalf("got %d rules, want 2", len(rules))
}
want := []routeros.Rule{
{Comment: routeros.ManagedForwardingComment("coturn", "coturn", "turn"), Protocol: "udp", DestinationPort: "3478", DestinationList: "WANs", ToAddress: "192.168.0.19", ToPort: "3478"},
{Comment: routeros.ManagedForwardingComment("coturn", "coturn", "relay"), Protocol: "udp", DestinationPort: "49160-49162", DestinationList: "WANs", ToAddress: "192.168.0.19", ToPort: "49160-49162"},
}
for i := range want {
if rules[i] != want[i] {
t.Fatalf("rule[%d] = %#v, want %#v", i, rules[i], want[i])
}
}
}
func TestDesiredRulesPreservesAllNamedRulesForInvalidJSON(t *testing.T) {
router := &mikrotikv1alpha1.MikroTikRouter{
ObjectMeta: metav1.ObjectMeta{Name: "edge"},
Spec: mikrotikv1alpha1.MikroTikRouterSpec{AllowedNamespaces: []string{"*"}},
}
service := corev1.Service{ObjectMeta: metav1.ObjectMeta{
Namespace: "default", Name: "traefik", Annotations: map[string]string{
AnnotationRouter: "edge", AnnotationForwardings: `[{broken]`,
},
}}
rules, preserve, issues := DesiredRules(router, []corev1.Service{service})
if len(rules) != 0 || len(issues) != 1 {
t.Fatalf("rules=%#v issues=%#v", rules, issues)
}
if _, ok := preserve[routeros.ManagedComment("default", "traefik")+"/"]; !ok {
t.Fatalf("named forwarding prefix was not preserved: %#v", preserve)
}
}