diff --git a/bundle/manifests/netobserv-operator.clusterserviceversion.yaml b/bundle/manifests/netobserv-operator.clusterserviceversion.yaml index 5a320a447..c2b67a38b 100644 --- a/bundle/manifests/netobserv-operator.clusterserviceversion.yaml +++ b/bundle/manifests/netobserv-operator.clusterserviceversion.yaml @@ -1002,6 +1002,14 @@ spec: - list - update - watch + - apiGroups: + - operator.openshift.io + resources: + - networks + verbs: + - get + - list + - watch - apiGroups: - rbac.authorization.k8s.io resources: diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 013f2f565..63d7a49c4 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -228,6 +228,14 @@ rules: - list - update - watch +- apiGroups: + - operator.openshift.io + resources: + - networks + verbs: + - get + - list + - watch - apiGroups: - rbac.authorization.k8s.io resources: diff --git a/helm/templates/clusterrole.yaml b/helm/templates/clusterrole.yaml index 8d2c4e85f..35d43a950 100644 --- a/helm/templates/clusterrole.yaml +++ b/helm/templates/clusterrole.yaml @@ -227,6 +227,14 @@ rules: - list - update - watch + - apiGroups: + - operator.openshift.io + resources: + - networks + verbs: + - get + - list + - watch - apiGroups: - rbac.authorization.k8s.io resources: diff --git a/internal/controller/flp/detect_subnets.go b/internal/controller/flp/detect_subnets.go new file mode 100644 index 000000000..9c85add2f --- /dev/null +++ b/internal/controller/flp/detect_subnets.go @@ -0,0 +1,183 @@ +package flp + +import ( + "context" + "errors" + "fmt" + "net" + + flowslatest "github.com/netobserv/network-observability-operator/api/flowcollector/v1beta2" + "github.com/netobserv/network-observability-operator/internal/pkg/cluster" + "github.com/netobserv/network-observability-operator/internal/pkg/helper" + configv1 "github.com/openshift/api/config/v1" + operatorv1 "github.com/openshift/api/operator/v1" + "gopkg.in/yaml.v2" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +func (r *Reconciler) getOpenShiftSubnets(ctx context.Context) ([]flowslatest.SubnetLabel, error) { + if !r.mgr.ClusterInfo.HasCNO() { + return nil, nil + } + var errs []error + var svcMachineCIDRs []*net.IPNet + + pods, services, extIPs, err := readNetworkConfig(ctx, r) + if err != nil { + errs = append(errs, err) + } + for _, strCIDR := range services { + if _, parsed, err := net.ParseCIDR(strCIDR); err == nil { + svcMachineCIDRs = append(svcMachineCIDRs, parsed) + } + } + + machines, err := readClusterConfig(ctx, r) + if err != nil { + errs = append(errs, err) + } + for _, strCIDR := range machines { + if _, parsed, err := net.ParseCIDR(strCIDR); err == nil { + svcMachineCIDRs = append(svcMachineCIDRs, parsed) + } + } + + // API server + if apiserverIPs, err := cluster.GetAPIServerEndpointIPs(ctx, r, r.mgr.ClusterInfo); err == nil { + // Check if this isn't already an IP covered in Services or Machines subnets + for _, ip := range apiserverIPs { + if parsed := net.ParseIP(ip); parsed != nil { + var alreadyCovered bool + for _, cidr := range svcMachineCIDRs { + if cidr.Contains(parsed) { + alreadyCovered = true + break + } + } + if !alreadyCovered { + cidr := helper.IPToCIDR(ip) + services = append(services, cidr) + } + } + } + } else { + errs = append(errs, fmt.Errorf("can't get API server endpoint IPs: %w", err)) + } + + // Additional OVN subnets + moreMachines, err := readNetworkOperatorConfig(ctx, r) + if err != nil { + errs = append(errs, err) + } + machines = append(machines, moreMachines...) + + var subnets []flowslatest.SubnetLabel + if len(machines) > 0 { + subnets = append(subnets, flowslatest.SubnetLabel{ + Name: "Machines", + CIDRs: machines, + }) + } + if len(pods) > 0 { + subnets = append(subnets, flowslatest.SubnetLabel{ + Name: "Pods", + CIDRs: pods, + }) + } + if len(services) > 0 { + subnets = append(subnets, flowslatest.SubnetLabel{ + Name: "Services", + CIDRs: services, + }) + } + if len(extIPs) > 0 { + subnets = append(subnets, flowslatest.SubnetLabel{ + Name: "EXT:ExternalIP", + CIDRs: extIPs, + }) + } + return subnets, errors.Join(errs...) +} + +func readNetworkConfig(ctx context.Context, cl client.Client) ([]string, []string, []string, error) { + // Pods and Services subnets are found in CNO config + var pods, services, extIPs []string + network := &configv1.Network{} + if err := cl.Get(ctx, types.NamespacedName{Name: "cluster"}, network); err != nil { + return nil, nil, nil, fmt.Errorf("can't get Network (config) information: %w", err) + } + for _, podsNet := range network.Spec.ClusterNetwork { + pods = append(pods, podsNet.CIDR) + } + services = network.Spec.ServiceNetwork + if network.Spec.ExternalIP != nil && len(network.Spec.ExternalIP.AutoAssignCIDRs) > 0 { + extIPs = network.Spec.ExternalIP.AutoAssignCIDRs + } + return pods, services, extIPs, nil +} + +func readClusterConfig(ctx context.Context, cl client.Client) ([]string, error) { + // Nodes subnet found in CM cluster-config-v1 (kube-system) + cm := &corev1.ConfigMap{} + if err := cl.Get(ctx, types.NamespacedName{Name: "cluster-config-v1", Namespace: "kube-system"}, cm); err != nil { + return nil, fmt.Errorf(`can't read "cluster-config-v1" ConfigMap: %w`, err) + } + return readMachineFromConfig(cm) +} + +func readMachineFromConfig(cm *corev1.ConfigMap) ([]string, error) { + type ClusterConfig struct { + Networking struct { + MachineNetwork []struct { + CIDR string `yaml:"cidr"` + } `yaml:"machineNetwork"` + } `yaml:"networking"` + } + + var rawConfig string + var ok bool + if rawConfig, ok = cm.Data["install-config"]; !ok { + return nil, fmt.Errorf(`can't find key "install-config" in "cluster-config-v1" ConfigMap`) + } + var config ClusterConfig + if err := yaml.Unmarshal([]byte(rawConfig), &config); err != nil { + return nil, fmt.Errorf(`can't deserialize content of "cluster-config-v1" ConfigMap: %w`, err) + } + + var cidrs []string + for _, cidr := range config.Networking.MachineNetwork { + cidrs = append(cidrs, cidr.CIDR) + } + + return cidrs, nil +} + +func readNetworkOperatorConfig(ctx context.Context, cl client.Client) ([]string, error) { + // Additional OVN subnets: https://github.com/openshift/cluster-network-operator/blob/fda7a9f07ab6f78d032d310cdd77f21d04f1289a/pkg/network/ovn_kubernetes.go#L76-L77 + var machines []string + networkOp := &operatorv1.Network{} + if err := cl.Get(ctx, types.NamespacedName{Name: "cluster"}, networkOp); err != nil { + return nil, fmt.Errorf("can't get Network (operator) information: %w", err) + } + internalSubnet := "100.64.0.0/16" + transitSwitchSubnet := "100.88.0.0/16" + masqueradeSubnet := "169.254.0.0/17" + ovnk := networkOp.Spec.DefaultNetwork.OVNKubernetesConfig + if ovnk != nil { + if ovnk.V4InternalSubnet != "" { + internalSubnet = ovnk.V4InternalSubnet + } + if ovnk.IPv4 != nil && ovnk.IPv4.InternalTransitSwitchSubnet != "" { + transitSwitchSubnet = ovnk.IPv4.InternalTransitSwitchSubnet + } + if ovnk.GatewayConfig != nil && ovnk.GatewayConfig.IPv4.InternalMasqueradeSubnet != "" { + masqueradeSubnet = ovnk.GatewayConfig.IPv4.InternalMasqueradeSubnet + } + } + machines = append(machines, internalSubnet) + machines = append(machines, transitSwitchSubnet) + machines = append(machines, masqueradeSubnet) + return machines, nil +} diff --git a/internal/controller/flp/flp_controller.go b/internal/controller/flp/flp_controller.go index 236ef557e..d585b2274 100644 --- a/internal/controller/flp/flp_controller.go +++ b/internal/controller/flp/flp_controller.go @@ -13,12 +13,9 @@ import ( "github.com/netobserv/network-observability-operator/internal/pkg/manager" "github.com/netobserv/network-observability-operator/internal/pkg/manager/status" "github.com/netobserv/network-observability-operator/internal/pkg/watchers" - configv1 "github.com/openshift/api/config/v1" - "gopkg.in/yaml.v2" appsv1 "k8s.io/api/apps/v1" ascv2 "k8s.io/api/autoscaling/v2" corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/types" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/handler" @@ -230,89 +227,3 @@ func reconcileMonitoringCerts(ctx context.Context, info *reconcilers.Common, tls return nil } - -func (r *Reconciler) getOpenShiftSubnets(ctx context.Context) ([]flowslatest.SubnetLabel, error) { - var subnets []flowslatest.SubnetLabel - - // Pods and Services subnets are found in CNO config - if r.mgr.ClusterInfo.HasCNO() { - network := &configv1.Network{} - err := r.Get(ctx, types.NamespacedName{Name: "cluster"}, network) - if err != nil { - return nil, fmt.Errorf("can't get Network information: %w", err) - } - var podCIDRs []string - for _, podsNet := range network.Spec.ClusterNetwork { - podCIDRs = append(podCIDRs, podsNet.CIDR) - } - if len(podCIDRs) > 0 { - subnets = append(subnets, flowslatest.SubnetLabel{ - Name: "Pods", - CIDRs: podCIDRs, - }) - } - if len(network.Spec.ServiceNetwork) > 0 { - subnets = append(subnets, flowslatest.SubnetLabel{ - Name: "Services", - CIDRs: network.Spec.ServiceNetwork, - }) - } - if network.Spec.ExternalIP != nil && len(network.Spec.ExternalIP.AutoAssignCIDRs) > 0 { - subnets = append(subnets, flowslatest.SubnetLabel{ - Name: "ExternalIP", - CIDRs: network.Spec.ExternalIP.AutoAssignCIDRs, - }) - } - } - - // Nodes subnet found in CM cluster-config-v1 (kube-system) - cm := &corev1.ConfigMap{} - if err := r.Get(ctx, types.NamespacedName{Name: "cluster-config-v1", Namespace: "kube-system"}, cm); err != nil { - return nil, fmt.Errorf(`can't read "cluster-config-v1" ConfigMap: %w`, err) - } - machines, err := readMachineNetworks(cm) - if err != nil { - return nil, err - } - - if len(machines) > 0 { - subnets = append(subnets, machines...) - } - - return subnets, nil -} - -func readMachineNetworks(cm *corev1.ConfigMap) ([]flowslatest.SubnetLabel, error) { - var subnets []flowslatest.SubnetLabel - - type ClusterConfig struct { - Networking struct { - MachineNetwork []struct { - CIDR string `yaml:"cidr"` - } `yaml:"machineNetwork"` - } `yaml:"networking"` - } - - var rawConfig string - var ok bool - if rawConfig, ok = cm.Data["install-config"]; !ok { - return nil, fmt.Errorf(`can't find key "install-config" in "cluster-config-v1" ConfigMap`) - } - var config ClusterConfig - if err := yaml.Unmarshal([]byte(rawConfig), &config); err != nil { - return nil, fmt.Errorf(`can't deserialize content of "cluster-config-v1" ConfigMap: %w`, err) - } - - var cidrs []string - for _, cidr := range config.Networking.MachineNetwork { - cidrs = append(cidrs, cidr.CIDR) - } - if len(cidrs) > 0 { - subnets = append(subnets, flowslatest.SubnetLabel{ - Name: "Machines", - CIDRs: cidrs, - }) - } - - return subnets, nil -} diff --git a/internal/controller/flp/flp_pipeline_builder_test.go b/internal/controller/flp/flp_pipeline_builder_test.go index 3942062fb..fa51c1220 100644 --- a/internal/controller/flp/flp_pipeline_builder_test.go +++ b/internal/controller/flp/flp_pipeline_builder_test.go @@ -293,16 +293,10 @@ publish: External`, }, } - machines, err := readMachineNetworks(&cm) + machines, err := readMachineFromConfig(&cm) assert.NoError(t, err) - assert.Equal(t, - []flowslatest.SubnetLabel{ - { - Name: "Machines", - CIDRs: []string{"10.0.0.0/16"}, - }, - }, machines) + assert.Equal(t, []string{"10.0.0.0/16"}, machines) } func TestPipelineWithSubnetLabels(t *testing.T) { diff --git a/internal/controller/networkpolicy/apiserver_endpoint_test.go b/internal/controller/networkpolicy/apiserver_endpoint_test.go deleted file mode 100644 index cb3dd56ba..000000000 --- a/internal/controller/networkpolicy/apiserver_endpoint_test.go +++ /dev/null @@ -1,26 +0,0 @@ -package networkpolicy - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestIPToCIDR(t *testing.T) { - assert := assert.New(t) - - // Test IPv4 addresses - assert.Equal("192.168.1.1/32", ipToCIDR("192.168.1.1")) - assert.Equal("10.0.0.1/32", ipToCIDR("10.0.0.1")) - assert.Equal("172.20.0.1/32", ipToCIDR("172.20.0.1")) - - // Test IPv6 addresses - assert.Equal("2001:db8::1/128", ipToCIDR("2001:db8::1")) - assert.Equal("fe80::1/128", ipToCIDR("fe80::1")) - assert.Equal("::1/128", ipToCIDR("::1")) - - // Test invalid IP - assert.Equal("", ipToCIDR("invalid")) - assert.Equal("", ipToCIDR("")) - assert.Equal("", ipToCIDR("256.256.256.256")) -} diff --git a/internal/controller/networkpolicy/np_controller.go b/internal/controller/networkpolicy/np_controller.go index 2bb4c84f5..5d2b84d3f 100644 --- a/internal/controller/networkpolicy/np_controller.go +++ b/internal/controller/networkpolicy/np_controller.go @@ -11,6 +11,7 @@ import ( flowslatest "github.com/netobserv/network-observability-operator/api/flowcollector/v1beta2" "github.com/netobserv/network-observability-operator/internal/controller/reconcilers" + "github.com/netobserv/network-observability-operator/internal/pkg/cluster" "github.com/netobserv/network-observability-operator/internal/pkg/helper" "github.com/netobserv/network-observability-operator/internal/pkg/manager" "github.com/netobserv/network-observability-operator/internal/pkg/manager/status" @@ -80,7 +81,7 @@ func (r *Reconciler) reconcile(ctx context.Context, clh *helper.Client, desired // Get API server endpoint IPs for network policy var apiServerIPs []string if r.mgr.ClusterInfo.IsOpenShift() { - apiServerIPs, err = GetAPIServerEndpointIPs(ctx, r.Client, r.mgr.ClusterInfo) + apiServerIPs, err = cluster.GetAPIServerEndpointIPs(ctx, r.Client, r.mgr.ClusterInfo) if err != nil { l.Error(err, "Failed to get API server endpoint IPs") return fmt.Errorf("cannot determine API server endpoint IPs: %w", err) diff --git a/internal/controller/networkpolicy/np_objects.go b/internal/controller/networkpolicy/np_objects.go index f7010a6df..863675dc9 100644 --- a/internal/controller/networkpolicy/np_objects.go +++ b/internal/controller/networkpolicy/np_objects.go @@ -1,8 +1,6 @@ package networkpolicy import ( - "net" - flowslatest "github.com/netobserv/network-observability-operator/api/flowcollector/v1beta2" "github.com/netobserv/network-observability-operator/internal/controller/constants" "github.com/netobserv/network-observability-operator/internal/pkg/cluster" @@ -18,22 +16,6 @@ import ( const netpolName = "netobserv" -// ipToCIDR converts an IP address to a CIDR with proper prefix length -// IPv4 addresses get /32, IPv6 addresses get /128 -func ipToCIDR(ipStr string) string { - ip := net.ParseIP(ipStr) - if ip == nil { - return "" - } - - // Check if it's IPv4 (net.IP.To4() returns nil for IPv6) - if ip.To4() != nil { - return ipStr + "/32" - } - // IPv6 - return ipStr + "/128" -} - func peerInNamespace(ns string) networkingv1.NetworkPolicyPeer { return networkingv1.NetworkPolicyPeer{ NamespaceSelector: &metav1.LabelSelector{ @@ -192,7 +174,7 @@ func buildMainNetworkPolicy(desired *flowslatest.FlowCollector, mgr *manager.Man // Build a single egress rule with multiple IP peers peers := []networkingv1.NetworkPolicyPeer{} for _, ip := range apiServerIPs { - cidr := ipToCIDR(ip) + cidr := helper.IPToCIDR(ip) if cidr != "" { peers = append(peers, networkingv1.NetworkPolicyPeer{ IPBlock: &networkingv1.IPBlock{ diff --git a/internal/controller/networkpolicy/apiserver_endpoint.go b/internal/pkg/cluster/apiserver_endpoint.go similarity index 95% rename from internal/controller/networkpolicy/apiserver_endpoint.go rename to internal/pkg/cluster/apiserver_endpoint.go index 870e241a8..f1c0903d6 100644 --- a/internal/controller/networkpolicy/apiserver_endpoint.go +++ b/internal/pkg/cluster/apiserver_endpoint.go @@ -1,10 +1,9 @@ -package networkpolicy +package cluster import ( "context" "fmt" - "github.com/netobserv/network-observability-operator/internal/pkg/cluster" corev1 "k8s.io/api/core/v1" discoveryv1 "k8s.io/api/discovery/v1" "k8s.io/apimachinery/pkg/api/errors" @@ -20,7 +19,7 @@ const ( // GetAPIServerEndpointIPs retrieves the API server endpoint IP addresses. // It uses EndpointSlice API if available, otherwise falls back to Endpoints API. -func GetAPIServerEndpointIPs(ctx context.Context, cl client.Client, clusterInfo *cluster.Info) ([]string, error) { +func GetAPIServerEndpointIPs(ctx context.Context, cl client.Client, clusterInfo *Info) ([]string, error) { logger := log.FromContext(ctx) var ips []string diff --git a/internal/pkg/helper/ip.go b/internal/pkg/helper/ip.go new file mode 100644 index 000000000..bbc0a7cd8 --- /dev/null +++ b/internal/pkg/helper/ip.go @@ -0,0 +1,19 @@ +package helper + +import "net" + +// IPToCIDR converts an IP address to a CIDR with proper prefix length +// IPv4 addresses get /32, IPv6 addresses get /128 +func IPToCIDR(ipStr string) string { + ip := net.ParseIP(ipStr) + if ip == nil { + return "" + } + + // Check if it's IPv4 (net.IP.To4() returns nil for IPv6) + if ip.To4() != nil { + return ipStr + "/32" + } + // IPv6 + return ipStr + "/128" +} diff --git a/internal/pkg/helper/ip_test.go b/internal/pkg/helper/ip_test.go new file mode 100644 index 000000000..0caf2f821 --- /dev/null +++ b/internal/pkg/helper/ip_test.go @@ -0,0 +1,26 @@ +package helper + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestIPToCIDR(t *testing.T) { + assert := assert.New(t) + + // Test IPv4 addresses + assert.Equal("192.168.1.1/32", IPToCIDR("192.168.1.1")) + assert.Equal("10.0.0.1/32", IPToCIDR("10.0.0.1")) + assert.Equal("172.20.0.1/32", IPToCIDR("172.20.0.1")) + + // Test IPv6 addresses + assert.Equal("2001:db8::1/128", IPToCIDR("2001:db8::1")) + assert.Equal("fe80::1/128", IPToCIDR("fe80::1")) + assert.Equal("::1/128", IPToCIDR("::1")) + + // Test invalid IP + assert.Equal("", IPToCIDR("invalid")) + assert.Equal("", IPToCIDR("")) + assert.Equal("", IPToCIDR("256.256.256.256")) +} diff --git a/internal/pkg/manager/manager.go b/internal/pkg/manager/manager.go index 0e218d91f..6175cf51b 100644 --- a/internal/pkg/manager/manager.go +++ b/internal/pkg/manager/manager.go @@ -24,6 +24,7 @@ import ( //+kubebuilder:rbac:groups=rbac.authorization.k8s.io,resources=clusterrolebindings;rolebindings,verbs=get;list;create;delete;update;watch //+kubebuilder:rbac:groups=console.openshift.io,resources=consoleplugins,verbs=get;create;delete;update;patch;list;watch //+kubebuilder:rbac:groups=operator.openshift.io,resources=consoles,verbs=get;list;update;watch +//+kubebuilder:rbac:groups=operator.openshift.io,resources=networks,verbs=get;list;watch //+kubebuilder:rbac:groups=flows.netobserv.io,resources=flowcollectors;flowmetrics,verbs=get;list;watch;create;update;patch;delete //+kubebuilder:rbac:groups=flows.netobserv.io,resources=flowcollectors/status;flowmetrics/status,verbs=get;update;patch //+kubebuilder:rbac:groups=flows.netobserv.io,resources=flowcollectors/finalizers,verbs=update