diff --git a/Makefile b/Makefile index 7b7fa385f..3116462a5 100644 --- a/Makefile +++ b/Makefile @@ -279,6 +279,7 @@ endif doc: crdoc ## Generate markdown documentation $(CRDOC) --resources config/crd/bases/flows.netobserv.io_flowcollectors.yaml --output docs/FlowCollector.md $(CRDOC) --resources config/crd/bases/flows.netobserv.io_flowmetrics.yaml --output docs/FlowMetric.md + $(CRDOC) --resources config/crd/bases/flows.netobserv.io_flowcollectorslices.yaml --output docs/FlowCollectorSlice.md # Hack to reintroduce when the API stored version != latest version; see also envtest.go (CRD path config) # .PHONY: hack-crd-for-test @@ -288,6 +289,7 @@ doc: crdoc ## Generate markdown documentation # '(.spec.versions.[]|select(.name != "v1beta2").storage) = false,(.spec.versions.[]|select(.name == "v1beta2").storage) = true' \ # > ./hack/cloned.flows.netobserv.io_flowcollectors.yaml # cp ./config/crd/bases/flows.netobserv.io_flowmetrics.yaml ./hack/cloned.flows.netobserv.io_flowmetrics.yaml +# cp ./config/crd/bases/flows.netobserv.io_flowcollectorslices.yaml ./hack/cloned.flows.netobserv.io_flowcollectorslices.yaml generate: gencode manifests doc ## Run all code/file generators diff --git a/PROJECT b/PROJECT index 776d1aae0..36cbab34c 100644 --- a/PROJECT +++ b/PROJECT @@ -38,4 +38,12 @@ resources: webhooks: validation: true webhookVersion: v1 +- api: + crdVersion: v1 + namespaced: true + domain: netobserv.io + group: flows + kind: FlowCollectorSlice + path: github.com/netobserv/network-observability-operator/api/flowcollectorslice/v1alpha1 + version: v1alpha1 version: "3" diff --git a/api/flowcollector/v1beta2/flowcollector_types.go b/api/flowcollector/v1beta2/flowcollector_types.go index 16582d616..67d419cdb 100644 --- a/api/flowcollector/v1beta2/flowcollector_types.go +++ b/api/flowcollector/v1beta2/flowcollector_types.go @@ -697,6 +697,10 @@ type FlowCollectorFLP struct { // but with a lesser improvement in performance. Filters []FLPFilterSet `json:"filters"` + // Global configuration managing FlowCollectorSlices custom resources. + //+optional + SlicesConfig *SlicesConfig `json:"slicesConfig,omitempty"` + // `advanced` allows setting some aspects of the internal configuration of the flow processor. // This section is aimed mostly for debugging and fine-grained performance optimizations, // such as `GOGC` and `GOMAXPROCS` environment variables. Set these values at your own risk. @@ -787,6 +791,33 @@ type FlowCollectorHPA struct { Metrics []ascv2.MetricSpec `json:"metrics"` } +type SliceCollectionMode string + +const ( + CollectionAlwaysCollect SliceCollectionMode = "AlwaysCollect" + CollectionAllowList SliceCollectionMode = "AllowList" +) + +type SlicesConfig struct { + // `enable` determines if the FlowCollectorSlice feature is enabled. If not, all resources of kind FlowCollectorSlice are simply ignored. + //+kubebuilder:default:=false + //+kubebuilder:validation:Required + Enable bool `json:"enable,omitempty"` + + // `collectionMode` determines how the FlowCollectorSlice custom resources impacts the flow collection process:
+ // - When set to `AlwaysCollect`, all flows are collected regardless of the presence of FlowCollectorSlice.
+ // - When set to `AllowList`, only the flows related to namespaces where a FlowCollectorSlice resource is present, or configured via the global `namespacesAllowList`, are collected.
+ //+kubebuilder:validation:Enum=AlwaysCollect;AllowList + //+kubebuilder:default:="AlwaysCollect" + CollectionMode SliceCollectionMode `json:"collectionMode,omitempty"` + + // `namespacesAllowList` is a list of namespaces for which flows are always collected, regardless of the presence of FlowCollectorSlice in those namespaces. + // An entry enclosed by slashes, such as `/openshift-.*/`, is matched as a regular expression. + // This setting is ignored if `collectionMode` is different from `AllowList`. + //+kubebuilder:validation:optional + NamespacesAllowList []string `json:"namespacesAllowList,omitempty"` +} + type LokiAuthToken string const ( diff --git a/api/flowcollector/v1beta2/helper.go b/api/flowcollector/v1beta2/helper.go index 190d33ff5..ffa626b4e 100644 --- a/api/flowcollector/v1beta2/helper.go +++ b/api/flowcollector/v1beta2/helper.go @@ -227,3 +227,7 @@ func (spec *FlowCollectorConsolePlugin) IsUnmanagedConsolePluginReplicas() bool } return spec.Autoscaler.IsHPAEnabled() } + +func (spec *FlowCollectorSpec) IsSliceEnabled() bool { + return spec.Processor.SlicesConfig != nil && spec.Processor.SlicesConfig.Enable +} diff --git a/api/flowcollector/v1beta2/zz_generated.deepcopy.go b/api/flowcollector/v1beta2/zz_generated.deepcopy.go index cbc20a6cf..1733992e6 100644 --- a/api/flowcollector/v1beta2/zz_generated.deepcopy.go +++ b/api/flowcollector/v1beta2/zz_generated.deepcopy.go @@ -723,6 +723,11 @@ func (in *FlowCollectorFLP) DeepCopyInto(out *FlowCollectorFLP) { *out = make([]FLPFilterSet, len(*in)) copy(*out, *in) } + if in.SlicesConfig != nil { + in, out := &in.SlicesConfig, &out.SlicesConfig + *out = new(SlicesConfig) + (*in).DeepCopyInto(*out) + } if in.Advanced != nil { in, out := &in.Advanced, &out.Advanced *out = new(AdvancedProcessorConfig) @@ -1361,6 +1366,26 @@ func (in *ServerTLS) DeepCopy() *ServerTLS { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SlicesConfig) DeepCopyInto(out *SlicesConfig) { + *out = *in + if in.NamespacesAllowList != nil { + in, out := &in.NamespacesAllowList, &out.NamespacesAllowList + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SlicesConfig. +func (in *SlicesConfig) DeepCopy() *SlicesConfig { + if in == nil { + return nil + } + out := new(SlicesConfig) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *SubnetLabel) DeepCopyInto(out *SubnetLabel) { *out = *in diff --git a/api/flowcollectorslice/v1alpha1/doc.go b/api/flowcollectorslice/v1alpha1/doc.go new file mode 100644 index 000000000..a5191cf9a --- /dev/null +++ b/api/flowcollectorslice/v1alpha1/doc.go @@ -0,0 +1,2 @@ +// Package v1aplha1 contains the v1alpha1 API implementation. +package v1alpha1 diff --git a/api/flowcollectorslice/v1alpha1/flowcollectorslice_types.go b/api/flowcollectorslice/v1alpha1/flowcollectorslice_types.go new file mode 100644 index 000000000..8fe939395 --- /dev/null +++ b/api/flowcollectorslice/v1alpha1/flowcollectorslice_types.go @@ -0,0 +1,65 @@ +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// FlowCollectorSliceSpec defines the desired state of FlowCollectorSlice +type FlowCollectorSliceSpec struct { + // `subnetLabels` allows to customize subnets and IPs labelling, such as to identify cluster-external workloads or web services. + // Beware that the subnet labels configured in FlowCollectorSlice are not limited to the flows of the related namespace: any flow + // in the whole cluster can be labelled using this configuration. However, subnet labels defined in the cluster-scoped FlowCollector take + // precedence in case of conflicting rules. + //+optional + SubnetLabels []SubnetLabel `json:"subnetLabels,omitempty"` + + // `sampling` is an optional sampling interval to apply to this slice. For example, a value of `50` means that 1 matching flow in 50 is sampled. + //+kubebuilder:validation:Minimum=0 + // +optional + Sampling int32 `json:"sampling,omitempty"` +} + +// SubnetLabel allows to label subnets and IPs, such as to identify cluster-external workloads or web services. +type SubnetLabel struct { + // List of CIDRs, such as `["1.2.3.4/32"]`. + //+required + CIDRs []string `json:"cidrs,omitempty"` // Note, starting with k8s 1.31 / ocp 4.16 there's a new way to validate CIDR such as `+kubebuilder:validation:XValidation:rule="isCIDR(self)",message="field should be in CIDR notation format"`. But older versions would reject the CRD so we cannot implement it now to maintain compatibility. + // Label name, used to flag matching flows. + //+required + Name string `json:"name,omitempty"` +} + +// FlowCollectorSliceStatus defines the observed state of FlowCollectorSlice +type FlowCollectorSliceStatus struct { + // `conditions` represent the latest available observations of an object's state + Conditions []metav1.Condition `json:"conditions"` + // Filter that is applied for flow collection + // +optional + FilterApplied string `json:"filterApplied"` + // Number of subnet labels configured + // +optional + SubnetLabelsConfigured int `json:"subnetLabelsConfigured"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// FlowMetric is the API allowing to create custom metrics from the collected flow logs. +type FlowCollectorSlice struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec FlowCollectorSliceSpec `json:"spec,omitempty"` + Status FlowCollectorSliceStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true +// FlowCollectorSliceList contains a list of FlowCollectorSlice +type FlowCollectorSliceList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []FlowCollectorSlice `json:"items"` +} + +func init() { + SchemeBuilder.Register(&FlowCollectorSlice{}, &FlowCollectorSliceList{}) +} diff --git a/api/flowcollectorslice/v1alpha1/groupversion_info.go b/api/flowcollectorslice/v1alpha1/groupversion_info.go new file mode 100644 index 000000000..c23689d0a --- /dev/null +++ b/api/flowcollectorslice/v1alpha1/groupversion_info.go @@ -0,0 +1,20 @@ +// Package v1alpha1 contains API Schema definitions for the flows v1alpha1 API group +// +kubebuilder:object:generate=true +// +groupName=flows.netobserv.io +package v1alpha1 + +import ( + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/scheme" +) + +var ( + // GroupVersion is group version used to register these objects + GroupVersion = schema.GroupVersion{Group: "flows.netobserv.io", Version: "v1alpha1"} + + // SchemeBuilder is used to add go types to the GroupVersionKind scheme + SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} + + // AddToScheme adds the types in this group-version to the given scheme. + AddToScheme = SchemeBuilder.AddToScheme +) diff --git a/api/flowcollectorslice/v1alpha1/zz_generated.deepcopy.go b/api/flowcollectorslice/v1alpha1/zz_generated.deepcopy.go new file mode 100644 index 000000000..7e6849289 --- /dev/null +++ b/api/flowcollectorslice/v1alpha1/zz_generated.deepcopy.go @@ -0,0 +1,149 @@ +//go:build !ignore_autogenerated + +/* +Copyright 2021. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by controller-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FlowCollectorSlice) DeepCopyInto(out *FlowCollectorSlice) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FlowCollectorSlice. +func (in *FlowCollectorSlice) DeepCopy() *FlowCollectorSlice { + if in == nil { + return nil + } + out := new(FlowCollectorSlice) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *FlowCollectorSlice) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FlowCollectorSliceList) DeepCopyInto(out *FlowCollectorSliceList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]FlowCollectorSlice, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FlowCollectorSliceList. +func (in *FlowCollectorSliceList) DeepCopy() *FlowCollectorSliceList { + if in == nil { + return nil + } + out := new(FlowCollectorSliceList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *FlowCollectorSliceList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FlowCollectorSliceSpec) DeepCopyInto(out *FlowCollectorSliceSpec) { + *out = *in + if in.SubnetLabels != nil { + in, out := &in.SubnetLabels, &out.SubnetLabels + *out = make([]SubnetLabel, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FlowCollectorSliceSpec. +func (in *FlowCollectorSliceSpec) DeepCopy() *FlowCollectorSliceSpec { + if in == nil { + return nil + } + out := new(FlowCollectorSliceSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FlowCollectorSliceStatus) DeepCopyInto(out *FlowCollectorSliceStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]v1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FlowCollectorSliceStatus. +func (in *FlowCollectorSliceStatus) DeepCopy() *FlowCollectorSliceStatus { + if in == nil { + return nil + } + out := new(FlowCollectorSliceStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SubnetLabel) DeepCopyInto(out *SubnetLabel) { + *out = *in + if in.CIDRs != nil { + in, out := &in.CIDRs, &out.CIDRs + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SubnetLabel. +func (in *SubnetLabel) DeepCopy() *SubnetLabel { + if in == nil { + return nil + } + out := new(SubnetLabel) + in.DeepCopyInto(out) + return out +} diff --git a/bundle/manifests/flows.netobserv.io_flowcollectors.yaml b/bundle/manifests/flows.netobserv.io_flowcollectors.yaml index b741295ad..0a12132c7 100644 --- a/bundle/manifests/flows.netobserv.io_flowcollectors.yaml +++ b/bundle/manifests/flows.netobserv.io_flowcollectors.yaml @@ -6108,6 +6108,37 @@ spec: More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ type: object type: object + slicesConfig: + description: Global configuration managing FlowCollectorSlices + custom resources. + properties: + collectionMode: + default: AlwaysCollect + description: |- + `collectionMode` determines how the FlowCollectorSlice custom resources impacts the flow collection process:
+ - When set to `AlwaysCollect`, all flows are collected regardless of the presence of FlowCollectorSlice.
+ - When set to `AllowList`, only the flows related to namespaces where a FlowCollectorSlice resource is present, or configured via the global `namespacesAllowList`, are collected.
+ enum: + - AlwaysCollect + - AllowList + type: string + enable: + default: false + description: '`enable` determines if the FlowCollectorSlice + feature is enabled. If not, all resources of kind FlowCollectorSlice + are simply ignored.' + type: boolean + namespacesAllowList: + description: |- + `namespacesAllowList` is a list of namespaces for which flows are always collected, regardless of the presence of FlowCollectorSlice in those namespaces. + An entry enclosed by slashes, such as `/openshift-.*/`, is matched as a regular expression. + This setting is ignored if `collectionMode` is different from `AllowList`. + items: + type: string + type: array + required: + - enable + type: object subnetLabels: description: |- `subnetLabels` allows to define custom labels on subnets and IPs or to enable automatic labelling of recognized subnets in OpenShift, which is used to identify cluster external traffic. diff --git a/bundle/manifests/flows.netobserv.io_flowcollectorslices.yaml b/bundle/manifests/flows.netobserv.io_flowcollectorslices.yaml new file mode 100644 index 000000000..5983127bc --- /dev/null +++ b/bundle/manifests/flows.netobserv.io_flowcollectorslices.yaml @@ -0,0 +1,154 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.16.2 + creationTimestamp: null + name: flowcollectorslices.flows.netobserv.io +spec: + group: flows.netobserv.io + names: + kind: FlowCollectorSlice + listKind: FlowCollectorSliceList + plural: flowcollectorslices + singular: flowcollectorslice + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: FlowMetric is the API allowing to create custom metrics from + the collected flow logs. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: FlowCollectorSliceSpec defines the desired state of FlowCollectorSlice + properties: + sampling: + description: '`sampling` is an optional sampling interval to apply + to this slice. For example, a value of `50` means that 1 matching + flow in 50 is sampled.' + format: int32 + minimum: 0 + type: integer + subnetLabels: + description: |- + `subnetLabels` allows to customize subnets and IPs labelling, such as to identify cluster-external workloads or web services. + Beware that the subnet labels configured in FlowCollectorSlice are not limited to the flows of the related namespace: any flow + in the whole cluster can be labelled using this configuration. However, subnet labels defined in the cluster-scoped FlowCollector take + precedence in case of conflicting rules. + items: + description: SubnetLabel allows to label subnets and IPs, such as + to identify cluster-external workloads or web services. + properties: + cidrs: + description: List of CIDRs, such as `["1.2.3.4/32"]`. + items: + type: string + type: array + name: + description: Label name, used to flag matching flows. + type: string + required: + - cidrs + - name + type: object + type: array + type: object + status: + description: FlowCollectorSliceStatus defines the observed state of FlowCollectorSlice + properties: + conditions: + description: '`conditions` represent the latest available observations + of an object''s state' + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + filterApplied: + description: Filter that is applied for flow collection + type: string + subnetLabelsConfigured: + description: Number of subnet labels configured + type: integer + required: + - conditions + type: object + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: null + storedVersions: null diff --git a/bundle/manifests/netobserv-operator.clusterserviceversion.yaml b/bundle/manifests/netobserv-operator.clusterserviceversion.yaml index c2b67a38b..d9a224c2b 100644 --- a/bundle/manifests/netobserv-operator.clusterserviceversion.yaml +++ b/bundle/manifests/netobserv-operator.clusterserviceversion.yaml @@ -4,61 +4,28 @@ metadata: annotations: alm-examples: |- [ + { + "apiVersion": "flows.netobserv.io/v1alpha1", + "kind": "FlowCollectorSlice", + "metadata": { + "name": "flowcollectorslice-sample" + }, + "spec": { + "sampling": 1 + } + }, { "apiVersion": "flows.netobserv.io/v1alpha1", "kind": "FlowMetric", "metadata": { - "labels": { - "app.kubernetes.io/created-by": "netobserv-operator", - "app.kubernetes.io/instance": "flowmetric-sample", - "app.kubernetes.io/managed-by": "kustomize", - "app.kubernetes.io/name": "flowmetric", - "app.kubernetes.io/part-of": "netobserv-operator" - }, "name": "flowmetric-sample" }, "spec": { - "charts": [ - { - "dashboardName": "Main", - "queries": [ - { - "legend": "", - "promQL": "sum(rate($METRIC[2m]))" - } - ], - "title": "External ingress traffic", - "type": "SingleStat", - "unit": "Bps" - }, - { - "dashboardName": "Main", - "queries": [ - { - "legend": "{{DstK8S_Namespace}} / {{DstK8S_OwnerName}}", - "promQL": "sum(rate($METRIC{DstK8S_Namespace!=\"\"}[2m])) by (DstK8S_Namespace, DstK8S_OwnerName)" - } - ], - "sectionName": "External", - "title": "Top external ingress traffic per workload", - "type": "StackArea", - "unit": "Bps" - } - ], "direction": "Ingress", - "filters": [ - { - "field": "SrcSubnetLabel", - "matchType": "Absence" - } - ], "labels": [ - "DstK8S_HostName", - "DstK8S_Namespace", - "DstK8S_OwnerName", - "DstK8S_OwnerType" + "SrcK8S_Namespace", + "DstK8S_Namespace" ], - "metricName": "cluster_external_ingress_bytes_total", "type": "Counter", "valueField": "Bytes" } @@ -237,6 +204,13 @@ metadata: "cpu": "100m", "memory": "100Mi" } + }, + "slicesConfig": { + "collectionMode": "AllowList", + "enable": false, + "namespacesAllowList": [ + "/openshift-.*|netobserv.*/" + ] } }, "prometheus": { @@ -253,7 +227,7 @@ metadata: categories: Monitoring, Networking, Observability console.openshift.io/plugins: '["netobserv-plugin"]' containerImage: quay.io/netobserv/network-observability-operator:1.10.0-community - createdAt: "2025-11-26T13:16:01Z" + createdAt: "2025-12-11T14:18:07Z" description: Network flows collector and monitoring solution operatorframework.io/initialization-resource: '{"apiVersion":"flows.netobserv.io/v1beta2", "kind":"FlowCollector","metadata":{"name":"cluster"},"spec": {}}' @@ -628,6 +602,14 @@ spec: path: processor.metrics.includeList - displayName: Port path: processor.metrics.server.port + - displayName: Slices config + path: processor.slicesConfig + - displayName: Collection mode + path: processor.slicesConfig.collectionMode + - displayName: Enable + path: processor.slicesConfig.enable + - displayName: Namespaces allow list + path: processor.slicesConfig.namespacesAllowList - displayName: Subnet labels path: processor.subnetLabels - displayName: Custom labels @@ -663,6 +645,12 @@ spec: x-descriptors: - urn:alm:descriptor:io.kubernetes.conditions version: v1beta2 + - description: '`FlowCollectorSlice` is the schema allowing delegated configuration + per namespace.' + displayName: Flow Collector Slice + kind: FlowCollectorSlice + name: flowcollectorslices.flows.netobserv.io + version: v1alpha1 - description: '`FlowMetric` is the schema for the custom metrics API, which allows to generate more metrics out of flow logs. You can find examples here: https://github.com/netobserv/network-observability-operator/tree/main/config/samples/flowmetrics' displayName: Flow Metric @@ -913,6 +901,7 @@ spec: - flows.netobserv.io resources: - flowcollectors + - flowcollectorslices - flowmetrics verbs: - create @@ -932,6 +921,7 @@ spec: - flows.netobserv.io resources: - flowcollectors/status + - flowcollectorslices/status - flowmetrics/status verbs: - get diff --git a/config/crd/bases/flows.netobserv.io_flowcollectors.yaml b/config/crd/bases/flows.netobserv.io_flowcollectors.yaml index 3addd7bc5..dd70a309a 100644 --- a/config/crd/bases/flows.netobserv.io_flowcollectors.yaml +++ b/config/crd/bases/flows.netobserv.io_flowcollectors.yaml @@ -5645,6 +5645,34 @@ spec: More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ type: object type: object + slicesConfig: + description: Global configuration managing FlowCollectorSlices custom resources. + properties: + collectionMode: + default: AlwaysCollect + description: |- + `collectionMode` determines how the FlowCollectorSlice custom resources impacts the flow collection process:
+ - When set to `AlwaysCollect`, all flows are collected regardless of the presence of FlowCollectorSlice.
+ - When set to `AllowList`, only the flows related to namespaces where a FlowCollectorSlice resource is present, or configured via the global `namespacesAllowList`, are collected.
+ enum: + - AlwaysCollect + - AllowList + type: string + enable: + default: false + description: '`enable` determines if the FlowCollectorSlice feature is enabled. If not, all resources of kind FlowCollectorSlice are simply ignored.' + type: boolean + namespacesAllowList: + description: |- + `namespacesAllowList` is a list of namespaces for which flows are always collected, regardless of the presence of FlowCollectorSlice in those namespaces. + An entry enclosed by slashes, such as `/openshift-.*/`, is matched as a regular expression. + This setting is ignored if `collectionMode` is different from `AllowList`. + items: + type: string + type: array + required: + - enable + type: object subnetLabels: description: |- `subnetLabels` allows to define custom labels on subnets and IPs or to enable automatic labelling of recognized subnets in OpenShift, which is used to identify cluster external traffic. diff --git a/config/crd/bases/flows.netobserv.io_flowcollectorslices.yaml b/config/crd/bases/flows.netobserv.io_flowcollectorslices.yaml new file mode 100644 index 000000000..dde78403c --- /dev/null +++ b/config/crd/bases/flows.netobserv.io_flowcollectorslices.yaml @@ -0,0 +1,148 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.16.2 + name: flowcollectorslices.flows.netobserv.io +spec: + group: flows.netobserv.io + names: + kind: FlowCollectorSlice + listKind: FlowCollectorSliceList + plural: flowcollectorslices + singular: flowcollectorslice + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: FlowMetric is the API allowing to create custom metrics from + the collected flow logs. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: FlowCollectorSliceSpec defines the desired state of FlowCollectorSlice + properties: + sampling: + description: '`sampling` is an optional sampling interval to apply + to this slice. For example, a value of `50` means that 1 matching + flow in 50 is sampled.' + format: int32 + minimum: 0 + type: integer + subnetLabels: + description: |- + `subnetLabels` allows to customize subnets and IPs labelling, such as to identify cluster-external workloads or web services. + Beware that the subnet labels configured in FlowCollectorSlice are not limited to the flows of the related namespace: any flow + in the whole cluster can be labelled using this configuration. However, subnet labels defined in the cluster-scoped FlowCollector take + precedence in case of conflicting rules. + items: + description: SubnetLabel allows to label subnets and IPs, such as + to identify cluster-external workloads or web services. + properties: + cidrs: + description: List of CIDRs, such as `["1.2.3.4/32"]`. + items: + type: string + type: array + name: + description: Label name, used to flag matching flows. + type: string + required: + - cidrs + - name + type: object + type: array + type: object + status: + description: FlowCollectorSliceStatus defines the observed state of FlowCollectorSlice + properties: + conditions: + description: '`conditions` represent the latest available observations + of an object''s state' + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + filterApplied: + description: Filter that is applied for flow collection + type: string + subnetLabelsConfigured: + description: Number of subnet labels configured + type: integer + required: + - conditions + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index 05d72ac6d..5d743e06a 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -7,6 +7,7 @@ kind: Kustomization resources: - bases/flows.netobserv.io_flowcollectors.yaml - bases/flows.netobserv.io_flowmetrics.yaml +- bases/flows.netobserv.io_flowcollectorslices.yaml #+kubebuilder:scaffold:crdkustomizeresource # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix. diff --git a/config/csv/bases/netobserv-operator.clusterserviceversion.yaml b/config/csv/bases/netobserv-operator.clusterserviceversion.yaml index 080a2400a..5aabf970c 100644 --- a/config/csv/bases/netobserv-operator.clusterserviceversion.yaml +++ b/config/csv/bases/netobserv-operator.clusterserviceversion.yaml @@ -297,6 +297,11 @@ spec: kind: FlowMetric name: flowmetrics.flows.netobserv.io version: v1alpha1 + - description: '`FlowCollectorSlice` is the schema allowing delegated configuration per namespace.' + displayName: Flow Collector Slice + kind: FlowCollectorSlice + name: flowcollectorslices.flows.netobserv.io + version: v1alpha1 description: ':full-description:' displayName: NetObserv Operator icon: diff --git a/config/openshift/patch.yaml b/config/openshift/patch.yaml index 62a77a5a0..034d0bebc 100644 --- a/config/openshift/patch.yaml +++ b/config/openshift/patch.yaml @@ -28,4 +28,4 @@ kind: CustomResourceDefinition metadata: annotations: service.beta.openshift.io/inject-cabundle: "true" - name: flowmetrics.flows.netobserv.io \ No newline at end of file + name: flowmetrics.flows.netobserv.io diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 63d7a49c4..e370c951a 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -139,6 +139,7 @@ rules: - flows.netobserv.io resources: - flowcollectors + - flowcollectorslices - flowmetrics verbs: - create @@ -158,6 +159,7 @@ rules: - flows.netobserv.io resources: - flowcollectors/status + - flowcollectorslices/status - flowmetrics/status verbs: - get diff --git a/config/samples/flows_v1alpha1_flowcollectorslice.yaml b/config/samples/flows_v1alpha1_flowcollectorslice.yaml new file mode 100644 index 000000000..c85e25eb2 --- /dev/null +++ b/config/samples/flows_v1alpha1_flowcollectorslice.yaml @@ -0,0 +1,10 @@ +apiVersion: flows.netobserv.io/v1alpha1 +kind: FlowCollectorSlice +metadata: + name: flowcollectorslice-sample +spec: + sampling: 1 + # subnetLabels: + # - name: my-database + # cidrs: + # - 1.2.3.4/24 diff --git a/config/samples/flows_v1alpha1_flowmetric.yaml b/config/samples/flows_v1alpha1_flowmetric.yaml index d048bd32f..223bef56a 100644 --- a/config/samples/flows_v1alpha1_flowmetric.yaml +++ b/config/samples/flows_v1alpha1_flowmetric.yaml @@ -1,36 +1,29 @@ apiVersion: flows.netobserv.io/v1alpha1 kind: FlowMetric metadata: - labels: - app.kubernetes.io/name: flowmetric - app.kubernetes.io/instance: flowmetric-sample - app.kubernetes.io/part-of: netobserv-operator - app.kubernetes.io/managed-by: kustomize - app.kubernetes.io/created-by: netobserv-operator name: flowmetric-sample spec: # More examples in https://github.com/netobserv/network-observability-operator/tree/main/config/samples/flowmetrics - metricName: cluster_external_ingress_bytes_total type: Counter valueField: Bytes direction: Ingress - labels: [DstK8S_HostName,DstK8S_Namespace,DstK8S_OwnerName,DstK8S_OwnerType] - filters: - - field: SrcSubnetLabel - matchType: Absence - charts: - - dashboardName: Main - title: External ingress traffic - unit: Bps - type: SingleStat - queries: - - promQL: "sum(rate($METRIC[2m]))" - legend: "" - - dashboardName: Main - sectionName: External - title: Top external ingress traffic per workload - unit: Bps - type: StackArea - queries: - - promQL: "sum(rate($METRIC{DstK8S_Namespace!=\"\"}[2m])) by (DstK8S_Namespace, DstK8S_OwnerName)" - legend: "{{DstK8S_Namespace}} / {{DstK8S_OwnerName}}" + labels: [SrcK8S_Namespace,DstK8S_Namespace] + # filters: + # - field: SrcSubnetLabel + # matchType: Absence + # charts: + # - dashboardName: Main + # title: External ingress traffic + # unit: Bps + # type: SingleStat + # queries: + # - promQL: "sum(rate($METRIC[2m]))" + # legend: "" + # - dashboardName: Main + # sectionName: External + # title: Top external ingress traffic per workload + # unit: Bps + # type: StackArea + # queries: + # - promQL: "sum(rate($METRIC{DstK8S_Namespace!=\"\"}[2m])) by (DstK8S_Namespace, DstK8S_OwnerName)" + # legend: "{{DstK8S_Namespace}} / {{DstK8S_OwnerName}}" diff --git a/config/samples/flows_v1beta2_flowcollector.yaml b/config/samples/flows_v1beta2_flowcollector.yaml index 451261884..c6f2ed994 100644 --- a/config/samples/flows_v1beta2_flowcollector.yaml +++ b/config/samples/flows_v1beta2_flowcollector.yaml @@ -121,6 +121,11 @@ spec: # info: "5" # groupBy: Namespace # lowVolumeThreshold: "5" + slicesConfig: + enable: false + collectionMode: AllowList + namespacesAllowList: + - /openshift-.*|netobserv.*/ # Kafka consumer stage configuration kafkaConsumerReplicas: 3 kafkaConsumerAutoscaler: null diff --git a/config/samples/kustomization.yaml b/config/samples/kustomization.yaml index 57c851a8a..47d8c88fb 100644 --- a/config/samples/kustomization.yaml +++ b/config/samples/kustomization.yaml @@ -4,3 +4,4 @@ kind: Kustomization resources: - flows_v1beta2_flowcollector.yaml - flows_v1alpha1_flowmetric.yaml +- flows_v1alpha1_flowcollectorslice.yaml diff --git a/docs/Architecture.md b/docs/Architecture.md index 39770c45b..07ce5f3be 100644 --- a/docs/Architecture.md +++ b/docs/Architecture.md @@ -22,7 +22,7 @@ The components are: - Different views include metrics overview, a network topology and a table listing raw flows logs. - It supports multi-tenant access, making it relevant for various use cases: cluster/network admins, SREs, development teams... - [An operator](https://github.com/netobserv/network-observability-operator) that manages all of the above. - - It provides two APIs (CRD), one called [FlowCollector](https://github.com/netobserv/network-observability-operator/blob/main/docs/FlowCollector.md), which configures and pilots the whole deployment, and another called [FlowMetrics](https://github.com/netobserv/network-observability-operator/blob/main/docs/FlowMetric.md) which allows to customize which metrics to generate out of flow logs. + - It provides three APIs (CRD), one called [FlowCollector](https://github.com/netobserv/network-observability-operator/blob/main/docs/FlowCollector.md), which configures and pilots the whole deployment, another called [FlowCollectorSlice](https://github.com/netobserv/network-observability-operator/blob/main/docs/FlowCollectorSlice.md) for per-tenant configuration, and lastly [FlowMetrics](https://github.com/netobserv/network-observability-operator/blob/main/docs/FlowMetric.md) which allows to customize which metrics to generate out of flow logs. - As an [OLM operator](https://olm.operatorframework.io/), it is designed with `operator-sdk`, and allows subscriptions for easy updates. - [A CLI](https://github.com/netobserv/network-observability-cli) that also manages some of the above components, for on-demand monitoring and packet capture. - It is provided as a `kubectl` or `oc` plugin, allowing to capture flows (similar to what the operator does, except it's on-demand and in the terminal), full packets (much like a `tcpdump` command) or metrics. diff --git a/docs/FlowCollector.md b/docs/FlowCollector.md index 1abc9937f..06ce563d8 100644 --- a/docs/FlowCollector.md +++ b/docs/FlowCollector.md @@ -8538,6 +8538,13 @@ For more information, see https://kubernetes.io/docs/concepts/configuration/mana Default: map[limits:map[memory:800Mi] requests:map[cpu:100m memory:100Mi]]
false + + slicesConfig + object + + Global configuration managing FlowCollectorSlices custom resources.
+ + false subnetLabels object @@ -11982,6 +11989,56 @@ only the result of this request.
+### FlowCollector.spec.processor.slicesConfig +[↩ Parent](#flowcollectorspecprocessor) + + + +Global configuration managing FlowCollectorSlices custom resources. + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionRequired
enableboolean + `enable` determines if the FlowCollectorSlice feature is enabled. If not, all resources of kind FlowCollectorSlice are simply ignored.
+
+ Default: false
+
true
collectionModeenum + `collectionMode` determines how the FlowCollectorSlice custom resources impacts the flow collection process:
+- When set to `AlwaysCollect`, all flows are collected regardless of the presence of FlowCollectorSlice.
+- When set to `AllowList`, only the flows related to namespaces where a FlowCollectorSlice resource is present, or configured via the global `namespacesAllowList`, are collected.

+
+ Enum: AlwaysCollect, AllowList
+ Default: AlwaysCollect
+
false
namespacesAllowList[]string + `namespacesAllowList` is a list of namespaces for which flows are always collected, regardless of the presence of FlowCollectorSlice in those namespaces. +An entry enclosed by slashes, such as `/openshift-.*/`, is matched as a regular expression. +This setting is ignored if `collectionMode` is different from `AllowList`.
+
false
+ + ### FlowCollector.spec.processor.subnetLabels [↩ Parent](#flowcollectorspecprocessor) diff --git a/docs/FlowCollectorSlice.md b/docs/FlowCollectorSlice.md new file mode 100644 index 000000000..98e581060 --- /dev/null +++ b/docs/FlowCollectorSlice.md @@ -0,0 +1,259 @@ +# API Reference + +Packages: + +- [flows.netobserv.io/v1alpha1](#flowsnetobserviov1alpha1) + +# flows.netobserv.io/v1alpha1 + +Resource Types: + +- [FlowCollectorSlice](#flowcollectorslice) + + + + +## FlowCollectorSlice +[↩ Parent](#flowsnetobserviov1alpha1 ) + + + + + + +FlowMetric is the API allowing to create custom metrics from the collected flow logs. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionRequired
apiVersionstringflows.netobserv.io/v1alpha1true
kindstringFlowCollectorSlicetrue
metadataobjectRefer to the Kubernetes API documentation for the fields of the `metadata` field.true
specobject + FlowCollectorSliceSpec defines the desired state of FlowCollectorSlice
+
false
statusobject + FlowCollectorSliceStatus defines the observed state of FlowCollectorSlice
+
false
+ + +### FlowCollectorSlice.spec +[↩ Parent](#flowcollectorslice) + + + +FlowCollectorSliceSpec defines the desired state of FlowCollectorSlice + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionRequired
samplinginteger + `sampling` is an optional sampling interval to apply to this slice. For example, a value of `50` means that 1 matching flow in 50 is sampled.
+
+ Format: int32
+ Minimum: 0
+
false
subnetLabels[]object + `subnetLabels` allows to customize subnets and IPs labelling, such as to identify cluster-external workloads or web services. +Beware that the subnet labels configured in FlowCollectorSlice are not limited to the flows of the related namespace: any flow +in the whole cluster can be labelled using this configuration. However, subnet labels defined in the cluster-scoped FlowCollector take +precedence in case of conflicting rules.
+
false
+ + +### FlowCollectorSlice.spec.subnetLabels[index] +[↩ Parent](#flowcollectorslicespec) + + + +SubnetLabel allows to label subnets and IPs, such as to identify cluster-external workloads or web services. + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionRequired
cidrs[]string + List of CIDRs, such as `["1.2.3.4/32"]`.
+
true
namestring + Label name, used to flag matching flows.
+
true
+ + +### FlowCollectorSlice.status +[↩ Parent](#flowcollectorslice) + + + +FlowCollectorSliceStatus defines the observed state of FlowCollectorSlice + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionRequired
conditions[]object + `conditions` represent the latest available observations of an object's state
+
true
filterAppliedstring + Filter that is applied for flow collection
+
false
subnetLabelsConfiguredinteger + Number of subnet labels configured
+
false
+ + +### FlowCollectorSlice.status.conditions[index] +[↩ Parent](#flowcollectorslicestatus) + + + +Condition contains details for one aspect of the current state of this API Resource. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionRequired
lastTransitionTimestring + lastTransitionTime is the last time the condition transitioned from one status to another. +This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.
+
+ Format: date-time
+
true
messagestring + message is a human readable message indicating details about the transition. +This may be an empty string.
+
true
reasonstring + reason contains a programmatic identifier indicating the reason for the condition's last transition. +Producers of specific condition types may define expected values and meanings for this field, +and whether the values are considered a guaranteed API. +The value should be a CamelCase string. +This field may not be empty.
+
true
statusenum + status of the condition, one of True, False, Unknown.
+
+ Enum: True, False, Unknown
+
true
typestring + type of condition in CamelCase or in foo.example.com/CamelCase.
+
true
observedGenerationinteger + observedGeneration represents the .metadata.generation that the condition was set based upon. +For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date +with respect to the current state of the instance.
+
+ Format: int64
+ Minimum: 0
+
false
\ No newline at end of file diff --git a/hack/asciidoc-gen-config.yaml b/hack/asciidoc-gen-config.yaml index 63065fcc6..af93883ef 100644 --- a/hack/asciidoc-gen-config.yaml +++ b/hack/asciidoc-gen-config.yaml @@ -10,3 +10,6 @@ apiMap: - kind: FlowMetric group: flows.netobserv.io version: v1alpha1 + - kind: FlowCollectorSlice + group: flows.netobserv.io + version: v1alpha1 diff --git a/hack/asciidoc-gen.sh b/hack/asciidoc-gen.sh index 2ac42ca80..1dc3aabdf 100755 --- a/hack/asciidoc-gen.sh +++ b/hack/asciidoc-gen.sh @@ -5,7 +5,7 @@ set -e mkdir -p _tmp oc get --raw /openapi/v2 | jq . > _tmp/openapi.1.json -jq '.definitions |= ({"io.netobserv.flows.v1beta2.FlowCollector", "io.netobserv.flows.v1alpha1.FlowMetric"}) +jq '.definitions |= ({"io.netobserv.flows.v1beta2.FlowCollector", "io.netobserv.flows.v1alpha1.FlowMetric", "io.netobserv.flows.v1alpha1.FlowCollectorSlice"}) | del(.definitions."io.netobserv.flows.v1beta2.FlowCollector".properties.status) | del(.definitions."io.netobserv.flows.v1beta2.FlowCollector".properties.metadata."$ref") | .definitions."io.netobserv.flows.v1beta2.FlowCollector".properties.metadata += {type:"object"} @@ -25,7 +25,10 @@ jq '.definitions |= ({"io.netobserv.flows.v1beta2.FlowCollector", "io.netobserv. | .definitions."io.netobserv.flows.v1beta2.FlowCollector".properties.spec.properties.processor.properties.kafkaConsumerAutoscaler.description |= . + " Refer to HorizontalPodAutoscaler documentation (autoscaling/v2)." | del(.definitions."io.netobserv.flows.v1alpha1.FlowMetric".properties.status) | del(.definitions."io.netobserv.flows.v1alpha1.FlowMetric".properties.metadata."$ref") - | .definitions."io.netobserv.flows.v1alpha1.FlowMetric".properties.metadata += {type:"object"}' \ + | .definitions."io.netobserv.flows.v1alpha1.FlowMetric".properties.metadata += {type:"object"} + | del(.definitions."io.netobserv.flows.v1alpha1.FlowCollectorSlice".properties.status) + | del(.definitions."io.netobserv.flows.v1alpha1.FlowCollectorSlice".properties.metadata."$ref") + | .definitions."io.netobserv.flows.v1alpha1.FlowCollectorSlice".properties.metadata += {type:"object"}' \ _tmp/openapi.1.json > _tmp/openapi.2.json openshift-apidocs-gen build -c hack/asciidoc-gen-config.yaml _tmp/openapi.2.json @@ -57,3 +60,4 @@ amend_doc() { amend_doc "flowcollector-flows-netobserv-io-v1beta2.adoc" amend_doc "flowmetric-flows-netobserv-io-v1alpha1.adoc" +amend_doc "flowcollectorslice-flows-netobserv-io-v1alpha1.adoc" diff --git a/hack/helm-update.sh b/hack/helm-update.sh index 1de45f7a7..dd4ec17e8 100755 --- a/hack/helm-update.sh +++ b/hack/helm-update.sh @@ -3,7 +3,7 @@ mkdir -p _tmp # Copy and edit CRDs -for crd in "flows.netobserv.io_flowcollectors.yaml" "flows.netobserv.io_flowmetrics.yaml"; do +for crd in "flows.netobserv.io_flowcollectors.yaml" "flows.netobserv.io_flowmetrics.yaml" "flows.netobserv.io_flowcollectorslices.yaml"; do cp "bundle/manifests/$crd" helm/crds sed -i -r 's/(`[^`]*\{\{[^`]*`)/{{\1}}/g' helm/crds/$crd # escape "{{" for helm yq -i 'del(.spec.conversion)' helm/crds/$crd diff --git a/helm/crds/flows.netobserv.io_flowcollectors.yaml b/helm/crds/flows.netobserv.io_flowcollectors.yaml index 153219ed7..4d682faf8 100644 --- a/helm/crds/flows.netobserv.io_flowcollectors.yaml +++ b/helm/crds/flows.netobserv.io_flowcollectors.yaml @@ -5649,6 +5649,34 @@ spec: More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ type: object type: object + slicesConfig: + description: Global configuration managing FlowCollectorSlices custom resources. + properties: + collectionMode: + default: AlwaysCollect + description: |- + `collectionMode` determines how the FlowCollectorSlice custom resources impacts the flow collection process:
+ - When set to `AlwaysCollect`, all flows are collected regardless of the presence of FlowCollectorSlice.
+ - When set to `AllowList`, only the flows related to namespaces where a FlowCollectorSlice resource is present, or configured via the global `namespacesAllowList`, are collected.
+ enum: + - AlwaysCollect + - AllowList + type: string + enable: + default: false + description: '`enable` determines if the FlowCollectorSlice feature is enabled. If not, all resources of kind FlowCollectorSlice are simply ignored.' + type: boolean + namespacesAllowList: + description: |- + `namespacesAllowList` is a list of namespaces for which flows are always collected, regardless of the presence of FlowCollectorSlice in those namespaces. + An entry enclosed by slashes, such as `/openshift-.*/`, is matched as a regular expression. + This setting is ignored if `collectionMode` is different from `AllowList`. + items: + type: string + type: array + required: + - enable + type: object subnetLabels: description: |- `subnetLabels` allows to define custom labels on subnets and IPs or to enable automatic labelling of recognized subnets in OpenShift, which is used to identify cluster external traffic. diff --git a/helm/crds/flows.netobserv.io_flowcollectorslices.yaml b/helm/crds/flows.netobserv.io_flowcollectorslices.yaml new file mode 100644 index 000000000..4f47a4f3e --- /dev/null +++ b/helm/crds/flows.netobserv.io_flowcollectorslices.yaml @@ -0,0 +1,148 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.16.2 + creationTimestamp: null + name: flowcollectorslices.flows.netobserv.io +spec: + group: flows.netobserv.io + names: + kind: FlowCollectorSlice + listKind: FlowCollectorSliceList + plural: flowcollectorslices + singular: flowcollectorslice + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: FlowMetric is the API allowing to create custom metrics from the collected flow logs. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: FlowCollectorSliceSpec defines the desired state of FlowCollectorSlice + properties: + sampling: + description: '`sampling` is an optional sampling interval to apply to this slice. For example, a value of `50` means that 1 matching flow in 50 is sampled.' + format: int32 + minimum: 0 + type: integer + subnetLabels: + description: |- + `subnetLabels` allows to customize subnets and IPs labelling, such as to identify cluster-external workloads or web services. + Beware that the subnet labels configured in FlowCollectorSlice are not limited to the flows of the related namespace: any flow + in the whole cluster can be labelled using this configuration. However, subnet labels defined in the cluster-scoped FlowCollector take + precedence in case of conflicting rules. + items: + description: SubnetLabel allows to label subnets and IPs, such as to identify cluster-external workloads or web services. + properties: + cidrs: + description: List of CIDRs, such as `["1.2.3.4/32"]`. + items: + type: string + type: array + name: + description: Label name, used to flag matching flows. + type: string + required: + - cidrs + - name + type: object + type: array + type: object + status: + description: FlowCollectorSliceStatus defines the observed state of FlowCollectorSlice + properties: + conditions: + description: '`conditions` represent the latest available observations of an object''s state' + items: + description: Condition contains details for one aspect of the current state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + filterApplied: + description: Filter that is applied for flow collection + type: string + subnetLabelsConfigured: + description: Number of subnet labels configured + type: integer + required: + - conditions + type: object + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: null + storedVersions: null diff --git a/helm/templates/clusterrole.yaml b/helm/templates/clusterrole.yaml index 35d43a950..fbc8e7555 100644 --- a/helm/templates/clusterrole.yaml +++ b/helm/templates/clusterrole.yaml @@ -138,6 +138,7 @@ rules: - flows.netobserv.io resources: - flowcollectors + - flowcollectorslices - flowmetrics verbs: - create @@ -157,6 +158,7 @@ rules: - flows.netobserv.io resources: - flowcollectors/status + - flowcollectorslices/status - flowmetrics/status verbs: - get diff --git a/internal/controller/flp/flp_controller.go b/internal/controller/flp/flp_controller.go index d585b2274..a9fa1c111 100644 --- a/internal/controller/flp/flp_controller.go +++ b/internal/controller/flp/flp_controller.go @@ -3,11 +3,15 @@ package flp import ( "context" "fmt" + "slices" + "strings" flowslatest "github.com/netobserv/network-observability-operator/api/flowcollector/v1beta2" + sliceslatest "github.com/netobserv/network-observability-operator/api/flowcollectorslice/v1alpha1" metricslatest "github.com/netobserv/network-observability-operator/api/flowmetrics/v1alpha1" "github.com/netobserv/network-observability-operator/internal/controller/constants" "github.com/netobserv/network-observability-operator/internal/controller/flp/fmstatus" + "github.com/netobserv/network-observability-operator/internal/controller/flp/slicesstatus" "github.com/netobserv/network-observability-operator/internal/controller/reconcilers" "github.com/netobserv/network-observability-operator/internal/pkg/helper" "github.com/netobserv/network-observability-operator/internal/pkg/manager" @@ -59,6 +63,11 @@ func Start(ctx context.Context, mgr *manager.Manager) (manager.PostCreateHook, e return []reconcile.Request{} }), reconcilers.IgnoreStatusChange, + ). + Watches( + &sliceslatest.FlowCollectorSlice{}, + &handler.EnqueueRequestForObject{}, + reconcilers.IgnoreStatusChange, ) ctrl, err := builder.Build(&r) @@ -72,7 +81,7 @@ func Start(ctx context.Context, mgr *manager.Manager) (manager.PostCreateHook, e type subReconciler interface { context(context.Context) context.Context - reconcile(context.Context, *flowslatest.FlowCollector, *metricslatest.FlowMetricList, []flowslatest.SubnetLabel) error + reconcile(context.Context, *flowslatest.FlowCollector, *metricslatest.FlowMetricList, []sliceslatest.FlowCollectorSlice, []flowslatest.SubnetLabel) error getStatus() *status.Instance } @@ -137,6 +146,20 @@ func (r *Reconciler) reconcile(ctx context.Context, clh *helper.Client, fc *flow fmstatus.Reset() defer fmstatus.Sync(ctx, r.Client, &fm) + // List flowcollector slices + fcSlices := sliceslatest.FlowCollectorSliceList{} + if fc.Spec.IsSliceEnabled() { + if err := r.Client.List(ctx, &fcSlices); err != nil { + return r.status.Error("CantListFlowCollectorSlices", err) + } + // Sort alphabetically + slices.SortFunc(fcSlices.Items, func(a, b sliceslatest.FlowCollectorSlice) int { + return strings.Compare(a.Name, b.Name) + }) + slicesstatus.Reset(&fcSlices) + defer slicesstatus.Sync(ctx, r.Client, &fcSlices) + } + // Create sub-reconcilers // TODO: refactor to move these subReconciler allocations in `Start`. It will involve some decoupling work, as currently // `reconcilers.Common` is dependent on the FlowCollector object, which isn't known at start time. @@ -155,7 +178,7 @@ func (r *Reconciler) reconcile(ctx context.Context, clh *helper.Client, fc *flow } for _, sr := range reconcilers { - if err := sr.reconcile(sr.context(ctx), fc, &fm, subnetLabels); err != nil { + if err := sr.reconcile(sr.context(ctx), fc, &fm, fcSlices.Items, subnetLabels); err != nil { return sr.getStatus().Error("FLPReconcileError", err) } } diff --git a/internal/controller/flp/flp_monolith_objects.go b/internal/controller/flp/flp_monolith_objects.go index 9879e863e..dc2773d0b 100644 --- a/internal/controller/flp/flp_monolith_objects.go +++ b/internal/controller/flp/flp_monolith_objects.go @@ -2,6 +2,7 @@ package flp import ( flowslatest "github.com/netobserv/network-observability-operator/api/flowcollector/v1beta2" + sliceslatest "github.com/netobserv/network-observability-operator/api/flowcollectorslice/v1alpha1" metricslatest "github.com/netobserv/network-observability-operator/api/flowmetrics/v1alpha1" "github.com/netobserv/network-observability-operator/internal/controller/constants" "github.com/netobserv/network-observability-operator/internal/controller/reconcilers" @@ -27,13 +28,14 @@ type monolithBuilder struct { info *reconcilers.Instance desired *flowslatest.FlowCollectorSpec flowMetrics *metricslatest.FlowMetricList + fcSlices []sliceslatest.FlowCollectorSlice detectedSubnets []flowslatest.SubnetLabel version string promTLS *flowslatest.CertificateReference volumes volumes.Builder } -func newMonolithBuilder(info *reconcilers.Instance, desired *flowslatest.FlowCollectorSpec, flowMetrics *metricslatest.FlowMetricList, detectedSubnets []flowslatest.SubnetLabel) (monolithBuilder, error) { +func newMonolithBuilder(info *reconcilers.Instance, desired *flowslatest.FlowCollectorSpec, flowMetrics *metricslatest.FlowMetricList, fcSlices []sliceslatest.FlowCollectorSlice, detectedSubnets []flowslatest.SubnetLabel) (monolithBuilder, error) { version := helper.ExtractVersion(info.Images[reconcilers.MainImage]) promTLS, err := getPromTLS(desired, constants.FLPMetricsSvcName) if err != nil { @@ -43,6 +45,7 @@ func newMonolithBuilder(info *reconcilers.Instance, desired *flowslatest.FlowCol info: info, desired: desired, flowMetrics: flowMetrics, + fcSlices: fcSlices, detectedSubnets: detectedSubnets, version: helper.MaxLabelLength(version), promTLS: promTLS, @@ -116,15 +119,16 @@ func (b *monolithBuilder) deployment(annotations map[string]string) *appsv1.Depl } func (b *monolithBuilder) configMaps() (*corev1.ConfigMap, string, *corev1.ConfigMap, error) { - kafkaStage := newGRPCPipeline(b.desired) + grpcStage := newGRPCPipeline(b.desired) pipeline := newPipelineBuilder( b.desired, b.flowMetrics, + b.fcSlices, b.detectedSubnets, b.info.Loki, b.info.ClusterInfo.GetID(), &b.volumes, - &kafkaStage, + &grpcStage, ) err := pipeline.AddProcessorStages() if err != nil { diff --git a/internal/controller/flp/flp_monolith_reconciler.go b/internal/controller/flp/flp_monolith_reconciler.go index 843872e1a..ede15f620 100644 --- a/internal/controller/flp/flp_monolith_reconciler.go +++ b/internal/controller/flp/flp_monolith_reconciler.go @@ -11,6 +11,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/log" flowslatest "github.com/netobserv/network-observability-operator/api/flowcollector/v1beta2" + sliceslatest "github.com/netobserv/network-observability-operator/api/flowcollectorslice/v1alpha1" metricslatest "github.com/netobserv/network-observability-operator/api/flowmetrics/v1alpha1" "github.com/netobserv/network-observability-operator/internal/controller/constants" "github.com/netobserv/network-observability-operator/internal/controller/reconcilers" @@ -70,7 +71,7 @@ func (r *monolithReconciler) getStatus() *status.Instance { return &r.Status } -func (r *monolithReconciler) reconcile(ctx context.Context, desired *flowslatest.FlowCollector, flowMetrics *metricslatest.FlowMetricList, detectedSubnets []flowslatest.SubnetLabel) error { +func (r *monolithReconciler) reconcile(ctx context.Context, desired *flowslatest.FlowCollector, flowMetrics *metricslatest.FlowMetricList, fcSlices []sliceslatest.FlowCollectorSlice, detectedSubnets []flowslatest.SubnetLabel) error { // Retrieve current owned objects err := r.Managed.FetchAll(ctx) if err != nil { @@ -85,7 +86,7 @@ func (r *monolithReconciler) reconcile(ctx context.Context, desired *flowslatest r.Status.SetReady() // will be overidden if necessary, as error or pending - builder, err := newMonolithBuilder(r.Instance, &desired.Spec, flowMetrics, detectedSubnets) + builder, err := newMonolithBuilder(r.Instance, &desired.Spec, flowMetrics, fcSlices, detectedSubnets) if err != nil { return err } diff --git a/internal/controller/flp/flp_pipeline_builder.go b/internal/controller/flp/flp_pipeline_builder.go index 064af47ac..0ef2d7cad 100644 --- a/internal/controller/flp/flp_pipeline_builder.go +++ b/internal/controller/flp/flp_pipeline_builder.go @@ -2,6 +2,7 @@ package flp import ( "fmt" + "net" "strconv" "strings" "time" @@ -12,6 +13,7 @@ import ( "github.com/prometheus/common/model" flowslatest "github.com/netobserv/network-observability-operator/api/flowcollector/v1beta2" + sliceslatest "github.com/netobserv/network-observability-operator/api/flowcollectorslice/v1alpha1" metricslatest "github.com/netobserv/network-observability-operator/api/flowmetrics/v1alpha1" "github.com/netobserv/network-observability-operator/internal/controller/constants" "github.com/netobserv/network-observability-operator/internal/controller/flp/fmstatus" @@ -30,7 +32,8 @@ const ( type PipelineBuilder struct { *config.PipelineBuilderStage desired *flowslatest.FlowCollectorSpec - flowMetrics metricslatest.FlowMetricList + flowMetrics *metricslatest.FlowMetricList + fcSlices []sliceslatest.FlowCollectorSlice detectedSubnets []flowslatest.SubnetLabel volumes *volumes.Builder loki *helper.LokiConfig @@ -40,6 +43,7 @@ type PipelineBuilder struct { func newPipelineBuilder( desired *flowslatest.FlowCollectorSpec, flowMetrics *metricslatest.FlowMetricList, + fcSlices []sliceslatest.FlowCollectorSlice, detectedSubnets []flowslatest.SubnetLabel, loki *helper.LokiConfig, clusterID string, @@ -49,7 +53,8 @@ func newPipelineBuilder( return PipelineBuilder{ PipelineBuilderStage: pipeline, desired: desired, - flowMetrics: *flowMetrics, + flowMetrics: flowMetrics, + fcSlices: fcSlices, detectedSubnets: detectedSubnets, loki: loki, clusterID: clusterID, @@ -68,7 +73,23 @@ func (b *PipelineBuilder) AddProcessorStages() error { addZone := b.desired.Processor.IsZoneEnabled() // Get all subnet labels + // Highest priority: admin-defined labels allLabels := b.desired.Processor.SubnetLabels.CustomLabels + var cidrs []*net.IPNet + for _, label := range allLabels { + for _, strCIDR := range label.CIDRs { + _, cidr, err := net.ParseCIDR(strCIDR) + if err != nil { + return fmt.Errorf("wrong CIDR for subnet label '%s': %w", label.Name, err) + } + cidrs = append(cidrs, cidr) + } + } + // Then: slice labels + if b.desired.IsSliceEnabled() { + allLabels = append(allLabels, slicesToFCSubnetLabels(b.fcSlices, cidrs)...) + } + // Finally: detected/fallback labels allLabels = append(allLabels, b.detectedSubnets...) flpLabels := subnetLabelsToFLP(allLabels) @@ -199,6 +220,10 @@ func (b *PipelineBuilder) AddProcessorStages() error { // Custom filters filters := filtersToFLP(b.desired.Processor.Filters, flowslatest.FLPFilterTargetAll) + sliceFilters := slicesToFilters(b.desired, b.fcSlices) + if len(sliceFilters) > 0 { + filters = append(filters, sliceFilters...) + } if len(filters) > 0 { nextStage = nextStage.TransformFilter("filters", newTransformFilter(filters)) } diff --git a/internal/controller/flp/flp_test.go b/internal/controller/flp/flp_test.go index cf443fdbe..324151c3f 100644 --- a/internal/controller/flp/flp_test.go +++ b/internal/controller/flp/flp_test.go @@ -160,14 +160,14 @@ func monoBuilder(ns string, cfg *flowslatest.FlowCollectorSpec) monolithBuilder func monoBuilderWithMetrics(ns string, cfg *flowslatest.FlowCollectorSpec, metrics *metricslatest.FlowMetricList) monolithBuilder { loki := helper.NewLokiConfig(&cfg.Loki, "any") info := reconcilers.Common{Namespace: ns, Loki: &loki, ClusterInfo: &cluster.Info{}} - b, _ := newMonolithBuilder(info.NewInstance(image, status.Instance{}), cfg, metrics, nil) + b, _ := newMonolithBuilder(info.NewInstance(image, status.Instance{}), cfg, metrics, nil, nil) return b } func transfBuilder(ns string, cfg *flowslatest.FlowCollectorSpec) transfoBuilder { loki := helper.NewLokiConfig(&cfg.Loki, "any") info := reconcilers.Common{Namespace: ns, Loki: &loki, ClusterInfo: &cluster.Info{}} - b, _ := newTransfoBuilder(info.NewInstance(image, status.Instance{}), cfg, &metricslatest.FlowMetricList{}, nil) + b, _ := newTransfoBuilder(info.NewInstance(image, status.Instance{}), cfg, &metricslatest.FlowMetricList{}, nil, nil) return b } @@ -545,7 +545,7 @@ func TestServiceMonitorChanged(t *testing.T) { // Check labels change info := reconcilers.Common{Namespace: "namespace2", ClusterInfo: &cluster.Info{}} - b, _ = newMonolithBuilder(info.NewInstance(image2, status.Instance{}), &cfg, b.flowMetrics, nil) + b, _ = newMonolithBuilder(info.NewInstance(image2, status.Instance{}), &cfg, b.flowMetrics, nil, nil) third := b.serviceMonitor() report = helper.NewChangeReport("") @@ -553,7 +553,7 @@ func TestServiceMonitorChanged(t *testing.T) { assert.Contains(report.String(), "ServiceMonitor labels changed") // Check scheme changed - b, _ = newMonolithBuilder(info.NewInstance(image2, status.Instance{}), &cfg, b.flowMetrics, nil) + b, _ = newMonolithBuilder(info.NewInstance(image2, status.Instance{}), &cfg, b.flowMetrics, nil, nil) fourth := b.serviceMonitor() fourth.Spec.Endpoints[0].Scheme = "https" @@ -601,7 +601,7 @@ func TestPrometheusRuleChanged(t *testing.T) { // Check labels change info := reconcilers.Common{Namespace: "namespace2", ClusterInfo: &cluster.Info{}} - b, _ = newMonolithBuilder(info.NewInstance(image2, status.Instance{}), &cfg, b.flowMetrics, nil) + b, _ = newMonolithBuilder(info.NewInstance(image2, status.Instance{}), &cfg, b.flowMetrics, nil, nil) r = alerts.BuildRules(context.Background(), &cfg) third := b.prometheusRule(r) @@ -750,8 +750,8 @@ func TestLabels(t *testing.T) { cfg := getConfig() info := reconcilers.Common{Namespace: "ns", ClusterInfo: &cluster.Info{}} - builder, _ := newMonolithBuilder(info.NewInstance(image, status.Instance{}), &cfg, &metricslatest.FlowMetricList{}, nil) - tBuilder, _ := newTransfoBuilder(info.NewInstance(image, status.Instance{}), &cfg, &metricslatest.FlowMetricList{}, nil) + builder, _ := newMonolithBuilder(info.NewInstance(image, status.Instance{}), &cfg, &metricslatest.FlowMetricList{}, nil, nil) + tBuilder, _ := newTransfoBuilder(info.NewInstance(image, status.Instance{}), &cfg, &metricslatest.FlowMetricList{}, nil, nil) // Deployment depl := tBuilder.deployment(annotate("digest")) @@ -797,8 +797,8 @@ func TestToleration(t *testing.T) { cfgKafka := cfg cfgKafka.DeploymentModel = flowslatest.DeploymentModelKafka info := reconcilers.Common{Namespace: "ns", ClusterInfo: &cluster.Info{}} - builder, _ := newMonolithBuilder(info.NewInstance(image, status.Instance{}), &cfg, &metricslatest.FlowMetricList{}, nil) - tBuilder, _ := newTransfoBuilder(info.NewInstance(image, status.Instance{}), &cfgKafka, &metricslatest.FlowMetricList{}, nil) + builder, _ := newMonolithBuilder(info.NewInstance(image, status.Instance{}), &cfg, &metricslatest.FlowMetricList{}, nil, nil) + tBuilder, _ := newTransfoBuilder(info.NewInstance(image, status.Instance{}), &cfgKafka, &metricslatest.FlowMetricList{}, nil, nil) // Deployment: no specific toleration depl := tBuilder.deployment(annotate("digest")) diff --git a/internal/controller/flp/flp_transfo_objects.go b/internal/controller/flp/flp_transfo_objects.go index 936c7f595..7733dfcc9 100644 --- a/internal/controller/flp/flp_transfo_objects.go +++ b/internal/controller/flp/flp_transfo_objects.go @@ -7,6 +7,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" flowslatest "github.com/netobserv/network-observability-operator/api/flowcollector/v1beta2" + sliceslatest "github.com/netobserv/network-observability-operator/api/flowcollectorslice/v1alpha1" metricslatest "github.com/netobserv/network-observability-operator/api/flowmetrics/v1alpha1" "github.com/netobserv/network-observability-operator/internal/controller/constants" "github.com/netobserv/network-observability-operator/internal/controller/reconcilers" @@ -28,13 +29,14 @@ type transfoBuilder struct { info *reconcilers.Instance desired *flowslatest.FlowCollectorSpec flowMetrics *metricslatest.FlowMetricList + fcSlices []sliceslatest.FlowCollectorSlice detectedSubnets []flowslatest.SubnetLabel version string promTLS *flowslatest.CertificateReference volumes volumes.Builder } -func newTransfoBuilder(info *reconcilers.Instance, desired *flowslatest.FlowCollectorSpec, flowMetrics *metricslatest.FlowMetricList, detectedSubnets []flowslatest.SubnetLabel) (transfoBuilder, error) { +func newTransfoBuilder(info *reconcilers.Instance, desired *flowslatest.FlowCollectorSpec, flowMetrics *metricslatest.FlowMetricList, fcSlices []sliceslatest.FlowCollectorSlice, detectedSubnets []flowslatest.SubnetLabel) (transfoBuilder, error) { version := helper.ExtractVersion(info.Images[reconcilers.MainImage]) promTLS, err := getPromTLS(desired, constants.FLPTransfoMetricsSvcName) if err != nil { @@ -44,6 +46,7 @@ func newTransfoBuilder(info *reconcilers.Instance, desired *flowslatest.FlowColl info: info, desired: desired, flowMetrics: flowMetrics, + fcSlices: fcSlices, detectedSubnets: detectedSubnets, version: helper.MaxLabelLength(version), promTLS: promTLS, @@ -87,6 +90,7 @@ func (b *transfoBuilder) configMaps() (*corev1.ConfigMap, string, *corev1.Config pipeline := newPipelineBuilder( b.desired, b.flowMetrics, + b.fcSlices, b.detectedSubnets, b.info.Loki, b.info.ClusterInfo.GetID(), diff --git a/internal/controller/flp/flp_transfo_reconciler.go b/internal/controller/flp/flp_transfo_reconciler.go index 0b13ebb5c..5a9fe67ac 100644 --- a/internal/controller/flp/flp_transfo_reconciler.go +++ b/internal/controller/flp/flp_transfo_reconciler.go @@ -12,6 +12,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/log" flowslatest "github.com/netobserv/network-observability-operator/api/flowcollector/v1beta2" + sliceslatest "github.com/netobserv/network-observability-operator/api/flowcollectorslice/v1alpha1" metricslatest "github.com/netobserv/network-observability-operator/api/flowmetrics/v1alpha1" "github.com/netobserv/network-observability-operator/internal/controller/constants" "github.com/netobserv/network-observability-operator/internal/controller/reconcilers" @@ -67,7 +68,7 @@ func (r *transformerReconciler) getStatus() *status.Instance { return &r.Status } -func (r *transformerReconciler) reconcile(ctx context.Context, desired *flowslatest.FlowCollector, flowMetrics *metricslatest.FlowMetricList, detectedSubnets []flowslatest.SubnetLabel) error { +func (r *transformerReconciler) reconcile(ctx context.Context, desired *flowslatest.FlowCollector, flowMetrics *metricslatest.FlowMetricList, fcSlices []sliceslatest.FlowCollectorSlice, detectedSubnets []flowslatest.SubnetLabel) error { // Retrieve current owned objects err := r.Managed.FetchAll(ctx) if err != nil { @@ -82,7 +83,7 @@ func (r *transformerReconciler) reconcile(ctx context.Context, desired *flowslat r.Status.SetReady() // will be overidden if necessary, as error or pending - builder, err := newTransfoBuilder(r.Instance, &desired.Spec, flowMetrics, detectedSubnets) + builder, err := newTransfoBuilder(r.Instance, &desired.Spec, flowMetrics, fcSlices, detectedSubnets) if err != nil { return err } diff --git a/internal/controller/flp/metrics_api_test.go b/internal/controller/flp/metrics_api_test.go index 5777c9d4b..302bae808 100644 --- a/internal/controller/flp/metrics_api_test.go +++ b/internal/controller/flp/metrics_api_test.go @@ -36,7 +36,7 @@ func defaultBuilderWithMetrics(metrics *metricslatest.FlowMetricList) (monolithB cfg := getConfig() loki := helper.NewLokiConfig(&cfg.Loki, "any") info := reconcilers.Common{Namespace: "namespace", Loki: &loki, ClusterInfo: &cluster.Info{}} - return newMonolithBuilder(info.NewInstance(image, status.Instance{}), &cfg, metrics, nil) + return newMonolithBuilder(info.NewInstance(image, status.Instance{}), &cfg, metrics, nil, nil) } func metric(metrics api.MetricsItems, name string) *api.MetricsItem { diff --git a/internal/controller/flp/slices.go b/internal/controller/flp/slices.go new file mode 100644 index 000000000..03d53c85b --- /dev/null +++ b/internal/controller/flp/slices.go @@ -0,0 +1,123 @@ +package flp + +import ( + "fmt" + "net" + "strings" + + "github.com/netobserv/flowlogs-pipeline/pkg/api" + flowslatest "github.com/netobserv/network-observability-operator/api/flowcollector/v1beta2" + sliceslatest "github.com/netobserv/network-observability-operator/api/flowcollectorslice/v1alpha1" + "github.com/netobserv/network-observability-operator/internal/controller/flp/slicesstatus" +) + +func slicesToFilters(fc *flowslatest.FlowCollectorSpec, fcSlices []sliceslatest.FlowCollectorSlice) []api.TransformFilterRule { + if !fc.IsSliceEnabled() { + return nil + } + if fc.Processor.SlicesConfig.CollectionMode == flowslatest.CollectionAlwaysCollect { + return nil + } + processed := make(map[string]any) + var rules []api.TransformFilterRule + // First, process admin config + for _, ns := range fc.Processor.SlicesConfig.NamespacesAllowList { + if len(ns) >= 2 && strings.HasPrefix(ns, "/") && strings.HasSuffix(ns, "/") { + // Strings enclosed between '/' are considered as regexes + pattern := strings.TrimPrefix(strings.TrimSuffix(ns, "/"), "/") + rules = append(rules, api.TransformFilterRule{ + Type: api.KeepEntryQuery, + KeepEntryQuery: fmt.Sprintf(`SrcK8S_Namespace=~"%s" or DstK8S_Namespace=~"%s"`, pattern, pattern), + }) + } else if _, found := processed[ns]; !found { + rules = append(rules, api.TransformFilterRule{ + Type: api.KeepEntryQuery, + KeepEntryQuery: fmt.Sprintf(`SrcK8S_Namespace="%s" or DstK8S_Namespace="%s"`, ns, ns), + }) + processed[ns] = nil + } + } + // Then add slices config + for i := range fcSlices { + if _, found := processed[fcSlices[i].Namespace]; !found { + q := fmt.Sprintf(`SrcK8S_Namespace="%s" or DstK8S_Namespace="%s"`, fcSlices[i].Namespace, fcSlices[i].Namespace) + rules = append(rules, api.TransformFilterRule{ + Type: api.KeepEntryQuery, + KeepEntryQuery: q, + KeepEntrySampling: uint16(fcSlices[i].Spec.Sampling), + }) + processed[fcSlices[i].Namespace] = nil + fcSlices[i].Status.FilterApplied = q + } else { + fcSlices[i].Status.FilterApplied = "(skipped, not needed)" + } + } + return rules +} + +func slicesToFCSubnetLabels(fcSlices []sliceslatest.FlowCollectorSlice, configuredCIDRs []*net.IPNet) []flowslatest.SubnetLabel { + // In order to report any overlap warning with higher priority config, store the existing CIDRs in a temporary structure + type cidrsPerOwner struct { + cidrs []*net.IPNet + owner string + } + cidrsToCheck := []cidrsPerOwner{{cidrs: configuredCIDRs, owner: "admin"}} + var fcLabels []flowslatest.SubnetLabel + for i := range fcSlices { + var hasError bool + var countConfigured int + for _, sl := range fcSlices[i].Spec.SubnetLabels { + var strCIDRs []string + var cidrs []*net.IPNet + for _, strCIDR := range sl.CIDRs { + // Check for parse error + if _, cidr, err := net.ParseCIDR(strCIDR); err != nil { + hasError = true + slicesstatus.SetFailure(&fcSlices[i], fmt.Sprintf("Wrong CIDR for subnet label '%s': %v", sl.Name, err)) + } else { + var skip bool + // Check for overlap with higher priority CIDRs + for _, otherOwner := range cidrsToCheck { + for _, other := range otherOwner.cidrs { + if other.Contains(cidr.IP) { + thisMaskSize, _ := cidr.Mask.Size() + otherMaskSize, _ := other.Mask.Size() + if thisMaskSize >= otherMaskSize { + // E.g: admin defined 10.100.0.0/16 and slice defined 10.100.10.0/24 + // => fully included, warn and skip adding for FLP + slicesstatus.AddSubnetWarning(&fcSlices[i], fmt.Sprintf("CIDR for '%s' (%v) is fully overlapped by config (%s: %v) and will be ignored", sl.Name, cidr, otherOwner.owner, other)) + skip = true + } else { + // E.g: admin defined 10.100.0.0/17 and slice defined 10.100.0.0/16 + // => slice includes admin config, warn but add to FLP + slicesstatus.AddSubnetWarning(&fcSlices[i], fmt.Sprintf("CIDR for '%s' (%v) overlaps with config (%s: %v)", sl.Name, cidr, otherOwner.owner, other)) + } + } else if cidr.Contains(other.IP) { + // E.g: admin defined 10.100.10.0/24 and slice defined 10.100.0.0/16 + // => slice includes admin config, warn but add to FLP + slicesstatus.AddSubnetWarning(&fcSlices[i], fmt.Sprintf("CIDR for '%s' (%v) overlaps with config (%s: %v)", sl.Name, cidr, otherOwner.owner, other)) + } + } + } + if !skip { + strCIDRs = append(strCIDRs, strCIDR) + cidrs = append(cidrs, cidr) + } + } + } + if len(cidrs) > 0 { + cidrsToCheck = append(cidrsToCheck, cidrsPerOwner{cidrs: cidrs, owner: fcSlices[i].Namespace + "/" + fcSlices[i].Name}) + fcLabels = append(fcLabels, flowslatest.SubnetLabel{ + Name: sl.Name, + CIDRs: strCIDRs, + }) + countConfigured++ + } + } + fcSlices[i].Status.SubnetLabelsConfigured = countConfigured + if !hasError { + slicesstatus.SetReady(&fcSlices[i]) + } + } + return fcLabels +} diff --git a/internal/controller/flp/slices_api_test.go b/internal/controller/flp/slices_api_test.go new file mode 100644 index 000000000..242a37256 --- /dev/null +++ b/internal/controller/flp/slices_api_test.go @@ -0,0 +1,262 @@ +package flp + +import ( + "encoding/json" + "testing" + + "github.com/netobserv/flowlogs-pipeline/pkg/api" + "github.com/netobserv/flowlogs-pipeline/pkg/config" + "github.com/stretchr/testify/assert" + corev1 "k8s.io/api/core/v1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + flowslatest "github.com/netobserv/network-observability-operator/api/flowcollector/v1beta2" + sliceslatest "github.com/netobserv/network-observability-operator/api/flowcollectorslice/v1alpha1" + "github.com/netobserv/network-observability-operator/api/flowmetrics/v1alpha1" + "github.com/netobserv/network-observability-operator/internal/controller/flp/fmstatus" + "github.com/netobserv/network-observability-operator/internal/controller/flp/slicesstatus" + "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/status" +) + +var ( + adminSubnets = []flowslatest.SubnetLabel{ + { + Name: "admin", + CIDRs: []string{"10.0.0.0/16"}, + }, + } + autoSubnets = []flowslatest.SubnetLabel{ + { + Name: "test", + CIDRs: []string{"1.2.3.4/32", "10.0.0.0/8"}, + }, + } + slicez = []sliceslatest.FlowCollectorSlice{ + { + ObjectMeta: v1.ObjectMeta{Name: "a", Namespace: "ns-a"}, + }, + { + ObjectMeta: v1.ObjectMeta{Name: "b1", Namespace: "ns-b"}, + Spec: sliceslatest.FlowCollectorSliceSpec{ + SubnetLabels: []sliceslatest.SubnetLabel{ + { + Name: "my-override", + CIDRs: []string{"10.10.0.0/16"}, + }, + { + Name: "my-label", + CIDRs: []string{"100.0.0.0/24"}, + }, + }, + }, + }, + { + ObjectMeta: v1.ObjectMeta{Name: "b2", Namespace: "ns-b"}, + Spec: sliceslatest.FlowCollectorSliceSpec{ + SubnetLabels: []sliceslatest.SubnetLabel{ + { + Name: "skipped-overlap", + CIDRs: []string{"10.0.0.0/24"}, + }, + { + Name: "partial-overlap", + CIDRs: []string{"100.0.0.0/23"}, + }, + }, + }, + }, + } +) + +func getConfiguredFiltersAndSubnets(cm *corev1.ConfigMap) ([]api.TransformFilterRule, []api.NetworkTransformSubnetLabel) { + var cfs config.Root + err := json.Unmarshal([]byte(cm.Data[configFile]), &cfs) + if err != nil { + return nil, nil + } + var filters []api.TransformFilterRule + var subnetLabels []api.NetworkTransformSubnetLabel + for _, stage := range cfs.Parameters { + if stage.Transform != nil && stage.Name == "enrich" { + subnetLabels = stage.Transform.Network.SubnetLabels + } + if stage.Transform != nil && stage.Name == "filters" { + filters = stage.Transform.Filter.Rules + } + } + return filters, subnetLabels +} + +func defaultBuilderWithSlices(cfg *flowslatest.SlicesConfig) (monolithBuilder, error) { + fc := getConfig() + fc.Processor.SlicesConfig = cfg + fc.Processor.SubnetLabels.CustomLabels = adminSubnets + info := reconcilers.Common{Namespace: "namespace", Loki: &helper.LokiConfig{}, ClusterInfo: &cluster.Info{}} + return newMonolithBuilder(info.NewInstance(image, status.Instance{}), &fc, &v1alpha1.FlowMetricList{}, slicez, autoSubnets) +} + +func TestSlicesDisabled(t *testing.T) { + fmstatus.Reset() + slicesstatus.Reset(&sliceslatest.FlowCollectorSliceList{}) + b, err := defaultBuilderWithSlices(&flowslatest.SlicesConfig{Enable: false}) + assert.NoError(t, err) + cm, _, _, err := b.configMaps() + assert.NoError(t, err) + filters, subnets := getConfiguredFiltersAndSubnets(cm) + assert.Nil(t, filters) + assert.Equal(t, []api.NetworkTransformSubnetLabel{ + { + Name: "admin", + CIDRs: []string{"10.0.0.0/16"}, + }, + { + Name: "test", + CIDRs: []string{"1.2.3.4/32", "10.0.0.0/8"}, + }, + }, subnets) + for _, slice := range slicez { + assert.Nil(t, slicesstatus.GetReadyCondition(&slice)) + assert.Nil(t, slicesstatus.GetSubnetWarningCondition(&slice)) + assert.Equal(t, 0, slice.Status.SubnetLabelsConfigured) + assert.Equal(t, "", slice.Status.FilterApplied) + } +} + +func TestSlicesEnablesCollectAll(t *testing.T) { + fmstatus.Reset() + slicesstatus.Reset(&sliceslatest.FlowCollectorSliceList{}) + b, err := defaultBuilderWithSlices(&flowslatest.SlicesConfig{ + Enable: true, + CollectionMode: flowslatest.CollectionAlwaysCollect, + NamespacesAllowList: []string{"should-be-ignored"}, + }) + assert.NoError(t, err) + cm, _, _, err := b.configMaps() + assert.NoError(t, err) + filters, subnets := getConfiguredFiltersAndSubnets(cm) + assert.Nil(t, filters) + assert.Equal(t, []api.NetworkTransformSubnetLabel{ + { + Name: "admin", + CIDRs: []string{"10.0.0.0/16"}, + }, + { + Name: "my-override", + CIDRs: []string{"10.10.0.0/16"}, + }, + { + Name: "my-label", + CIDRs: []string{"100.0.0.0/24"}, + }, + { + Name: "partial-overlap", + CIDRs: []string{"100.0.0.0/23"}, + }, + { + Name: "test", + CIDRs: []string{"1.2.3.4/32", "10.0.0.0/8"}, + }, + }, subnets) + // Slice 0 + ready := slicesstatus.GetReadyCondition(&slicez[0]) + assert.NotNil(t, ready) + assert.Equal(t, v1.ConditionTrue, ready.Status) + assert.Nil(t, slicesstatus.GetSubnetWarningCondition(&slicez[0])) + assert.Equal(t, 0, slicez[0].Status.SubnetLabelsConfigured) + assert.Equal(t, "", slicez[0].Status.FilterApplied) + // Slice 1 + ready = slicesstatus.GetReadyCondition(&slicez[1]) + assert.NotNil(t, ready) + assert.Equal(t, v1.ConditionTrue, ready.Status) + assert.Nil(t, slicesstatus.GetSubnetWarningCondition(&slicez[1])) + assert.Equal(t, 2, slicez[1].Status.SubnetLabelsConfigured) + assert.Equal(t, "", slicez[1].Status.FilterApplied) + // Slice 2 + ready = slicesstatus.GetReadyCondition(&slicez[2]) + assert.NotNil(t, ready) + assert.Equal(t, v1.ConditionTrue, ready.Status) + warnings := slicesstatus.GetSubnetWarningCondition(&slicez[2]) + assert.NotNil(t, warnings) + assert.Equal(t, `CIDR for 'skipped-overlap' (10.0.0.0/24) is fully overlapped by config (admin: 10.0.0.0/16) and will be ignored; CIDR for 'partial-overlap' (100.0.0.0/23) overlaps with config (ns-b/b1: 100.0.0.0/24)`, warnings.Message) + assert.Equal(t, 1, slicez[2].Status.SubnetLabelsConfigured) + assert.Equal(t, "", slicez[2].Status.FilterApplied) +} + +func TestSlicesEnablesWhitelist(t *testing.T) { + fmstatus.Reset() + slicesstatus.Reset(&sliceslatest.FlowCollectorSliceList{}) + b, err := defaultBuilderWithSlices(&flowslatest.SlicesConfig{ + Enable: true, + CollectionMode: flowslatest.CollectionAllowList, + NamespacesAllowList: []string{"should-be-filtered", "/should-.*/"}, + }) + assert.NoError(t, err) + cm, _, _, err := b.configMaps() + assert.NoError(t, err) + filters, subnets := getConfiguredFiltersAndSubnets(cm) + assert.Equal(t, []api.TransformFilterRule{ + { + Type: api.KeepEntryQuery, + KeepEntryQuery: `SrcK8S_Namespace="should-be-filtered" or DstK8S_Namespace="should-be-filtered"`, + }, + { + Type: api.KeepEntryQuery, + KeepEntryQuery: `SrcK8S_Namespace=~"should-.*" or DstK8S_Namespace=~"should-.*"`, + }, + { + Type: api.KeepEntryQuery, + KeepEntryQuery: `SrcK8S_Namespace="ns-a" or DstK8S_Namespace="ns-a"`, + }, + { + Type: api.KeepEntryQuery, + KeepEntryQuery: `SrcK8S_Namespace="ns-b" or DstK8S_Namespace="ns-b"`, + }, + }, filters) + assert.Equal(t, []api.NetworkTransformSubnetLabel{ + { + Name: "admin", + CIDRs: []string{"10.0.0.0/16"}, + }, + { + Name: "my-override", + CIDRs: []string{"10.10.0.0/16"}, + }, + { + Name: "my-label", + CIDRs: []string{"100.0.0.0/24"}, + }, + { + Name: "partial-overlap", + CIDRs: []string{"100.0.0.0/23"}, + }, + { + Name: "test", + CIDRs: []string{"1.2.3.4/32", "10.0.0.0/8"}, + }, + }, subnets) + // Slice 0 + ready := slicesstatus.GetReadyCondition(&slicez[0]) + assert.NotNil(t, ready) + assert.Equal(t, v1.ConditionTrue, ready.Status) + assert.Nil(t, slicesstatus.GetSubnetWarningCondition(&slicez[0])) + assert.Equal(t, 0, slicez[0].Status.SubnetLabelsConfigured) + assert.Equal(t, `SrcK8S_Namespace="ns-a" or DstK8S_Namespace="ns-a"`, slicez[0].Status.FilterApplied) + // Slice 1 + ready = slicesstatus.GetReadyCondition(&slicez[1]) + assert.NotNil(t, ready) + assert.Equal(t, v1.ConditionTrue, ready.Status) + assert.Nil(t, slicesstatus.GetSubnetWarningCondition(&slicez[1])) + assert.Equal(t, 2, slicez[1].Status.SubnetLabelsConfigured) + assert.Equal(t, `SrcK8S_Namespace="ns-b" or DstK8S_Namespace="ns-b"`, slicez[1].Status.FilterApplied) + // Slice 2 + ready = slicesstatus.GetReadyCondition(&slicez[2]) + assert.NotNil(t, ready) + assert.Equal(t, v1.ConditionTrue, ready.Status) + warnings := slicesstatus.GetSubnetWarningCondition(&slicez[2]) + assert.NotNil(t, warnings) + assert.Equal(t, 1, slicez[2].Status.SubnetLabelsConfigured) + assert.Equal(t, `(skipped, not needed)`, slicez[2].Status.FilterApplied) +} diff --git a/internal/controller/flp/slicesstatus/flowcollectorslices_status.go b/internal/controller/flp/slicesstatus/flowcollectorslices_status.go new file mode 100644 index 000000000..ae8142cec --- /dev/null +++ b/internal/controller/flp/slicesstatus/flowcollectorslices_status.go @@ -0,0 +1,126 @@ +package slicesstatus + +import ( + "context" + + sliceslatest "github.com/netobserv/network-observability-operator/api/flowcollectorslice/v1alpha1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/util/retry" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" +) + +const ( + ConditionReady = "Ready" + ConditionSubnetWarning = "SubnetWarning" +) + +var ( + mapStatuses map[types.NamespacedName]*metav1.Condition = make(map[types.NamespacedName]*metav1.Condition) + mapSubnetWarnings map[types.NamespacedName]*metav1.Condition = make(map[types.NamespacedName]*metav1.Condition) +) + +func Reset(fcs *sliceslatest.FlowCollectorSliceList) { + mapStatuses = make(map[types.NamespacedName]*metav1.Condition) + mapSubnetWarnings = make(map[types.NamespacedName]*metav1.Condition) + for i := range fcs.Items { + fcs.Items[i].Status.FilterApplied = "" + fcs.Items[i].Status.SubnetLabelsConfigured = 0 + } +} + +func SetReady(fcs *sliceslatest.FlowCollectorSlice) { + nsname := types.NamespacedName{Name: fcs.Name, Namespace: fcs.Namespace} + mapStatuses[nsname] = &metav1.Condition{ + Type: ConditionReady, + Reason: "Ready", + Message: "flowlogs-pipeline configured", + Status: metav1.ConditionTrue, + } +} + +func SetFailure(fcs *sliceslatest.FlowCollectorSlice, msg string) { + nsname := types.NamespacedName{Name: fcs.Name, Namespace: fcs.Namespace} + mapStatuses[nsname] = &metav1.Condition{ + Type: ConditionReady, + Reason: "Failure", + Message: msg, + Status: metav1.ConditionFalse, + } +} + +func AddSubnetWarning(fcs *sliceslatest.FlowCollectorSlice, msg string) { + nsname := types.NamespacedName{Name: fcs.Name, Namespace: fcs.Namespace} + if existing := mapSubnetWarnings[nsname]; existing != nil { + // Limit number of reported warnings + if len(existing.Message) < 500 { + existing.Message += "; " + msg + } + } else { + mapSubnetWarnings[nsname] = &metav1.Condition{ + Type: ConditionSubnetWarning, + Reason: "SubnetOverlap", + Message: msg, + Status: metav1.ConditionTrue, + } + } +} + +func Sync(ctx context.Context, c client.Client, fcs *sliceslatest.FlowCollectorSliceList) { + log := log.FromContext(ctx) + log.Info("Syncing FlowCollectorSlices status") + for i := range fcs.Items { + nsname := types.NamespacedName{Name: fcs.Items[i].Name, Namespace: fcs.Items[i].Namespace} + // main condition is mandatory; subnet warning condition is optional + if cond, ok := mapStatuses[nsname]; ok { + subnetCond := mapSubnetWarnings[nsname] + setStatus(ctx, c, nsname, func(s *sliceslatest.FlowCollectorSliceStatus) { + if cond != nil { + meta.SetStatusCondition(&s.Conditions, *cond) + } + if subnetCond != nil { + meta.SetStatusCondition(&s.Conditions, *subnetCond) + } else { + meta.RemoveStatusCondition(&s.Conditions, ConditionSubnetWarning) + } + s.FilterApplied = fcs.Items[i].Status.FilterApplied + s.SubnetLabelsConfigured = fcs.Items[i].Status.SubnetLabelsConfigured + }) + } + } +} + +func GetReadyCondition(slice *sliceslatest.FlowCollectorSlice) *metav1.Condition { + nsname := types.NamespacedName{Name: slice.Name, Namespace: slice.Namespace} + return mapStatuses[nsname] +} + +func GetSubnetWarningCondition(slice *sliceslatest.FlowCollectorSlice) *metav1.Condition { + nsname := types.NamespacedName{Name: slice.Name, Namespace: slice.Namespace} + return mapSubnetWarnings[nsname] +} + +func setStatus(ctx context.Context, c client.Client, nsname types.NamespacedName, applyStatus func(s *sliceslatest.FlowCollectorSliceStatus)) { + log := log.FromContext(ctx) + + err := retry.RetryOnConflict(retry.DefaultBackoff, func() error { + fcs := sliceslatest.FlowCollectorSlice{} + if err := c.Get(ctx, nsname, &fcs); err != nil { + log.WithValues("NsName", nsname).Error(err, "failed to get FlowCollectorSlices status") + if errors.IsNotFound(err) { + // ignore: when it's being deleted, there's no point trying to update its status + return nil + } + return err + } + applyStatus(&fcs.Status) + return c.Status().Update(ctx, &fcs) + }) + + if err != nil { + log.Error(err, "failed to update FlowCollectorSlices status") + } +} diff --git a/internal/pkg/manager/manager.go b/internal/pkg/manager/manager.go index 6175cf51b..abdc420d5 100644 --- a/internal/pkg/manager/manager.go +++ b/internal/pkg/manager/manager.go @@ -25,8 +25,8 @@ import ( //+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;flowmetrics;flowcollectorslices,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=flows.netobserv.io,resources=flowcollectors/status;flowmetrics/status;flowcollectorslices/status,verbs=get;update;patch //+kubebuilder:rbac:groups=flows.netobserv.io,resources=flowcollectors/finalizers,verbs=update //+kubebuilder:rbac:groups=security.openshift.io,resources=securitycontextconstraints,resourceNames=hostnetwork,verbs=use //+kubebuilder:rbac:groups=security.openshift.io,resources=securitycontextconstraints,verbs=list;create;update;watch diff --git a/internal/pkg/test/envtest.go b/internal/pkg/test/envtest.go index f343c6b98..dce0cc276 100644 --- a/internal/pkg/test/envtest.go +++ b/internal/pkg/test/envtest.go @@ -40,6 +40,7 @@ import ( _ "github.com/openshift/api/security/v1/zz_generated.crd-manifests" flowsv1beta2 "github.com/netobserv/network-observability-operator/api/flowcollector/v1beta2" + slicesv1alpha1 "github.com/netobserv/network-observability-operator/api/flowcollectorslice/v1alpha1" metricsv1alpha1 "github.com/netobserv/network-observability-operator/api/flowmetrics/v1alpha1" "github.com/netobserv/network-observability-operator/internal/pkg/helper" "github.com/netobserv/network-observability-operator/internal/pkg/manager" @@ -99,6 +100,9 @@ func PrepareEnvTest(controllers []manager.Registerer, namespaces []string, baseP err = metricsv1alpha1.AddToScheme(scheme.Scheme) Expect(err).NotTo(HaveOccurred()) + err = slicesv1alpha1.AddToScheme(scheme.Scheme) + Expect(err).NotTo(HaveOccurred()) + err = corev1.AddToScheme(scheme.Scheme) Expect(err).NotTo(HaveOccurred()) diff --git a/main.go b/main.go index 7afff61b5..2f97cba02 100644 --- a/main.go +++ b/main.go @@ -50,6 +50,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/webhook" flowsv1beta2 "github.com/netobserv/network-observability-operator/api/flowcollector/v1beta2" + slicesv1alpha1 "github.com/netobserv/network-observability-operator/api/flowcollectorslice/v1alpha1" metricsv1alpha1 "github.com/netobserv/network-observability-operator/api/flowmetrics/v1alpha1" controllers "github.com/netobserv/network-observability-operator/internal/controller" "github.com/netobserv/network-observability-operator/internal/controller/constants" @@ -74,6 +75,7 @@ func init() { utilruntime.Must(clientgoscheme.AddToScheme(scheme)) utilruntime.Must(flowsv1beta2.AddToScheme(scheme)) utilruntime.Must(metricsv1alpha1.AddToScheme(scheme)) + utilruntime.Must(slicesv1alpha1.AddToScheme(scheme)) utilruntime.Must(corev1.AddToScheme(scheme)) utilruntime.Must(ascv2.AddToScheme(scheme)) utilruntime.Must(osv1.AddToScheme(scheme))