121 lines
4.1 KiB
Go
121 lines
4.1 KiB
Go
package controller
|
|
|
|
import (
|
|
"fmt"
|
|
"net/netip"
|
|
"strconv"
|
|
"strings"
|
|
|
|
corev1 "k8s.io/api/core/v1"
|
|
|
|
mikrotikv1alpha1 "code.brunner.ninja/feedc0de/mikrotik-operator/api/v1alpha1"
|
|
"code.brunner.ninja/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)
|
|
}
|