Skip to content

Commit 7b2a893

Browse files
author
Mario Macias
authored
NETOBSERV-223: filter by empty IP and numbers (#168)
* NETOBSERV-223: filter by empty IP * filter by empty/undefined port and protocol * renamed constant * fix linting error
1 parent 07ebba8 commit 7b2a893

File tree

8 files changed

+63
-11
lines changed

8 files changed

+63
-11
lines changed

pkg/loki/flow_query.go

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ const (
1616
limitParam = "limit"
1717
queryRangePath = "/loki/api/v1/query_range?query="
1818
jsonOrJoiner = "+or+"
19+
emptyMatch = `""`
1920
)
2021

2122
// can contains only alphanumeric / '-' / '_' / '.' / ',' / '"' / '*' / ':' / '/' characteres
@@ -131,11 +132,11 @@ func (q *FlowQueryBuilder) addLineFilters(key string, values []string) {
131132
for _, value := range values {
132133
lm := lineMatch{}
133134
switch {
134-
case isNumeric:
135-
lm = lineMatch{valueType: typeNumber, value: value}
136135
case isExactMatch(value):
137136
lm = lineMatch{valueType: typeString, value: trimExactMatch(value)}
138137
emptyMatches = emptyMatches || len(lm.value) == 0
138+
case isNumeric:
139+
lm = lineMatch{valueType: typeNumber, value: value}
139140
default:
140141
lm = lineMatch{valueType: typeRegex, value: value}
141142
}
@@ -153,9 +154,14 @@ func (q *FlowQueryBuilder) addLineFilters(key string, values []string) {
153154
// addIPFilters assumes that we are searching for that IP addresses as part
154155
// of the log line (not in the stream selector labels)
155156
func (q *FlowQueryBuilder) addIPFilters(key string, values []string) {
156-
var filtersPerKey []labelFilter
157+
filtersPerKey := make([]labelFilter, 0, len(values))
157158
for _, value := range values {
158-
filtersPerKey = append(filtersPerKey, ipLabelFilter(key, value))
159+
// empty exact matches should be treated as attribute filters looking for empty IP
160+
if value == emptyMatch {
161+
filtersPerKey = append(filtersPerKey, stringLabelFilter(key, ""))
162+
} else {
163+
filtersPerKey = append(filtersPerKey, ipLabelFilter(key, value))
164+
}
159165
}
160166
q.jsonFilters = append(q.jsonFilters, filtersPerKey)
161167
}

pkg/server/server_flows_test.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,17 @@ func TestLokiFiltering(t *testing.T) {
185185
outputQueries: []string{
186186
"?query={app=\"netobserv-flowcollector\"}|json|SrcK8S_Type=\"\"+or+SrcK8S_Type=\"Pod\"",
187187
},
188+
}, {
189+
inputPath: "?filters=" + url.QueryEscape(`SrcAddr=""|DstAddr=""`),
190+
outputQueries: []string{
191+
"?query={app=\"netobserv-flowcollector\"}|json|DstAddr=\"\"",
192+
"?query={app=\"netobserv-flowcollector\"}|json|SrcAddr=\"\"",
193+
},
194+
}, {
195+
inputPath: "?filters=" + url.QueryEscape(`SrcPort=""`),
196+
outputQueries: []string{
197+
"?query={app=\"netobserv-flowcollector\"}|json|SrcPort=\"\"",
198+
},
188199
}}
189200

190201
numberQueriesExpected := 0

web/locales/en/plugin__network-observability-plugin.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,7 @@
205205
"A single IPv4 or IPv6 address like 192.0.2.0, ::1": "A single IPv4 or IPv6 address like 192.0.2.0, ::1",
206206
"An IP address range like 192.168.0.1-192.189.10.12, 2001:db8::1-2001:db8::8": "An IP address range like 192.168.0.1-192.189.10.12, 2001:db8::1-2001:db8::8",
207207
"A CIDR specification like 192.51.100.0/24, 2001:db8::/32": "A CIDR specification like 192.51.100.0/24, 2001:db8::/32",
208+
"Empty double quotes \"\" for an empty IP": "Empty double quotes \"\" for an empty IP",
208209
"Not a valid IPv4 or IPv6, nor a CIDR, nor an IP range separated by hyphen": "Not a valid IPv4 or IPv6, nor a CIDR, nor an IP range separated by hyphen",
209210
"Owner Name": "Owner Name",
210211
"Incomplete resource name, either kind, namespace or name is missing.": "Incomplete resource name, either kind, namespace or name is missing.",
@@ -222,9 +223,11 @@
222223
"Specify a single port following one of these rules:": "Specify a single port following one of these rules:",
223224
"A port number like 80, 21": "A port number like 80, 21",
224225
"A IANA name like HTTP, FTP": "A IANA name like HTTP, FTP",
226+
"Empty double quotes \"\" for undefined port": "Empty double quotes \"\" for undefined port",
225227
"Unknown protocol": "Unknown protocol",
226228
"Specify a single protocol number or name.": "Specify a single protocol number or name.",
227229
"Specify a single protocol following one of these rules:": "Specify a single protocol following one of these rules:",
228230
"A protocol number like 6, 17": "A protocol number like 6, 17",
229-
"A IANA name like TCP, UDP": "A IANA name like TCP, UDP"
231+
"A IANA name like TCP, UDP": "A IANA name like TCP, UDP",
232+
"Empty double quotes \"\" for undefined protocol": "Empty double quotes \"\" for undefined protocol"
230233
}

web/src/utils/__tests__/ip.spec.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,4 +69,7 @@ describe('validate IP filter', () => {
6969
expect(validateIPFilter('1.3.3.4/')).toBe(false);
7070
expect(validateIPFilter('1.3.3.4/3.2.1.0')).toBe(false);
7171
});
72+
it('should validate empty IPs', () => {
73+
expect(validateIPFilter('""')).toBe(true);
74+
});
7275
});

web/src/utils/__tests__/port.spec.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import { formatPort, comparePorts } from '../port';
22
import { config } from '../config';
3+
import { getFilterDefinitions } from '../filter-definitions';
4+
5+
const t = (k: string) => k;
36

47
describe('formatport', () => {
58
beforeEach(() => {
@@ -46,3 +49,10 @@ describe('comparePort', () => {
4649
expect(comparePorts(45392, 45392)).toEqual(0);
4750
});
4851
});
52+
53+
describe('validatePort', () => {
54+
it('should accept empty double quotes for empty/undefined ports', () => {
55+
const portFilter = getFilterDefinitions(t).find(f => f.id == 'port')!;
56+
expect(portFilter.validate(`""`)).toEqual({ val: '""' });
57+
});
58+
});

web/src/utils/__tests__/protocol.spec.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
import { compareProtocols, formatProtocol } from '../protocol';
2+
import { getFilterDefinitions } from '../filter-definitions';
3+
4+
const t = (k: string) => k;
25

36
describe('formatProtocol', () => {
47
it('should format protocol', () => {
@@ -15,3 +18,10 @@ describe('compareProtocol', () => {
1518
expect(sorted).toEqual([121, 6, 17, undefined]);
1619
});
1720
});
21+
22+
describe('validateProtocol', () => {
23+
it('should accept empty double quotes for empty/undefined protocols', () => {
24+
const protocolFilter = getFilterDefinitions(t).find(f => f.id == 'protocol')!;
25+
expect(protocolFilter.validate(`""`)).toEqual({ val: '""' });
26+
});
27+
});

web/src/utils/filter-definitions.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ import {
2424
cap10
2525
} from './filter-options';
2626

27+
// Convenience string to filter by undefined field values
28+
export const undefinedValue = '""';
29+
2730
type Field = keyof Fields | keyof Labels;
2831

2932
const singleFieldMapping = (field: Field) => {
@@ -133,7 +136,8 @@ export const getFilterDefinitions = (t: TFunction): FilterDefinition[] => {
133136
const ipExamples = `${t('Specify IP following one of these rules:')}
134137
- ${t('A single IPv4 or IPv6 address like 192.0.2.0, ::1')}
135138
- ${t('An IP address range like 192.168.0.1-192.189.10.12, 2001:db8::1-2001:db8::8')}
136-
- ${t('A CIDR specification like 192.51.100.0/24, 2001:db8::/32')}`;
139+
- ${t('A CIDR specification like 192.51.100.0/24, 2001:db8::/32')}
140+
- ${t('Empty double quotes "" for an empty IP')}`;
137141

138142
const invalidIPMessage = t('Not a valid IPv4 or IPv6, nor a CIDR, nor an IP range separated by hyphen');
139143

@@ -284,15 +288,16 @@ export const getFilterDefinitions = (t: TFunction): FilterDefinition[] => {
284288
return invalid(t('Value is empty'));
285289
}
286290
//allow any port number or valid name / value
287-
if (!isNaN(Number(value)) || getPort(value)) {
291+
if (value == undefinedValue || !isNaN(Number(value)) || getPort(value)) {
288292
return valid(value);
289293
}
290294
return invalid(t('Unknown port'));
291295
},
292296
hint: t('Specify a single port number or name.'),
293297
examples: `${t('Specify a single port following one of these rules:')}
294298
- ${t('A port number like 80, 21')}
295-
- ${t('A IANA name like HTTP, FTP')}`,
299+
- ${t('A IANA name like HTTP, FTP')}
300+
- ${t('Empty double quotes "" for undefined port')}`,
296301
fieldMatching: {}
297302
},
298303
singleFieldMapping('SrcPort'),
@@ -344,7 +349,7 @@ export const getFilterDefinitions = (t: TFunction): FilterDefinition[] => {
344349
return invalid(t('Value is empty'));
345350
}
346351
//allow any protocol number or valid name / value
347-
if (!isNaN(Number(value))) {
352+
if (value == undefinedValue || !isNaN(Number(value))) {
348353
return valid(value);
349354
} else {
350355
const proto = findProtocolOption(value);
@@ -357,7 +362,8 @@ export const getFilterDefinitions = (t: TFunction): FilterDefinition[] => {
357362
hint: t('Specify a single protocol number or name.'),
358363
examples: `${t('Specify a single protocol following one of these rules:')}
359364
- ${t('A protocol number like 6, 17')}
360-
- ${t('A IANA name like TCP, UDP')}`,
365+
- ${t('A IANA name like TCP, UDP')}
366+
- ${t('Empty double quotes "" for undefined protocol')}`,
361367
fieldMatching: { always: singleFieldMapping('Proto') }
362368
}
363369
];

web/src/utils/ip.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { undefinedValue } from './filter-definitions';
2+
13
export const compareIPs = (ip1: string, ip2: string) => {
24
const splitIp2 = ip2.split('.');
35
const tmpRes = ip1.split('.').map((num, i) => Number(num) - Number(splitIp2[i]));
@@ -14,10 +16,11 @@ export const compareIPs = (ip1: string, ip2: string) => {
1416
* - A single IPv4 or IPv6 address. Examples: 192.0.2.0, ::1
1517
* - A range within the IP address. Examples: 192.168.0.1-192.189.10.12, 2001:db8::1-2001:db8::8
1618
* - A CIDR specification. Examples: 192.51.100.0/24, 2001:db8::/32
19+
* - Empty double quotes "" for empty/null IP
1720
*/
1821
export const validateIPFilter = (ipFilter: string) => {
1922
ipFilter = ipFilter.trim();
20-
if (ipv4.test(ipFilter) || ipv6.test(ipFilter)) {
23+
if (ipFilter == undefinedValue || ipv4.test(ipFilter) || ipv6.test(ipFilter)) {
2124
return true;
2225
}
2326
const ips = ipFilter.split('-');

0 commit comments

Comments
 (0)