diff --git a/Makefile b/Makefile index 1b2fe066..353597e2 100644 --- a/Makefile +++ b/Makefile @@ -181,6 +181,10 @@ install-commands: commands ## Generate plugins and add them to /usr/bin/ docs: oc-commands ## Generate asciidoc ./scripts/generate-doc.sh +.PHONY: update-config +update-config: ## Update config from operator repo + ./scripts/update-config.sh + .PHONY: release release: clean ## Generate tar.gz containing krew plugin and display krew updated index $(MAKE) KREW_PLUGIN=true kubectl-commands diff --git a/cmd/config.go b/cmd/config.go new file mode 100644 index 00000000..c5c92006 --- /dev/null +++ b/cmd/config.go @@ -0,0 +1,61 @@ +package cmd + +import ( + _ "embed" + + "gopkg.in/yaml.v3" +) + +type ColumnConfig struct { + ID string `yaml:"id" json:"id"` + Name string `yaml:"name" json:"name"` + + Group string `yaml:"group,omitempty" json:"group,omitempty"` + Field string `yaml:"field,omitempty" json:"field,omitempty"` + Fields []string `yaml:"fields,omitempty" json:"fields,omitempty"` + Calculated string `yaml:"calculated,omitempty" json:"calculated,omitempty"` + Tooltip string `yaml:"tooltip,omitempty" json:"tooltip,omitempty"` + DocURL string `yaml:"docURL,omitempty" json:"docURL,omitempty"` + Filter string `yaml:"filter,omitempty" json:"filter,omitempty"` + Default bool `yaml:"default,omitempty" json:"default,omitempty"` + Width int `yaml:"width,omitempty" json:"width,omitempty"` + Feature string `yaml:"feature" json:"feature"` +} + +type FilterConfig struct { + ID string `yaml:"id" json:"id"` + Name string `yaml:"name" json:"name"` + Component string `yaml:"component" json:"component"` + + Category string `yaml:"category,omitempty" json:"category,omitempty"` + AutoCompleteAddsQuotes bool `yaml:"autoCompleteAddsQuotes,omitempty" json:"autoCompleteAddsQuotes,omitempty"` + Hint string `yaml:"hint,omitempty" json:"hint,omitempty"` + Examples string `yaml:"examples,omitempty" json:"examples,omitempty"` + DocURL string `yaml:"docUrl,omitempty" json:"docUrl,omitempty"` + Placeholder string `yaml:"placeholder,omitempty" json:"placeholder,omitempty"` +} + +type FieldConfig struct { + Name string `yaml:"name" json:"name"` + Type string `yaml:"type" json:"type"` + Description string `yaml:"description" json:"description"` + LokiLabel bool `yaml:"lokiLabel,omitempty" json:"lokiLabel,omitempty"` + Filter string `yaml:"filter,omitempty" json:"filter,omitempty"` +} + +type Config struct { + Columns []*ColumnConfig `yaml:"columns" json:"columns"` + Fields []*FieldConfig `yaml:"fields" json:"fields"` + Filters []*FilterConfig `yaml:"filters,omitempty" json:"filters,omitempty"` +} + +var ( + //go:embed config.yaml + rawConfig []byte + cfg Config +) + +func LoadConfig() error { + err := yaml.Unmarshal(rawConfig, &cfg) + return err +} diff --git a/cmd/config.yaml b/cmd/config.yaml new file mode 100644 index 00000000..2099feaf --- /dev/null +++ b/cmd/config.yaml @@ -0,0 +1,1187 @@ +# these configurations are static and append to the console plugin configmap +# other fields such as recordTypes, quickFilters, alertNamespaces, sampling, features +# are taken from the CR +# see consoleplugin_objects.go -> configMap func +columns: + - id: StartTime + name: Start Time + tooltip: Time of the first packet observed. Unlike End Time, it is not used in queries + to select records in an interval. + field: TimeFlowStartMs + default: false + width: 15 + - id: EndTime + name: End Time + tooltip: Time of the last packet observed. This is what is used in queries to select + records in an interval. + field: TimeFlowEndMs + default: true + width: 15 + - id: RecordType + name: Event / Type + field: _RecordType + filter: type + default: true + width: 15 + - id: _HashId + name: Conversation Id + field: _HashId + filter: id + default: true + width: 15 + - id: ClusterName + name: Cluster + tooltip: The cluster ID or Name. + field: K8S_ClusterName + filter: cluster_name + default: false + width: 15 + feature: multiCluster + - id: SrcK8S_Name + group: Source + name: Name + tooltip: The source name of the related kubernetes resource. + docURL: http://kubernetes.io/docs/user-guide/identifiers#names + field: SrcK8S_Name + filter: src_name + default: true + width: 15 + - id: SrcK8S_Type + group: Source + name: Kind + tooltip: |- + The kind of the related kubernetes resource. Examples: + - Pod + - Service + - Node + field: SrcK8S_Type + filter: src_kind + default: false + width: 10 + - id: SrcK8S_OwnerName + group: Source + name: Owner + tooltip: The source owner name of the related kubernetes resource. + docURL: https://kubernetes.io/docs/concepts/overview/working-with-objects/owners-dependents/ + field: SrcK8S_OwnerName + filter: src_owner_name + default: false + width: 15 + - id: SrcK8S_OwnerType + group: Source + name: Owner Kind + tooltip: |- + The owner kind of the related kubernetes resource. Examples: + - Deployment + - StatefulSet + - DaemonSet + - Job + - CronJob + field: SrcK8S_OwnerType + filter: src_kind + default: false + width: 10 + - id: SrcK8S_Namespace + group: Source + name: Namespace + tooltip: The source namespace of the related kubernetes resource. + docURL: http://kubernetes.io/docs/user-guide/identifiers#namespaces + field: SrcK8S_Namespace + filter: src_namespace + default: true + width: 15 + - id: SrcAddr + group: Source + name: IP + tooltip: The source IP address. Can be either in IPv4 or IPv6 format. + field: SrcAddr + filter: src_address + default: false + width: 10 + - id: SrcPort + group: Source + name: Port + tooltip: The source port number. + field: SrcPort + filter: src_port + default: true + width: 10 + - id: SrcMac + group: Source + name: MAC + tooltip: The source MAC address. + field: SrcMac + filter: src_mac + default: false + width: 10 + - id: SrcK8S_HostIP + group: Source + name: Node IP + tooltip: The source node IP address. Can be either in IPv4 or IPv6 format. + field: SrcK8S_HostIP + filter: src_host_address + default: false + width: 10 + - id: SrcK8S_HostName + group: Source + name: Node Name + tooltip: The source name of the node running the workload. + docURL: https://kubernetes.io/docs/concepts/architecture/nodes/ + field: SrcK8S_HostName + filter: src_host_name + default: false + width: 15 + - id: SrcK8S_Object + group: Source + name: Kubernetes Object + calculated: getConcatenatedValue(SrcAddr,SrcPort,SrcK8S_Type,SrcK8S_Namespace,SrcK8S_Name) + default: false + width: 15 + - id: SrcK8S_OwnerObject + group: Source + name: Owner Kubernetes Object + calculated: getConcatenatedValue(SrcAddr,SrcPort,SrcK8S_OwnerType,SrcK8S_Namespace,SrcK8S_OwnerName) + default: false + width: 15 + - id: SrcAddrPort + group: Source + name: IP & Port + calculated: getConcatenatedValue(SrcAddr,SrcPort) + default: false + width: 15 + - id: SrcZone + group: Source + name: Zone + field: SrcK8S_Zone + filter: src_zone + default: false + width: 15 + feature: zones + - id: SrcSubnetLabel + group: Source + name: Subnet Label + field: SrcSubnetLabel + filter: src_subnet_label + default: false + width: 10 + feature: subnetLabels + - id: DstK8S_Name + group: Destination + name: Name + tooltip: The destination name of the related kubernetes resource. + docURL: http://kubernetes.io/docs/user-guide/identifiers#names + field: DstK8S_Name + filter: dst_name + default: true + width: 15 + - id: DstK8S_Type + group: Destination + name: Kind + tooltip: |- + The kind of the related kubernetes resource. Examples: + - Pod + - Service + - Node + field: DstK8S_Type + filter: dst_kind + default: false + width: 10 + - id: DstK8S_OwnerName + group: Destination + name: Owner + tooltip: The destination owner name of the related kubernetes resource. + docURL: https://kubernetes.io/docs/concepts/overview/working-with-objects/owners-dependents/ + field: DstK8S_OwnerName + filter: dst_owner_name + default: false + width: 15 + - id: DstK8S_OwnerType + group: Destination + name: Owner Kind + tooltip: |- + The owner kind of the related kubernetes resource. Examples: + - Deployment + - StatefulSet + - DaemonSet + - Job + - CronJob + field: DstK8S_OwnerType + filter: dst_kind + default: false + width: 10 + - id: DstK8S_Namespace + group: Destination + name: Namespace + tooltip: The destination namespace of the related kubernetes resource. + docURL: http://kubernetes.io/docs/user-guide/identifiers#namespaces + field: DstK8S_Namespace + filter: dst_namespace + default: true + width: 15 + - id: DstAddr + group: Destination + name: IP + tooltip: The destination IP address. Can be either in IPv4 or IPv6 format. + field: DstAddr + filter: dst_address + default: false + width: 10 + - id: DstPort + group: Destination + name: Port + tooltip: The destination port number. + field: DstPort + filter: dst_port + default: true + width: 10 + - id: DstMac + group: Destination + name: MAC + tooltip: The destination MAC address. + field: DstMac + filter: dst_mac + default: false + width: 10 + - id: DstK8S_HostIP + group: Destination + name: Node IP + tooltip: The destination node IP address. Can be either in IPv4 or IPv6 format. + field: DstK8S_HostIP + filter: dst_host_address + default: false + width: 10 + - id: DstK8S_HostName + group: Destination + name: Node Name + tooltip: The destination name of the node running the workload. + docURL: https://kubernetes.io/docs/concepts/architecture/nodes/ + field: DstK8S_HostName + filter: dst_host_name + default: false + width: 15 + - id: DstK8S_Object + group: Destination + name: Kubernetes Object + calculated: getConcatenatedValue(DstAddr,DstPort,DstK8S_Type,DstK8S_Namespace,DstK8S_Name) + default: false + width: 15 + - id: DstK8S_OwnerObject + group: Destination + name: Owner Kubernetes Object + calculated: getConcatenatedValue(DstAddr,DstPort,DstK8S_OwnerType,DstK8S_Namespace,DstK8S_OwnerName) + default: false + width: 15 + - id: DstAddrPort + group: Destination + name: IP & Port + calculated: getConcatenatedValue(DstAddr,DstPort) + default: false + width: 15 + - id: DstZone + group: Destination + name: Zone + field: DstK8S_Zone + filter: dst_zone + default: false + width: 15 + feature: zones + - id: DstSubnetLabel + group: Destination + name: Subnet Label + field: DstSubnetLabel + filter: dst_subnet_label + default: false + width: 10 + feature: subnetLabels + - id: K8S_Name + name: Names + calculated: getSrcOrDstValue(SrcK8S_Name,DstK8S_Name) + default: false + width: 15 + - id: K8S_Type + name: Kinds + calculated: getSrcOrDstValue(SrcK8S_Type,DstK8S_Type) + default: false + width: 10 + - id: K8S_OwnerName + name: Owners + calculated: getSrcOrDstValue(SrcK8S_OwnerName,DstK8S_OwnerName) + default: false + width: 15 + - id: K8S_OwnerType + name: Owner Kinds + calculated: getSrcOrDstValue(SrcK8S_OwnerType,DstK8S_OwnerType) + default: false + width: 10 + - id: K8S_Namespace + name: Namespaces + calculated: getSrcOrDstValue(SrcK8S_Namespace,DstK8S_Namespace) + default: false + width: 15 + - id: Addr + name: IP + calculated: getSrcOrDstValue(SrcAddr,DstAddr) + default: false + width: 10 + - id: Port + name: Ports + calculated: getSrcOrDstValue(SrcPort,DstPort) + default: false + width: 10 + - id: Mac + name: MAC + calculated: getSrcOrDstValue(SrcMac,DstMac) + default: false + width: 10 + - id: K8S_HostIP + name: Node IP + calculated: getSrcOrDstValue(SrcK8S_HostIP,DstK8S_HostIP) + default: false + width: 10 + - id: K8S_HostName + name: Node Name + calculated: getSrcOrDstValue(SrcK8S_HostName,DstK8S_HostName) + default: false + width: 15 + - id: K8S_Object + name: Kubernetes Objects + calculated: '[column.SrcK8S_Object,column.DstK8S_Object]' + default: false + width: 15 + - id: K8S_OwnerObject + name: Owner Kubernetes Objects + calculated: '[column.SrcK8S_OwnerObject,column.DstK8S_OwnerObject]' + default: false + width: 15 + - id: K8S_FlowLayer + name: Flow layer + field: K8S_FlowLayer + filter: flow_layer + default: false + width: 15 + - id: AddrPort + name: IPs & Ports + calculated: '[column.SrcAddrPort,column.DstAddrPort]' + default: false + width: 15 + - id: Proto + group: L3 Layer + name: Protocol + tooltip: The value of the protocol number in the IP packet header + field: Proto + filter: protocol + default: true + width: 10 + - id: Dscp + group: L3 Layer + name: DSCP + tooltip: The value of the Differentiated Services Code Point + field: Dscp + filter: dscp + default: true + width: 10 + - id: IcmpType + group: ICMP + name: Type + tooltip: The type of the ICMP message + field: IcmpType + fields: + - Proto + - IcmpType + filter: icmp_type + default: false + width: 10 + - id: IcmpCode + group: ICMP + name: Code + tooltip: The code of the ICMP message + field: IcmpCode + fields: + - Proto + - IcmpType + - IcmpCode + filter: icmp_code + default: false + width: 10 + - id: TCPFlags + name: TCP Flags + tooltip: Logical OR combination of unique TCP flags comprised in the flow, as per RFC-9293, with additional custom values. + field: Flags + filter: tcp_flags + default: false + width: 10 + - id: FlowDirection + name: Node Direction + tooltip: The interpreted direction of the flow observed at the Node observation point. + field: FlowDirection + filter: node_direction + default: false + width: 10 + - id: Interfaces + name: Interfaces + tooltip: The network interfaces of the Flow. + field: Interfaces + filter: interfaces + default: false + width: 10 + - id: IfDirections + name: Interface Directions + tooltip: The directions of the Flow observed at the network interface observation point. + field: IfDirections + filter: ifdirections + default: false + width: 10 + - id: FlowDirInts + name: Interfaces and Directions + tooltip: Pairs of network interface and direction of the Flow observed at the network interface observation point. + field: Interfaces + default: false + width: 15 + - id: Bytes + name: Bytes + tooltip: The total aggregated number of bytes. + field: Bytes + fields: + - Bytes + - PktDropBytes + default: true + width: 5 + - id: Packets + name: Packets + tooltip: The total aggregated number of packets. + field: Packets + fields: + - Packets + - PktDropPackets + filter: pkt_drop_cause + default: true + width: 5 + - id: FlowDuration + name: Duration + tooltip: Time elapsed between Start Time and End Time. + calculated: substract(TimeFlowEndMs,TimeFlowStartMs) + default: false + width: 5 + - id: CollectionTime + name: Collection Time + tooltip: Reception time of the record by the collector. + calculated: multiply(TimeReceived,1000), + field: TimeReceived + default: false + width: 15 + - id: CollectionLatency + name: Collection Latency + tooltip: Time elapsed between End Time and Collection Time. + calculated: substract(column.CollectionTime,TimeFlowEndMs) + default: false + width: 5 + - id: PktDropBytes + name: Dropped Bytes + tooltip: The total aggregated number of bytes dropped. + field: PktDropBytes + default: false + width: 5 + feature: pktDrop + - id: PktDropPackets + name: Dropped Packets + tooltip: The total aggregated number of packets dropped. + field: PktDropPackets + default: false + width: 5 + feature: pktDrop + - id: PktDropLatestState + name: Drop State + tooltip: TCP state on last dropped packet. + field: PktDropLatestState + default: false + width: 10 + feature: pktDrop + - id: PktDropLatestDropCause + name: Drop Cause + tooltip: TCP state on last dropped packet. + field: PktDropLatestDropCause + default: false + width: 10 + feature: pktDrop + - id: PktDropLatestFlags + name: Drop Flags + tooltip: TCP flags on last dropped packet. + field: PktDropLatestFlags + default: false + width: 10 + feature: pktDrop + - id: DNSId + group: DNS + name: DNS Id + tooltip: DNS request identifier. + field: DnsId + filter: dns_id + default: false + width: 5 + feature: dnsTracking + - id: DNSLatency + group: DNS + name: DNS Latency + tooltip: Time elapsed between DNS request and response. + field: DnsLatencyMs + filter: dns_latency + default: true + width: 5 + feature: dnsTracking + - id: DNSResponseCode + group: DNS + name: DNS Response Code + tooltip: DNS RCODE name from response header. + field: DnsFlagsResponseCode + filter: dns_flag_response_code + default: true + width: 5 + feature: dnsTracking + - id: DNSErrNo + group: DNS + name: DNS Error + tooltip: DNS error number returned by bpf_skb_load_bytes function. + field: DnsErrno + filter: dns_errno + default: false + width: 5 + feature: dnsTracking + - id: TimeFlowRttMs + name: Flow RTT + tooltip: TCP Smoothed Round Trip Time (SRTT) + field: TimeFlowRttNs + filter: time_flow_rtt + default: true + width: 5 + feature: flowRTT + - id: NetworkEvents + name: Network Events + tooltip: Network events flow monitor + field: NetworkEvents + filter: network_events + default: true + width: 15 + feature: networkEvents +filters: + - id: cluster_name + name: Cluster + component: autocomplete + hint: Specify a cluster ID or name. + - id: src_namespace + name: Namespace + component: autocomplete + autoCompleteAddsQuotes: true + category: source + placeholder: 'E.g: netobserv' + hint: Specify a single kubernetes name. + examples: |- + Specify a single kubernetes name following these rules: + - Containing any alphanumeric, hyphen, underscrore or dot character + - Partial text like cluster, cluster-image, image-registry + - Exact match using quotes like "cluster-image-registry" + - Case sensitive match using quotes like "Deployment" + - Starting text like cluster, "cluster-*" + - Ending text like "*-registry" + - Pattern like "cluster-*-registry", "c*-*-r*y", -i*e- + - id: dst_namespace + name: Namespace + component: autocomplete + autoCompleteAddsQuotes: true + category: destination + placeholder: 'E.g: netobserv' + hint: Specify a single kubernetes name. + examples: |- + Specify a single kubernetes name following these rules: + - Containing any alphanumeric, hyphen, underscrore or dot character + - Partial text like cluster, cluster-image, image-registry + - Exact match using quotes like "cluster-image-registry" + - Case sensitive match using quotes like "Deployment" + - Starting text like cluster, "cluster-*" + - Ending text like "*-registry" + - Pattern like "cluster-*-registry", "c*-*-r*y", -i*e- + - id: src_name + name: Name + component: text + category: source + placeholder: 'E.g: my-pod' + hint: Specify a single kubernetes name. + examples: |- + Specify a single kubernetes name following these rules: + - Containing any alphanumeric, hyphen, underscrore or dot character + - Partial text like cluster, cluster-image, image-registry + - Exact match using quotes like "cluster-image-registry" + - Case sensitive match using quotes like "Deployment" + - Starting text like cluster, "cluster-*" + - Ending text like "*-registry" + - Pattern like "cluster-*-registry", "c*-*-r*y", -i*e- + - id: dst_name + name: Name + component: text + category: destination + placeholder: 'E.g: my-pod' + hint: Specify a single kubernetes name. + examples: |- + Specify a single kubernetes name following these rules: + - Containing any alphanumeric, hyphen, underscrore or dot character + - Partial text like cluster, cluster-image, image-registry + - Exact match using quotes like "cluster-image-registry" + - Case sensitive match using quotes like "Deployment" + - Starting text like cluster, "cluster-*" + - Ending text like "*-registry" + - Pattern like "cluster-*-registry", "c*-*-r*y", -i*e- + - id: src_kind + name: Kind + component: autocomplete + autoCompleteAddsQuotes: true + category: source + placeholder: 'E.g: Pod, Service' + - id: dst_kind + name: Kind + component: autocomplete + autoCompleteAddsQuotes: true + category: destination + placeholder: 'E.g: Pod, Service' + - id: src_owner_name + name: Owner Name + component: text + category: source + placeholder: 'E.g: my-deployment' + hint: Specify a single kubernetes name. + examples: |- + Specify a single kubernetes name following these rules: + - Containing any alphanumeric, hyphen, underscrore or dot character + - Partial text like cluster, cluster-image, image-registry + - Exact match using quotes like "cluster-image-registry" + - Case sensitive match using quotes like "Deployment" + - Starting text like cluster, "cluster-*" + - Ending text like "*-registry" + - Pattern like "cluster-*-registry", "c*-*-r*y", -i*e- + - id: dst_owner_name + name: Owner Name + component: text + category: destination + placeholder: 'E.g: my-deployment' + hint: Specify a single kubernetes name. + examples: |- + Specify a single kubernetes name following these rules: + - Containing any alphanumeric, hyphen, underscrore or dot character + - Partial text like cluster, cluster-image, image-registry + - Exact match using quotes like "cluster-image-registry" + - Case sensitive match using quotes like "Deployment" + - Starting text like cluster, "cluster-*" + - Ending text like "*-registry" + - Pattern like "cluster-*-registry", "c*-*-r*y", -i*e- + - id: src_zone + name: Zone + component: autocomplete + category: source + hint: Specify a single zone. + - id: dst_zone + name: Zone + component: autocomplete + category: destination + hint: Specify a single zone. + - id: src_subnet_label + name: Subnet Label + component: autocomplete + category: source + hint: Specify a subnet label, or an empty string to get unmatched sources. + - id: dst_subnet_label + name: Subnet Label + component: autocomplete + category: destination + hint: Specify a subnet label, or an empty string to get unmatched destinations. + - id: src_resource + name: Resource + component: autocomplete + category: source + placeholder: 'E.g: Pod.default.my-pod' + hint: Specify an existing resource from its kind, namespace and name. + examples: |- + Specify a kind, namespace and name from existing: + - Select kind first from suggestions + - Then Select namespace from suggestions + - Finally select name from suggestions + You can also directly specify a kind, namespace and name like pod.openshift.apiserver + - id: dst_resource + name: Resource + component: autocomplete + category: destination + placeholder: 'E.g: Pod.default.my-pod' + hint: Specify an existing resource from its kind, namespace and name. + examples: |- + Specify a kind, namespace and name from existing: + - Select kind first from suggestions + - Then Select namespace from suggestions + - Finally select name from suggestions + You can also directly specify a kind, namespace and name like pod.openshift.apiserver + - id: src_address + name: IP + component: text + category: source + hint: Specify a single IP or range. + placeholder: 'E.g: 192.0.2.0' + examples: |- + Specify IP following one of these rules: + - A single IPv4 or IPv6 address like 192.0.2.0, ::1 + - An IP address range like 192.168.0.1-192.189.10.12, 2001:db8::1-2001:db8::8 + - A CIDR specification like 192.51.100.0/24, 2001:db8::/32 + - id: dst_address + name: IP + component: text + category: destination + hint: Specify a single IP or range. + placeholder: 'E.g: 192.0.2.0' + examples: |- + Specify IP following one of these rules: + - A single IPv4 or IPv6 address like 192.0.2.0, ::1 + - An IP address range like 192.168.0.1-192.189.10.12, 2001:db8::1-2001:db8::8 + - A CIDR specification like 192.51.100.0/24, 2001:db8::/32 + - id: src_port + name: Port + component: autocomplete + category: source + hint: Specify a single port number or name. + placeholder: 'E.g: 80' + examples: |- + Specify a single port following one of these rules: + - A port number like 80, 21 + - A IANA name like HTTP, FTP + docUrl: https://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml + - id: dst_port + name: Port + component: autocomplete + category: destination + hint: Specify a single port number or name. + placeholder: 'E.g: 80' + examples: |- + Specify a single port following one of these rules: + - A port number like 80, 21 + - A IANA name like HTTP, FTP + docUrl: https://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml + - id: src_mac + name: MAC + component: text + category: source + placeholder: 'E.g: 42:01:0A:00:00:01' + hint: Specify a single MAC address. + - id: dst_mac + name: MAC + component: text + category: destination + placeholder: 'E.g: 42:01:0A:00:00:01' + hint: Specify a single MAC address. + - id: src_host_address + name: Node IP + component: text + category: source + placeholder: 'E.g: 10.0.0.1' + hint: Specify a single IP or range. + examples: |- + Specify IP following one of these rules: + - A single IPv4 or IPv6 address like 192.0.2.0, ::1 + - An IP address range like 192.168.0.1-192.189.10.12, 2001:db8::1-2001:db8::8 + - A CIDR specification like 192.51.100.0/24, 2001:db8::/32 + - id: dst_host_address + name: Node IP + component: text + category: destination + placeholder: 'E.g: 10.0.0.1' + hint: Specify a single IP or range. + examples: |- + Specify IP following one of these rules: + - A single IPv4 or IPv6 address like 192.0.2.0, ::1 + - An IP address range like 192.168.0.1-192.189.10.12, 2001:db8::1-2001:db8::8 + - A CIDR specification like 192.51.100.0/24, 2001:db8::/32 + - id: src_host_name + name: Node Name + component: text + category: source + placeholder: 'E.g: my-node' + hint: Specify a single kubernetes name. + examples: |- + Specify a single kubernetes name following these rules: + - Containing any alphanumeric, hyphen, underscrore or dot character + - Partial text like cluster, cluster-image, image-registry + - Exact match using quotes like "cluster-image-registry" + - Case sensitive match using quotes like "Deployment" + - Starting text like cluster, "cluster-*" + - Ending text like "*-registry" + - Pattern like "cluster-*-registry", "c*-*-r*y", -i*e- + - id: dst_host_name + name: Node Name + component: text + category: destination + placeholder: 'E.g: my-node' + hint: Specify a single kubernetes name. + examples: |- + Specify a single kubernetes name following these rules: + - Containing any alphanumeric, hyphen, underscrore or dot character + - Partial text like cluster, cluster-image, image-registry + - Exact match using quotes like "cluster-image-registry" + - Case sensitive match using quotes like "Deployment" + - Starting text like cluster, "cluster-*" + - Ending text like "*-registry" + - Pattern like "cluster-*-registry", "c*-*-r*y", -i*e- + - id: protocol + name: Protocol + component: autocomplete + placeholder: 'E.g: TCP, UDP' + hint: Specify a single protocol number or name. + examples: |- + Specify a single protocol following one of these rules: + - A protocol number like 6, 17 + - A IANA name like TCP, UDP + docUrl: https://www.iana.org/assignments/protocol-numbers/protocol-numbers.xhtml + - id: dscp + name: DSCP + component: autocomplete + hint: Specify a Differentiated Services Code Point number or name. + examples: |- + Specify a Differentiated Services Code Point following one of these rules: + - A DSCP number like 8, 10 + - A service class name like Low-Priority Data, High-Throughput Data + - id: icmp_type + name: ICMP type + component: number + hint: Specify an ICMP type value as integer number. + - id: icmp_code + name: ICMP code + component: number + hint: Specify an ICMP code value as integer number. + - id: tcp_flags + name: TCP flags + component: autocomplete + hint: Specify a TCP flags value as integer number. + examples: |- + Logical OR combination of unique TCP flags comprised in the flow, as per RFC-9293, with additional custom flags + users can specify either numeric value or string representation of the flags as follows : + - FIN or 1, + - SYN or 2, + - RST or 4, + - PSH or 8, + - ACK or 16, + - URG or 32, + - ECE or 64, + - CWR or 128, + - SYN_ACK or 256, + - FIN_ACK or 512, + - RST_ACK or 1024, + - id: node_direction + name: Node Direction + component: autocomplete + placeholder: 'E.g: Ingress, Egress, Inner' + hint: Specify the interpreted direction of the Flow observed at the Node observation point. + - id: flow_layer + name: Flow layer + component: text + placeholder: 'Either infra or app' + hint: Specify the layer of Flow. + - id: interfaces + name: Network interfaces + component: text + placeholder: 'E.g: br-ex, ovn-k8s-mp0' + hint: Specify a network interface. + - id: ifdirections + name: Interface Directions + component: autocomplete + placeholder: 'E.g: Ingress, Egress' + hint: Specify the direction of the Flow observed at the network interface observation point. + - id: id + name: Conversation Id + component: text + hint: Specify a single conversation hash Id. + - id: pkt_drop_state + name: Packet drop TCP state + component: autocomplete + placeholder: 'E.g: ESTABLISHED, SYN_SENT, SYN_RECV' + hint: Specify a single TCP state. + examples: |- + Specify a single TCP state name like: + - A _LINUX_TCP_STATES_H number like 1, 2, 3 + - A _LINUX_TCP_STATES_H TCP name like ESTABLISHED, SYN_SENT, SYN_RECV + docUrl: https://github.com/torvalds/linux/blob/master/include/net/tcp_states.h + - id: pkt_drop_cause + name: Packet drop latest cause + component: autocomplete + placeholder: 'E.g: NO_SOCKET, PKT_TOO_SMALL' + hint: Specify a single packet drop cause. + examples: |- + Specify a single packet drop cause like: + - A _LINUX_DROPREASON_CORE_H number like 2, 3, 4 + - A _LINUX_DROPREASON_CORE_H SKB_DROP_REASON name like NOT_SPECIFIED, NO_SOCKET, PKT_TOO_SMALL + docUrl: https://github.com/torvalds/linux/blob/master/include/net/dropreason-core.h + - id: dns_id + name: DNS Id + component: number + hint: Specify a single DNS Id. + - id: dns_latency + name: DNS Latency + component: number + hint: Specify a DNS Latency in miliseconds. + - id: dns_flag_response_code + name: DNS Response Code + component: autocomplete + hint: Specify a single DNS RCODE name. + placeholder: 'E.g: NoError, NXDomain, NotAuth' + examples: |- + Specify a single DNS RCODE name like: + - A IANA RCODE number like 0, 3, 9 + - A IANA RCODE name like NoError, NXDomain, NotAuth + docUrl: https://www.iana.org/assignments/dns-parameters/dns-parameters.xhtml#dns-parameters-6 + - id: dns_errno + name: DNS Error + component: autocomplete + hint: Specify a single DNS error number. + - id: time_flow_rtt + name: Flow RTT + component: number + hint: Specify a TCP smoothed Round Trip Time in nanoseconds. + - id: network_events + name: Network events flow monitoring + component: text + hint: Specify a single network event. +# Fields definition, used to generate documentation +# The "cardinalityWarn" property relates to how the field is suitable for usage as a metric label wrt cardinality; it may have 3 values: fine, careful, avoid +fields: + - name: TimeFlowStartMs + type: number + description: Start timestamp of this flow, in milliseconds + cardinalityWarn: avoid + - name: TimeFlowEndMs + type: number + description: End timestamp of this flow, in milliseconds + cardinalityWarn: avoid + - name: TimeReceived + type: number + description: Timestamp when this flow was received and processed by the flow collector, in seconds + cardinalityWarn: avoid + - name: SrcK8S_Name + type: string + description: Name of the source Kubernetes object, such as Pod name, Service name or Node name. + cardinalityWarn: careful + - name: SrcK8S_Type + type: string + description: Kind of the source Kubernetes object, such as Pod, Service or Node. + lokiLabel: true + cardinalityWarn: fine + - name: SrcK8S_OwnerName + type: string + description: Name of the source owner, such as Deployment name, StatefulSet name, etc. + lokiLabel: true + cardinalityWarn: fine + - name: SrcK8S_OwnerType + type: string + description: Kind of the source owner, such as Deployment, StatefulSet, etc. + cardinalityWarn: fine + - name: SrcK8S_Namespace + type: string + description: Source namespace + lokiLabel: true + cardinalityWarn: fine + - name: SrcAddr + type: string + description: Source IP address (ipv4 or ipv6) + cardinalityWarn: avoid + - name: SrcPort + type: number + description: Source port + cardinalityWarn: careful + - name: SrcMac + type: string + description: Source MAC address + cardinalityWarn: avoid + - name: SrcK8S_HostIP + type: string + description: Source node IP + cardinalityWarn: fine + - name: SrcK8S_HostName + type: string + description: Source node name + cardinalityWarn: fine + - name: SrcK8S_Zone + type: string + description: Source availability zone + lokiLabel: true + cardinalityWarn: fine + - name: SrcSubnetLabel + type: string + description: Source subnet label + cardinalityWarn: fine + - name: DstK8S_Name + type: string + description: Name of the destination Kubernetes object, such as Pod name, Service name or Node name. + cardinalityWarn: careful + - name: DstK8S_Type + type: string + description: Kind of the destination Kubernetes object, such as Pod, Service or Node. + lokiLabel: true + cardinalityWarn: fine + - name: DstK8S_OwnerName + type: string + description: Name of the destination owner, such as Deployment name, StatefulSet name, etc. + lokiLabel: true + cardinalityWarn: fine + - name: DstK8S_OwnerType + type: string + description: Kind of the destination owner, such as Deployment, StatefulSet, etc. + cardinalityWarn: fine + - name: DstK8S_Namespace + type: string + description: Destination namespace + lokiLabel: true + cardinalityWarn: fine + - name: DstAddr + type: string + description: Destination IP address (ipv4 or ipv6) + cardinalityWarn: avoid + - name: DstPort + type: number + description: Destination port + cardinalityWarn: careful + - name: DstMac + type: string + description: Destination MAC address + cardinalityWarn: avoid + - name: DstK8S_HostIP + type: string + description: Destination node IP + cardinalityWarn: fine + - name: DstK8S_HostName + type: string + description: Destination node name + cardinalityWarn: fine + - name: DstK8S_Zone + type: string + description: Destination availability zone + lokiLabel: true + cardinalityWarn: fine + - name: DstSubnetLabel + type: string + description: Destination subnet label + cardinalityWarn: fine + - name: K8S_FlowLayer + type: string + description: "Flow layer: 'app' or 'infra'" + cardinalityWarn: fine + - name: Proto + type: number + description: L4 protocol + cardinalityWarn: fine + - name: Dscp + type: number + description: Differentiated Services Code Point (DSCP) value + cardinalityWarn: fine + - name: IcmpType + type: number + description: ICMP type + cardinalityWarn: fine + - name: IcmpCode + type: number + description: ICMP code + cardinalityWarn: fine + - name: Duplicate + type: boolean + description: Indicates if this flow was also captured from another interface on the same host + lokiLabel: true + cardinalityWarn: fine + - name: FlowDirection + type: number + description: | + Flow interpreted direction from the node observation point. Can be one of: + + - 0: Ingress (incoming traffic, from the node observation point) + + - 1: Egress (outgoing traffic, from the node observation point) + + - 2: Inner (with the same source and destination node) + lokiLabel: true + cardinalityWarn: fine + - name: IfDirections + type: number + description: | + Flow directions from the network interface observation point. Can be one of: + + - 0: Ingress (interface incoming traffic) + + - 1: Egress (interface outgoing traffic) + cardinalityWarn: fine + - name: Interfaces + type: string + description: Network interfaces + cardinalityWarn: careful + - name: Flags + type: number + description: | + Logical OR combination of unique TCP flags comprised in the flow, as per RFC-9293, with additional custom flags to represent the following per-packet combinations: + + - SYN+ACK (0x100) + + - FIN+ACK (0x200) + + - RST+ACK (0x400) + cardinalityWarn: fine + - name: Bytes + type: number + description: Number of bytes + cardinalityWarn: avoid + - name: Packets + type: number + description: Number of packets + cardinalityWarn: avoid + - name: PktDropBytes + type: number + description: Number of bytes dropped by the kernel + cardinalityWarn: avoid + - name: PktDropPackets + type: number + description: Number of packets dropped by the kernel + cardinalityWarn: avoid + - name: PktDropLatestState + type: string + description: TCP state on last dropped packet + filter: pkt_drop_state # couldn't guess from config + cardinalityWarn: fine + - name: PktDropLatestDropCause + type: string + description: Latest drop cause + filter: pkt_drop_cause # couldn't guess from config + cardinalityWarn: fine + - name: PktDropLatestFlags + type: number + description: TCP flags on last dropped packet + cardinalityWarn: fine + - name: DnsId + type: number + description: DNS record id + cardinalityWarn: avoid + - name: DnsLatencyMs + type: number + description: Time between a DNS request and response, in milliseconds + cardinalityWarn: avoid + - name: DnsFlags + type: number + description: DNS flags for DNS record + cardinalityWarn: fine + - name: DnsFlagsResponseCode + type: string + description: Parsed DNS header RCODEs name + cardinalityWarn: fine + - name: DnsErrno + type: number + description: Error number returned from DNS tracker ebpf hook function + cardinalityWarn: fine + - name: TimeFlowRttNs + type: number + description: TCP Smoothed Round Trip Time (SRTT), in nanoseconds + cardinalityWarn: avoid + - name: NetworkEvents + type: string + description: Network events flow monitoring + cardinalityWarn: avoid + - name: K8S_ClusterName + type: string + description: Cluster name or identifier + lokiLabel: true + cardinalityWarn: fine + - name: _RecordType + type: string + description: "Type of record: 'flowLog' for regular flow logs, or 'newConnection', 'heartbeat', 'endConnection' for conversation tracking" + lokiLabel: true + cardinalityWarn: fine + - name: _HashId + type: string + description: In conversation tracking, the conversation identifier + cardinalityWarn: avoid diff --git a/cmd/flow_capture.go b/cmd/flow_capture.go index 68179bc2..a2983037 100644 --- a/cmd/flow_capture.go +++ b/cmd/flow_capture.go @@ -37,18 +37,24 @@ var ( rawDisplay = "Raw" standardDisplay = "Standard" - pktDropDisplay = "PktDrop" - dnsDisplay = "DNS" - rttDisplay = "RTT" - networkEventsDisplay = "NetworkEvents" - display = []string{pktDropDisplay, dnsDisplay, rttDisplay, networkEventsDisplay} - - noEnrichment = "None" - zoneEnrichment = "Zone" - hostEnrichment = "Host" - ownerEnrichment = "Owner" - resourceEnrichment = "Resource" - enrichement = []string{resourceEnrichment} + exclusiveDisplays = []string{rawDisplay, standardDisplay} + pktDropDisplay = "pktDrop" + dnsDisplay = "dnsTracking" + rttDisplay = "flowRTT" + networkEventsDisplay = "networkEvents" + displays = []string{pktDropDisplay, dnsDisplay, rttDisplay, networkEventsDisplay} + display = []string{standardDisplay} + + noEnrichment = "None" + exclusiveEnrichments = []string{noEnrichment} + multiClusterEnrichment = "multiCluster" + zoneEnrichment = "zones" + hostEnrichment = "hosts" + ownerEnrichment = "owners" + resourceEnrichment = "resources" + subnetLabelEnrichment = "subnetLabels" + enrichments = []string{multiClusterEnrichment, zoneEnrichment, hostEnrichment, ownerEnrichment, resourceEnrichment, subnetLabelEnrichment} + enrichment = []string{resourceEnrichment} ) func runFlowCapture(_ *cobra.Command, _ []string) { @@ -218,27 +224,6 @@ func manageFlowsDisplay(genericMap config.GenericMap) { mutex.Unlock() } -func toSize(fieldName string) int { - switch fieldName { - case "SrcName", "DstName", "SrcOwnerName", "DstOwnerName", "SrcHostName", "DstHostName": - return 45 - case "DropCause", "SrcAddr", "DstAddr": - return 40 - case "DropState": - return 20 - case "Time", "Interfaces", "SrcZone", "DstZone", "NetworkEvents": - return 16 - case "DropBytes", "DropPackets", "SrcOwnerType", "DstOwnerType": - return 12 - case "Dir": - return 10 - case "Dscp", "SrcType", "DstType": - return 8 - default: - return 6 - } -} - func updateTable() { // don't refresh terminal too often to avoid blinking now := currentTime() @@ -256,8 +241,8 @@ func updateTable() { fmt.Printf("Filters: %s\n", filter) } fmt.Printf("Showing last: %d Use Up / Down keyboard arrows to increase / decrease limit\n", flowsToShow) - fmt.Printf("Display: %s Use Left / Right keyboard arrows to cycle views\n", strings.Join(display, ",")) - fmt.Printf("Enrichment: %s Use Page Up / Page Down keyboard keys to cycle enrichment scopes\n", strings.Join(enrichement, ",")) + fmt.Printf("Display: %s Use Left / Right keyboard arrows to cycle views\n", toShortTitleStr(display)) + fmt.Printf("Enrichment: %s Use Page Up / Page Down keyboard keys to cycle enrichment scopes\n", toShortTitleStr(enrichment)) } if slices.Contains(display, rawDisplay) { @@ -272,45 +257,29 @@ func updateTable() { columnFmt := color.New(color.FgHiYellow).SprintfFunc() // main field, always show the end time - cols := []string{ - "Time", + colIDs := []string{ + "EndTime", } // enrichment fields - if !slices.Contains(enrichement, noEnrichment) { - if slices.Contains(enrichement, zoneEnrichment) { - cols = append(cols, - "SrcZone", - "DstZone", - ) - } - - if slices.Contains(enrichement, hostEnrichment) { - cols = append(cols, - "SrcHostName", - "DstHostName", - ) - } - - if slices.Contains(enrichement, ownerEnrichment) { - cols = append(cols, - "SrcOwnerName", - "SrcOwnerType", - "DstOwnerName", - "DstOwnerType", - ) - } - - if slices.Contains(enrichement, resourceEnrichment) { - cols = append(cols, - "SrcName", - "SrcType", - "DstName", - "DstType", - ) + if !slices.Contains(enrichment, noEnrichment) { + for _, enr := range enrichment { + var fieldMatch string + if enr == resourceEnrichment { + fieldMatch = "K8S_Name" + } else { + fieldMatch = fmt.Sprintf("K8S_%s", enr) + } + + for _, col := range cfg.Columns { + if strings.Contains(col.Field, fieldMatch) { + colIDs = append(colIDs, col.ID) + } + } } } else { - cols = append(cols, + // TODO: add a new flag in the config to identify these as default non enriched fields + colIDs = append(colIDs, "SrcAddr", "SrcPort", "DstAddr", @@ -320,37 +289,15 @@ func updateTable() { // standard / feature fields if !slices.Contains(display, standardDisplay) { - if slices.Contains(display, pktDropDisplay) { - cols = append(cols, - "DropBytes", - "DropPackets", - "DropState", - "DropCause", - ) - } - - if slices.Contains(display, dnsDisplay) { - cols = append(cols, - "DnsId", - "DnsLatency", - "DnsRCode", - "DnsErrno", - ) - } - - if slices.Contains(display, rttDisplay) { - cols = append(cols, - "RTT", - ) - } - if slices.Contains(display, networkEventsDisplay) { - cols = append(cols, - "NetworkEvents", - ) + for _, col := range cfg.Columns { + if slices.Contains(display, col.Feature) { + colIDs = append(colIDs, col.ID) + } } } else { - cols = append(cols, - "Dir", + // TODO: add a new flag in the config to identify these as default feature fields + colIDs = append(colIDs, + "FlowDirection", "Interfaces", "Proto", "Dscp", @@ -359,9 +306,9 @@ func updateTable() { ) } - colInterfaces := make([]interface{}, len(cols)) - for i, c := range cols { - colInterfaces[i] = c + colInterfaces := make([]interface{}, len(colIDs)) + for i, id := range colIDs { + colInterfaces[i] = ToTableColName(id) } tbl := table.New(colInterfaces...) if outputBuffer != nil { @@ -371,13 +318,13 @@ func updateTable() { // append most recent rows for _, flow := range lastFlows { - tbl.AddRow(ToTableRow(flow, cols)...) + tbl.AddRow(ToTableRow(flow, colIDs)...) } // inserting empty row ensure minimum column sizes emptyRow := []interface{}{} - for _, col := range cols { - emptyRow = append(emptyRow, strings.Repeat("-", toSize(col))) + for _, id := range colIDs { + emptyRow = append(emptyRow, strings.Repeat("-", ToTableColWidth(id))) } tbl.AddRow(emptyRow...) @@ -398,6 +345,30 @@ func updateTable() { } } +func cycleOption(selection []string, exclusiveOptions []string, options []string, incr int) []string { + allOptions := slices.Concat(exclusiveOptions, options) + + var index int + if len(selection) == 1 { + index = slices.Index(allOptions, selection[0]) + if index+incr < 0 || index+incr > len(allOptions)-1 { + index = -1 + } else { + index += incr + } + } else if incr < 0 { + index = len(allOptions) - 1 + } + + if index != -1 { + selection = []string{allOptions[index]} + } else { + selection = slices.Clone(options) + } + + return selection +} + func scanner() { if err := keyboard.Open(); err != nil { keyboardError = fmt.Sprintf("Keyboard not supported %v", err) @@ -424,69 +395,13 @@ func scanner() { flowsToShow = flowsToShow - 1 } } else if key == keyboard.KeyArrowRight { - if slices.Contains(display, pktDropDisplay) && slices.Contains(display, dnsDisplay) && - slices.Contains(display, rttDisplay) && slices.Contains(display, networkEventsDisplay) { - display = []string{rawDisplay} - } else if slices.Contains(display, rawDisplay) { - display = []string{standardDisplay} - } else if slices.Contains(display, standardDisplay) { - display = []string{pktDropDisplay} - } else if slices.Contains(display, pktDropDisplay) { - display = []string{dnsDisplay} - } else if slices.Contains(display, dnsDisplay) { - display = []string{networkEventsDisplay} - } else if slices.Contains(display, networkEventsDisplay) { - display = []string{rttDisplay} - } else if slices.Contains(display, rttDisplay) { - display = []string{rawDisplay} - } else { - display = []string{pktDropDisplay, dnsDisplay, rttDisplay, networkEventsDisplay} - } + display = cycleOption(display, exclusiveDisplays, displays, 1) } else if key == keyboard.KeyArrowLeft { - if slices.Contains(display, pktDropDisplay) && slices.Contains(display, dnsDisplay) && slices.Contains(display, rttDisplay) && - slices.Contains(display, networkEventsDisplay) { - display = []string{rttDisplay} - } else if slices.Contains(display, rttDisplay) { - display = []string{dnsDisplay} - } else if slices.Contains(display, dnsDisplay) { - display = []string{pktDropDisplay} - } else if slices.Contains(display, pktDropDisplay) { - display = []string{networkEventsDisplay} - } else if slices.Contains(display, networkEventsDisplay) { - display = []string{standardDisplay} - } else if slices.Contains(display, standardDisplay) { - display = []string{rawDisplay} - } else { - display = []string{pktDropDisplay, dnsDisplay, rttDisplay, networkEventsDisplay} - } + display = cycleOption(display, exclusiveDisplays, displays, -1) } else if key == keyboard.KeyPgup { - if slices.Contains(enrichement, zoneEnrichment) && slices.Contains(enrichement, hostEnrichment) && slices.Contains(enrichement, ownerEnrichment) { - enrichement = []string{noEnrichment} - } else if slices.Contains(enrichement, noEnrichment) { - enrichement = []string{resourceEnrichment} - } else if slices.Contains(enrichement, resourceEnrichment) { - enrichement = []string{ownerEnrichment} - } else if slices.Contains(enrichement, ownerEnrichment) { - enrichement = []string{hostEnrichment} - } else if slices.Contains(enrichement, hostEnrichment) { - enrichement = []string{zoneEnrichment} - } else { - enrichement = []string{zoneEnrichment, hostEnrichment, ownerEnrichment, resourceEnrichment} - } + enrichment = cycleOption(enrichment, exclusiveEnrichments, enrichments, 1) } else if key == keyboard.KeyPgdn { - if slices.Contains(enrichement, zoneEnrichment) && slices.Contains(enrichement, hostEnrichment) && slices.Contains(enrichement, ownerEnrichment) { - enrichement = []string{zoneEnrichment} - } else if slices.Contains(enrichement, zoneEnrichment) { - enrichement = []string{hostEnrichment} - } else if slices.Contains(enrichement, hostEnrichment) { - enrichement = []string{ownerEnrichment} - } else if slices.Contains(enrichement, ownerEnrichment) { - enrichement = []string{resourceEnrichment} - } else if slices.Contains(enrichement, resourceEnrichment) { - enrichement = []string{noEnrichment} - } else { - enrichement = []string{zoneEnrichment, hostEnrichment, ownerEnrichment, resourceEnrichment} - } + enrichment = cycleOption(enrichment, exclusiveEnrichments, enrichments, -1) } else if key == keyboard.KeyBackspace || key == keyboard.KeyBackspace2 { if len(regexes) > 0 { lastIndex := len(regexes) - 1 diff --git a/cmd/flow_capture_test.go b/cmd/flow_capture_test.go index 4a2e64a4..2e81991a 100644 --- a/cmd/flow_capture_test.go +++ b/cmd/flow_capture_test.go @@ -11,7 +11,7 @@ import ( ) func TestFlowTableRefreshDelay(t *testing.T) { - setup() + setup(t) // set output buffer to draw table buf := bytes.Buffer{} @@ -24,7 +24,7 @@ func TestFlowTableRefreshDelay(t *testing.T) { } func TestFlowTableDefaultDisplay(t *testing.T) { - setup() + setup(t) // set output buffer to draw table buf := bytes.Buffer{} @@ -39,14 +39,14 @@ func TestFlowTableDefaultDisplay(t *testing.T) { rows := strings.Split(buf.String(), "\n") assert.Equal(t, 4, len(rows)) - assert.Equal(t, `Time SrcName SrcType DstName DstType DropBytes DropPackets DropState DropCause DnsId DnsLatency DnsRCode DnsErrno RTT NetworkEvents `, rows[0]) - assert.Equal(t, `17:25:28.703000 src-pod Pod dst-pod Pod 32B 1 TCP_INVALID_STATE SKB_DROP_REASON_TCP_INVALID_SEQUENCE 31319 1ms NoError 0 10µs hello `, rows[1]) - assert.Equal(t, `---------------- --------------------------------------------- -------- --------------------------------------------- -------- ------------ ------------ -------------------- ---------------------------------------- ------ ------ ------ ------ ------ ---------------- `, rows[2]) + assert.Equal(t, `End Time Src Name Src Namespace Dst Name Dst Namespace Node Dir Interfaces L3 Layer Protocol L3 Layer DSCP Bytes Packets `, rows[0]) + assert.Equal(t, `17:25:28.703000 src-pod first-namespace dst-pod second-namespace Ingress f18b970c2ce8fdd TCP Standard 456B 5 `, rows[1]) + assert.Equal(t, `--------------- --------------- --------------- --------------- --------------- ---------- ---------- ---------- ---------- ----- ----- `, rows[2]) assert.Empty(t, rows[3]) } func TestFlowTableMultipleFlows(t *testing.T) { - setup() + setup(t) // set output buffer to draw table buf := bytes.Buffer{} @@ -54,7 +54,7 @@ func TestFlowTableMultipleFlows(t *testing.T) { // set display to standard without enrichment display = []string{standardDisplay} - enrichement = []string{noEnrichment} + enrichment = []string{noEnrichment} // set time and bytes per flow flowTime := 1704063600000 @@ -86,22 +86,22 @@ func TestFlowTableMultipleFlows(t *testing.T) { rows := strings.Split(buf.String(), "\n") // table must display only 38 rows (35 flows + header + footer + empty line) assert.Equal(t, 38, len(rows)) - assert.Equal(t, `Time SrcAddr SrcPort DstAddr DstPort Dir Interfaces Proto Dscp Bytes Packets `, rows[0]) + assert.Equal(t, `End Time Src IP Src Port Dst IP Dst Port Node Dir Interfaces L3 Layer Protocol L3 Layer DSCP Bytes Packets `, rows[0]) // first flow is the 6th one that came to the display - assert.Equal(t, `00:00:06.000000 10.0.0.5 n/a 10.0.0.6 n/a n/a n/a n/a n/a 6KB 1 `, rows[1]) - assert.Equal(t, `00:00:07.000000 10.0.0.5 n/a 10.0.0.6 n/a n/a n/a n/a n/a 7KB 1 `, rows[2]) - assert.Equal(t, `00:00:08.000000 10.0.0.5 n/a 10.0.0.6 n/a n/a n/a n/a n/a 8KB 1 `, rows[3]) - assert.Equal(t, `00:00:09.000000 10.0.0.5 n/a 10.0.0.6 n/a n/a n/a n/a n/a 9KB 1 `, rows[4]) - assert.Equal(t, `00:00:10.000000 10.0.0.5 n/a 10.0.0.6 n/a n/a n/a n/a n/a 10KB 1 `, rows[5]) + assert.Equal(t, `00:00:06.000000 10.0.0.5 n/a 10.0.0.6 n/a n/a n/a n/a n/a 6KB 1 `, rows[1]) + assert.Equal(t, `00:00:07.000000 10.0.0.5 n/a 10.0.0.6 n/a n/a n/a n/a n/a 7KB 1 `, rows[2]) + assert.Equal(t, `00:00:08.000000 10.0.0.5 n/a 10.0.0.6 n/a n/a n/a n/a n/a 8KB 1 `, rows[3]) + assert.Equal(t, `00:00:09.000000 10.0.0.5 n/a 10.0.0.6 n/a n/a n/a n/a n/a 9KB 1 `, rows[4]) + assert.Equal(t, `00:00:10.000000 10.0.0.5 n/a 10.0.0.6 n/a n/a n/a n/a n/a 10KB 1 `, rows[5]) // last flow is the 40th one - assert.Equal(t, `00:00:40.000000 10.0.0.5 n/a 10.0.0.6 n/a n/a n/a n/a n/a 40KB 1 `, rows[35]) - assert.Equal(t, `---------------- ---------------------------------------- ------ ---------------------------------------- ------ ---------- ---------------- ------ -------- ------ ------ `, rows[36]) + assert.Equal(t, `00:00:40.000000 10.0.0.5 n/a 10.0.0.6 n/a n/a n/a n/a n/a 40KB 1 `, rows[35]) + assert.Equal(t, `--------------- ---------- ---------- ---------- ---------- ---------- ---------- ---------- ---------- ----- ----- `, rows[36]) assert.Empty(t, rows[37]) } func TestFlowTableAdvancedDisplay(t *testing.T) { - setup() + setup(t) // set output buffer to draw table buf := bytes.Buffer{} @@ -111,7 +111,7 @@ func TestFlowTableAdvancedDisplay(t *testing.T) { getRows := func(d []string, e []string) []string { // prepare display options display = d - enrichement = e + enrichment = e // clear filters and previous flows regexes = []string{} @@ -129,52 +129,52 @@ func TestFlowTableAdvancedDisplay(t *testing.T) { // set display without enrichment rows := getRows([]string{pktDropDisplay, dnsDisplay, rttDisplay, networkEventsDisplay}, []string{noEnrichment}) assert.Equal(t, 4, len(rows)) - assert.Equal(t, `Time SrcAddr SrcPort DstAddr DstPort DropBytes DropPackets DropState DropCause DnsId DnsLatency DnsRCode DnsErrno RTT NetworkEvents `, rows[0]) - assert.Equal(t, `17:25:28.703000 10.128.0.29 1234 10.129.0.26 5678 32B 1 TCP_INVALID_STATE SKB_DROP_REASON_TCP_INVALID_SEQUENCE 31319 1ms NoError 0 10µs hello `, rows[1]) - assert.Equal(t, `---------------- ---------------------------------------- ------ ---------------------------------------- ------ ------------ ------------ -------------------- ---------------------------------------- ------ ------ ------ ------ ------ ---------------- `, rows[2]) + assert.Equal(t, `End Time Src IP Src Port Dst IP Dst Port Dropped Bytes Dropped Packets Drop State Drop Cause Drop Flags DNS Id DNS Latency DNS RCode DNS Error Flow RTT Network Events `, rows[0]) + assert.Equal(t, `17:25:28.703000 10.128.0.29 1234 10.129.0.26 5678 32B 1 TCP_INVALID_STATE SKB_DROP_REASON_TCP_INVALID_SEQUENCE 16 31319 1ms NoError 0 10µs hello `, rows[1]) + assert.Equal(t, `--------------- ---------- ---------- ---------- ---------- ----- ----- ---------- ---------- ---------- ----- ----- ----- ----- ----- --------------- `, rows[2]) assert.Empty(t, rows[3]) // set display to standard rows = getRows([]string{standardDisplay}, []string{noEnrichment}) assert.Equal(t, 4, len(rows)) - assert.Equal(t, `Time SrcAddr SrcPort DstAddr DstPort Dir Interfaces Proto Dscp Bytes Packets `, rows[0]) - assert.Equal(t, `17:25:28.703000 10.128.0.29 1234 10.129.0.26 5678 Ingress f18b970c2ce8fdd TCP Standard 456B 5 `, rows[1]) - assert.Equal(t, `---------------- ---------------------------------------- ------ ---------------------------------------- ------ ---------- ---------------- ------ -------- ------ ------ `, rows[2]) + assert.Equal(t, `End Time Src IP Src Port Dst IP Dst Port Node Dir Interfaces L3 Layer Protocol L3 Layer DSCP Bytes Packets `, rows[0]) + assert.Equal(t, `17:25:28.703000 10.128.0.29 1234 10.129.0.26 5678 Ingress f18b970c2ce8fdd TCP Standard 456B 5 `, rows[1]) + assert.Equal(t, `--------------- ---------- ---------- ---------- ---------- ---------- ---------- ---------- ---------- ----- ----- `, rows[2]) assert.Empty(t, rows[3]) // set display to pktDrop rows = getRows([]string{pktDropDisplay}, []string{noEnrichment}) assert.Equal(t, 4, len(rows)) - assert.Equal(t, `Time SrcAddr SrcPort DstAddr DstPort DropBytes DropPackets DropState DropCause `, rows[0]) - assert.Equal(t, `17:25:28.703000 10.128.0.29 1234 10.129.0.26 5678 32B 1 TCP_INVALID_STATE SKB_DROP_REASON_TCP_INVALID_SEQUENCE `, rows[1]) - assert.Equal(t, `---------------- ---------------------------------------- ------ ---------------------------------------- ------ ------------ ------------ -------------------- ---------------------------------------- `, rows[2]) + assert.Equal(t, `End Time Src IP Src Port Dst IP Dst Port Dropped Bytes Dropped Packets Drop State Drop Cause Drop Flags `, rows[0]) + assert.Equal(t, `17:25:28.703000 10.128.0.29 1234 10.129.0.26 5678 32B 1 TCP_INVALID_STATE SKB_DROP_REASON_TCP_INVALID_SEQUENCE 16 `, rows[1]) + assert.Equal(t, `--------------- ---------- ---------- ---------- ---------- ----- ----- ---------- ---------- ---------- `, rows[2]) assert.Empty(t, rows[3]) // set display to DNS rows = getRows([]string{dnsDisplay}, []string{noEnrichment}) assert.Equal(t, 4, len(rows)) - assert.Equal(t, `Time SrcAddr SrcPort DstAddr DstPort DnsId DnsLatency DnsRCode DnsErrno `, rows[0]) - assert.Equal(t, `17:25:28.703000 10.128.0.29 1234 10.129.0.26 5678 31319 1ms NoError 0 `, rows[1]) - assert.Equal(t, `---------------- ---------------------------------------- ------ ---------------------------------------- ------ ------ ------ ------ ------ `, rows[2]) + assert.Equal(t, `End Time Src IP Src Port Dst IP Dst Port DNS Id DNS Latency DNS RCode DNS Error `, rows[0]) + assert.Equal(t, `17:25:28.703000 10.128.0.29 1234 10.129.0.26 5678 31319 1ms NoError 0 `, rows[1]) + assert.Equal(t, `--------------- ---------- ---------- ---------- ---------- ----- ----- ----- ----- `, rows[2]) assert.Empty(t, rows[3]) // set display to RTT rows = getRows([]string{rttDisplay}, []string{noEnrichment}) assert.Equal(t, 4, len(rows)) - assert.Equal(t, `Time SrcAddr SrcPort DstAddr DstPort RTT `, rows[0]) - assert.Equal(t, `17:25:28.703000 10.128.0.29 1234 10.129.0.26 5678 10µs `, rows[1]) - assert.Equal(t, `---------------- ---------------------------------------- ------ ---------------------------------------- ------ ------ `, rows[2]) + assert.Equal(t, `End Time Src IP Src Port Dst IP Dst Port Flow RTT `, rows[0]) + assert.Equal(t, `17:25:28.703000 10.128.0.29 1234 10.129.0.26 5678 10µs `, rows[1]) + assert.Equal(t, `--------------- ---------- ---------- ---------- ---------- ----- `, rows[2]) assert.Empty(t, rows[3]) // set display to NetworkEvents rows = getRows([]string{networkEventsDisplay}, []string{noEnrichment}) assert.Equal(t, 4, len(rows)) - assert.Equal(t, `Time SrcAddr SrcPort DstAddr DstPort NetworkEvents `, rows[0]) - assert.Equal(t, `17:25:28.703000 10.128.0.29 1234 10.129.0.26 5678 hello `, rows[1]) - assert.Equal(t, `---------------- ---------------------------------------- ------ ---------------------------------------- ------ ---------------- `, rows[2]) + assert.Equal(t, `End Time Src IP Src Port Dst IP Dst Port Network Events `, rows[0]) + assert.Equal(t, `17:25:28.703000 10.128.0.29 1234 10.129.0.26 5678 hello `, rows[1]) + assert.Equal(t, `--------------- ---------- ---------- ---------- ---------- --------------- `, rows[2]) assert.Empty(t, rows[3]) } diff --git a/cmd/map_format.go b/cmd/map_format.go index 2be596b3..772d6cf7 100644 --- a/cmd/map_format.go +++ b/cmd/map_format.go @@ -3,6 +3,7 @@ package cmd import ( "fmt" "reflect" + "slices" "strings" "time" @@ -14,6 +15,20 @@ const ( emptyText = "n/a" ) +var ( + // dictionnary to shorten long keywords at display + replacer = strings.NewReplacer( + "Source", "Src", + "Destination", "Dst", + "Direction", "Dir", + "MultiCluster", "Clusters", + "Response Code", "RCode", + "PktDrop", "Drop", + "DnsTracking", "DNS", + "FlowRTT", "RTT", + ) +) + func toCount(genericMap config.GenericMap, fieldName string) interface{} { v, ok := genericMap[fieldName] if ok { @@ -385,7 +400,7 @@ func toDSCP(genericMap config.GenericMap, fieldName string) interface{} { return emptyText } -func toText(genericMap config.GenericMap, fieldName string) interface{} { +func toValue(genericMap config.GenericMap, fieldName string) interface{} { v, ok := genericMap[fieldName] if ok { if reflect.TypeOf(v).Kind() == reflect.Slice { @@ -416,67 +431,82 @@ func toTimeString(genericMap config.GenericMap, fieldName string) string { return emptyText } -func ToTableRow(genericMap config.GenericMap, cols []string) []interface{} { +func toTitles(strs []string) []string { + titleCaseStrs := []string{} + for _, s := range strs { + titleCaseStrs = append(titleCaseStrs, fmt.Sprintf("%s%s", strings.ToUpper(s[:1]), s[1:])) + } + return titleCaseStrs +} + +func toShortTitleStr(strs []string) string { + return replacer.Replace(strings.Join(toTitles(strs), ",")) +} + +func ToTableColName(id string) string { + name := id + colIndex := slices.IndexFunc(cfg.Columns, func(c *ColumnConfig) bool { return c.ID == id }) + if colIndex != -1 { + col := cfg.Columns[colIndex] + if col.Group != "" && !strings.Contains(col.Name, col.Group) { + name = fmt.Sprintf("%s %s", col.Group, col.Name) + } else { + name = col.Name + } + } + return replacer.Replace(name) +} + +func ToTableColWidth(id string) int { + colIndex := slices.IndexFunc(cfg.Columns, func(c *ColumnConfig) bool { return c.ID == id }) + if colIndex != -1 { + return cfg.Columns[colIndex].Width + } + return 6 +} + +func toFieldName(id string) string { + colIndex := slices.IndexFunc(cfg.Columns, func(c *ColumnConfig) bool { return c.ID == id }) + if colIndex != -1 { + return cfg.Columns[colIndex].Field + } + return "" +} + +func ToTableRow(genericMap config.GenericMap, colIDs []string) []interface{} { row := []interface{}{} - for _, col := range cols { - // convert field name / value accordingly - switch col { - case "Time": + for _, colID := range colIDs { + // convert column id to its field accordingly + fieldName := toFieldName(colID) + + switch colID { + case "EndTime": if captureType == "Flow" { row = append(row, toTimeString(genericMap, "TimeFlowEndMs")) } else { row = append(row, toTimeString(genericMap, "Time")) } - case "SrcZone": - row = append(row, toText(genericMap, "SrcK8S_Zone")) - case "DstZone": - row = append(row, toText(genericMap, "DstK8S_Zone")) - case "SrcHostName": - row = append(row, toText(genericMap, "SrcK8S_HostName")) - case "DstHostName": - row = append(row, toText(genericMap, "DstK8S_HostName")) - case "SrcOwnerName": - row = append(row, toText(genericMap, "SrcK8S_OwnerName")) - case "SrcOwnerType": - row = append(row, toText(genericMap, "SrcK8S_OwnerType")) - case "DstOwnerName": - row = append(row, toText(genericMap, "DstK8S_OwnerName")) - case "DstOwnerType": - row = append(row, toText(genericMap, "DstK8S_OwnerType")) - case "SrcName": - row = append(row, toText(genericMap, "SrcK8S_Name")) - case "SrcType": - row = append(row, toText(genericMap, "SrcK8S_Type")) - case "DstName": - row = append(row, toText(genericMap, "DstK8S_Name")) - case "DstType": - row = append(row, toText(genericMap, "DstK8S_Type")) - case "Dir": - row = append(row, toDirection(genericMap, "FlowDirection")) + // special cases where autocompletes are involved + case "FlowDirection": + row = append(row, toDirection(genericMap, fieldName)) case "Proto": - row = append(row, toProto(genericMap, "Proto")) + row = append(row, toProto(genericMap, fieldName)) case "Dscp": - row = append(row, toDSCP(genericMap, "Dscp")) + row = append(row, toDSCP(genericMap, fieldName)) + // bytes count case "Bytes": row = append(row, toCount(genericMap, "Bytes")) - case "DropBytes": + case "PktDropBytes": row = append(row, toCount(genericMap, "PktDropBytes")) - case "DropPackets": - row = append(row, toText(genericMap, "PktDropPackets")) - case "DropState": - row = append(row, toText(genericMap, "PktDropLatestState")) - case "DropCause": - row = append(row, toText(genericMap, "PktDropLatestDropCause")) - case "DnsLatency": - row = append(row, toDuration(genericMap, "DnsLatencyMs", time.Millisecond)) - case "DnsRCode": - row = append(row, toText(genericMap, "DnsFlagsResponseCode")) - case "RTT": - row = append(row, toDuration(genericMap, "TimeFlowRttNs", time.Nanosecond)) + // duration parsing + case "DNSLatency": + row = append(row, toDuration(genericMap, fieldName, time.Millisecond)) + case "TimeFlowRttMs": + row = append(row, toDuration(genericMap, fieldName, time.Nanosecond)) default: // else simply pick field value as text from column name - row = append(row, toText(genericMap, col)) + row = append(row, toValue(genericMap, fieldName)) } } diff --git a/cmd/map_format_test.go b/cmd/map_format_test.go index ce9eaac5..cc7bc506 100644 --- a/cmd/map_format_test.go +++ b/cmd/map_format_test.go @@ -9,16 +9,18 @@ import ( ) func TestFormatTableRows(t *testing.T) { + setup(t) + var flow config.GenericMap err := json.Unmarshal([]byte(sampleFlow), &flow) assert.Nil(t, err) - tableRow := ToTableRow(flow, []string{"Time", "SrcAddr", "DstAddr", "Bytes", "Packets", "Dir", "Proto", "Dscp"}) + tableRow := ToTableRow(flow, []string{"EndTime", "SrcAddr", "DstAddr", "Bytes", "Packets", "FlowDirection", "Proto", "Dscp"}) assert.Equal(t, []interface{}{"17:25:28.703000", "10.128.0.29", "10.129.0.26", "456B", float64(5), "Ingress", "TCP", "Standard"}, tableRow) - tableRow = ToTableRow(flow, []string{"SrcZone", "DstZone", "SrcHostName", "DstHostName", "SrcOwnerName", "DstOwnerName", "SrcName", "DstName"}) + tableRow = ToTableRow(flow, []string{"SrcZone", "DstZone", "SrcK8S_HostName", "DstK8S_HostName", "SrcK8S_OwnerName", "DstK8S_OwnerName", "SrcK8S_Name", "DstK8S_Name"}) assert.Equal(t, []interface{}{"us-east-1d", "us-west-1a", "ip-XX-X-X-XX1.ec2.internal", "ip-XX-X-X-XX2.ec2.internal", "my-deployment", "my-statefulset", "src-pod", "dst-pod"}, tableRow) - tableRow = ToTableRow(flow, []string{"DropBytes", "DropPackets", "DropState", "DropCause", "DnsLatency", "DnsRCode", "RTT"}) + tableRow = ToTableRow(flow, []string{"PktDropBytes", "PktDropPackets", "PktDropLatestState", "PktDropLatestDropCause", "DNSLatency", "DNSResponseCode", "TimeFlowRttMs"}) assert.Equal(t, []interface{}{"32B", float64(1), "TCP_INVALID_STATE", "SKB_DROP_REASON_TCP_INVALID_SEQUENCE", "1ms", "NoError", "10µs"}, tableRow) } diff --git a/cmd/packet_capture.go b/cmd/packet_capture.go index b61fa2a9..70103bf2 100644 --- a/cmd/packet_capture.go +++ b/cmd/packet_capture.go @@ -177,23 +177,23 @@ func runPacketCaptureOnAddr(port int, filename string) { func writeEnrichedData(pw *pcapng.FileWriter, genericMap *config.GenericMap) { var io types.InterfaceOptions - srcType := toText(*genericMap, "SrcK8S_Type").(string) + srcType := toValue(*genericMap, "SrcK8S_Type").(string) if srcType != emptyText { io = types.InterfaceOptions{ Name: fmt.Sprintf( "%s: %s -> %s: %s", srcType, - toText(*genericMap, "SrcK8S_Name"), - toText(*genericMap, "DstK8S_Type"), - toText(*genericMap, "DstK8S_Name")), + toValue(*genericMap, "SrcK8S_Name"), + toValue(*genericMap, "DstK8S_Type"), + toValue(*genericMap, "DstK8S_Name")), Description: fmt.Sprintf( "%s: %s Namespace: %s -> %s: %s Namespace: %s", - toText(*genericMap, "SrcK8S_OwnerType"), - toText(*genericMap, "SrcK8S_OwnerName"), - toText(*genericMap, "SrcK8S_Namespace"), - toText(*genericMap, "DstK8S_OwnerType"), - toText(*genericMap, "DstK8S_OwnerName"), - toText(*genericMap, "DstK8S_Namespace"), + toValue(*genericMap, "SrcK8S_OwnerType"), + toValue(*genericMap, "SrcK8S_OwnerName"), + toValue(*genericMap, "SrcK8S_Namespace"), + toValue(*genericMap, "DstK8S_OwnerType"), + toValue(*genericMap, "DstK8S_OwnerName"), + toValue(*genericMap, "DstK8S_Namespace"), ), } } else { diff --git a/cmd/root.go b/cmd/root.go index 747b6c44..a3970d7f 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -103,6 +103,11 @@ func onInit() { log.Fatalf("specified nodes names doesn't match ports length") } + err := LoadConfig() + if err != nil { + log.Fatalf("can't load config from yaml: %v", err) + } + log.Infof("Running network-observability-cli\nLog level: %s\nFilter(s): %s", logLevel, filter) showKernelVersion() diff --git a/cmd/root_test.go b/cmd/root_test.go index 7164c8fe..dcff9950 100644 --- a/cmd/root_test.go +++ b/cmd/root_test.go @@ -76,13 +76,17 @@ func TestDefaultArguments(t *testing.T) { assert.Empty(t, filter) } -func setup() { +func setup(t *testing.T) { // reset time to startup time resetTime() // clear filters and previous flows regexes = []string{} lastFlows = []config.GenericMap{} + + // load config + err := LoadConfig() + assert.Equal(t, nil, err) } func resetTime() { diff --git a/go.mod b/go.mod index 626eeae3..344289a5 100644 --- a/go.mod +++ b/go.mod @@ -16,6 +16,7 @@ require ( github.com/sirupsen/logrus v1.9.3 github.com/spf13/cobra v1.8.1 github.com/stretchr/testify v1.9.0 + gopkg.in/yaml.v3 v3.0.1 sigs.k8s.io/e2e-framework v0.4.0 ) @@ -83,7 +84,6 @@ require ( google.golang.org/protobuf v1.34.2 gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/api v0.31.1 // indirect k8s.io/apimachinery v0.31.1 // indirect k8s.io/client-go v0.31.1 // indirect diff --git a/scripts/update-config.sh b/scripts/update-config.sh new file mode 100755 index 00000000..80578c05 --- /dev/null +++ b/scripts/update-config.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +echo "Downloading frontend config from operator repo" +curl "https://raw.githubusercontent.com/netobserv/network-observability-operator/refs/heads/main/controllers/consoleplugin/config/static-frontend-config.yaml" -o ./cmd/config.yaml \ No newline at end of file