Files
mikrotik-operator/internal/controller/service_rules.go
T
feedc0de d45d84462d
Validate and publish / Test image and chart (push) Failing after 2m25s
Validate and publish / Publish image and OCI chart (push) Skipped
Initial commit
2026-08-28 23:12:14 +02:00

121 lines
4.1 KiB
Go

package controller
import (
"fmt"
"net/netip"
"strconv"
"strings"
corev1 "k8s.io/api/core/v1"
mikrotikv1alpha1 "github.com/feedc0de/mikrotik-operator/api/v1alpha1"
"github.com/feedc0de/mikrotik-operator/internal/routeros"
)
const AnnotationPrefix = "mikrotik.brunner.ninja/"
const (
AnnotationRouter = AnnotationPrefix + "router"
AnnotationServicePort = AnnotationPrefix + "service-port"
AnnotationDestinationPort = AnnotationPrefix + "dst-port"
AnnotationDestinationIP = AnnotationPrefix + "dst-address"
AnnotationDestinationList = AnnotationPrefix + "dst-address-list"
AnnotationInInterface = AnnotationPrefix + "in-interface"
AnnotationInInterfaceList = AnnotationPrefix + "in-interface-list"
AnnotationSourceAddress = AnnotationPrefix + "src-address"
AnnotationSourceList = AnnotationPrefix + "src-address-list"
)
type RuleIssue struct {
Service string
Message string
}
func DesiredRules(router *mikrotikv1alpha1.MikroTikRouter, services []corev1.Service) ([]routeros.Rule, map[string]struct{}, []RuleIssue) {
desired := make([]routeros.Rule, 0)
preserve := make(map[string]struct{})
issues := make([]RuleIssue, 0)
for i := range services {
service := &services[i]
if service.Annotations[AnnotationRouter] != router.Name || !namespaceAllowed(router.Spec.AllowedNamespaces, service.Namespace) {
continue
}
comment := routeros.ManagedComment(service.Namespace, service.Name)
rule, err := ruleForService(service, comment)
if err != nil {
preserve[comment] = struct{}{}
issues = append(issues, RuleIssue{Service: service.Namespace + "/" + service.Name, Message: err.Error()})
continue
}
desired = append(desired, rule)
}
return desired, preserve, issues
}
func namespaceAllowed(allowed []string, namespace string) bool {
for _, item := range allowed {
if item == "*" || item == namespace {
return true
}
}
return false
}
func ruleForService(service *corev1.Service, comment string) (routeros.Rule, error) {
if service.Spec.Type != corev1.ServiceTypeLoadBalancer {
return routeros.Rule{}, fmt.Errorf("Service must have type LoadBalancer")
}
port, err := selectServicePort(service)
if err != nil {
return routeros.Rule{}, err
}
toAddress := ""
for _, ingress := range service.Status.LoadBalancer.Ingress {
address, parseErr := netip.ParseAddr(ingress.IP)
if parseErr == nil && address.Is4() {
toAddress = address.String()
break
}
}
if toAddress == "" {
return routeros.Rule{}, fmt.Errorf("waiting for an IPv4 address in status.loadBalancer.ingress")
}
destinationPort := service.Annotations[AnnotationDestinationPort]
if destinationPort == "" {
destinationPort = strconv.Itoa(int(port.Port))
}
parsedDestinationPort, err := strconv.ParseUint(destinationPort, 10, 16)
if err != nil || parsedDestinationPort == 0 {
return routeros.Rule{}, fmt.Errorf("%s must be a port from 1 through 65535", AnnotationDestinationPort)
}
return routeros.Rule{
Comment: comment, Protocol: strings.ToLower(string(port.Protocol)),
DestinationPort: destinationPort,
DestinationIP: service.Annotations[AnnotationDestinationIP],
DestinationList: service.Annotations[AnnotationDestinationList],
InInterface: service.Annotations[AnnotationInInterface],
InInterfaceList: service.Annotations[AnnotationInInterfaceList],
SourceAddress: service.Annotations[AnnotationSourceAddress],
SourceList: service.Annotations[AnnotationSourceList],
ToAddress: toAddress, ToPort: strconv.Itoa(int(port.Port)),
}, nil
}
func selectServicePort(service *corev1.Service) (corev1.ServicePort, error) {
selector := service.Annotations[AnnotationServicePort]
if selector == "" {
if len(service.Spec.Ports) != 1 {
return corev1.ServicePort{}, fmt.Errorf("%s is required when the Service has more than one port", AnnotationServicePort)
}
return service.Spec.Ports[0], nil
}
for _, port := range service.Spec.Ports {
if port.Name == selector || strconv.Itoa(int(port.Port)) == selector {
return port, nil
}
}
return corev1.ServicePort{}, fmt.Errorf("%s %q does not match a Service port name or number", AnnotationServicePort, selector)
}