Skip to content

Commit d23dc54

Browse files
alanconwayopenshift-cherrypick-robot
authored andcommitted
fix(#134): COO-946: Incorrect mapping of k8s event resources
K8s event resources were originally in the core API group, now are also in the 'events.k8s.io' group. Fix the troubleshooting panel to account for both possibilities. Also - added class name to context menus to disambiguate nodes with same Kind but different API version. fixes #134
1 parent 6169134 commit d23dc54

4 files changed

Lines changed: 86 additions & 53 deletions

File tree

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,24 @@
11
{
22
"Correlation result was empty.": "Correlation result was empty.",
3-
"Date and Time Range": "Date and Time Range",
3+
"Depth": "Depth",
44
"Find related resources.": "Find related resources.",
55
"Focus": "Focus",
6-
"Goal class: ": "Goal class: ",
7-
"Hide Query": "Hide Query",
8-
"Korrel8 query selecting the starting points for correlation.": "Starting query",
6+
"Generate a correlation graph starting from resources in the current view.": "Generate a correlation graph starting from resources in the current view.",
7+
"Goal directed search": "Goal directed search",
98
"Korrel8r Error": "Korrel8r Error",
109
"Logging Plugin Disabled": "Logging Plugin Disabled",
11-
"Neighbourhood depth: ": "Neighbourhood depth: ",
10+
"Neighbourhood search": "Neighbourhood search",
1211
"Netflow Plugin Disabled": "Netflow Plugin Disabled",
1312
"No Correlated Signals Found": "No Correlated Signals Found",
1413
"Open the Troubleshooting Panel": "Open the Troubleshooting Panel",
15-
"Query": "Query",
16-
"Re-calculate the correlation graph starting from resources on the current console page.": "Re-calculate the correlation graph starting from resources on the current console page.",
14+
"Query to select the starting resources for correlation.": "Query to select the starting resources for correlation.",
15+
"Refresh the graph using the current search settings": "Refresh the graph using the current search settings",
1716
"Request Failed": "Request Failed",
18-
"Show graph of connected classes up to the specified depth.": "Show graph of connected classes up to the specified depth.",
19-
"Show graph of paths to signals of the specified class.": "Show graph of paths to signals of the specified class.",
20-
"Show Query": "Show Query",
21-
"The current console page is not supported for correlation.": "The current console page is not supported for correlation.",
17+
"Search for correlated resources up to the specified depth.": "Search for correlated resources up to the specified depth.",
18+
"Search for paths to resources of the specified class.": "Search for paths to resources of the specified class.",
19+
"The current view does not support correlation.": "The current view does not support correlation.",
2220
"Troubleshooting": "Troubleshooting",
2321
"Troubleshooting Panel": "Troubleshooting Panel",
24-
"Unable to find Console Link": "Unable to find Console Link"
22+
"Unable to find Console Link": "Unable to find Console Link",
23+
"Update": "Update"
2524
}

web/src/__tests__/k8s.spec.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { URIRef, Query } from '../korrel8r/types';
21
import { K8sDomain } from '../korrel8r/k8s';
2+
import { Query, URIRef } from '../korrel8r/types';
33

44
const k8s = new K8sDomain();
55

@@ -138,10 +138,6 @@ describe('K8sNode.fromQuery', () => {
138138
},
139139

140140
// Variations on korrel8r class spec.
141-
{ query: `k8s:Role:{"namespace":"x", "name":"y"}`, u: `k8s/ns/x/roles/y` },
142-
{ query: `k8s:Role.:{"namespace":"x", "name":"y"}`, u: `k8s/ns/x/roles/y` },
143-
{ query: `k8s:Role.v1:{"namespace":"x", "name":"y"}`, u: `k8s/ns/x/roles/y` },
144-
{ query: `k8s:Role.v1:{"namespace":"x", "name":"y"}`, u: `k8s/ns/x/roles/y` },
145141
{
146142
query: `k8s:Role.v1.rbac.authorization.k8s.io:{"namespace":"x", "name":"y"}`,
147143
u: `k8s/ns/x/roles/y`,

web/src/components/topology/Korrel8rTopology.tsx

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Badge } from '@patternfly/react-core';
1+
import { Badge, Title } from '@patternfly/react-core';
22
import { ClusterIcon } from '@patternfly/react-icons';
33
import {
44
action,
@@ -147,7 +147,8 @@ export const Korrel8rTopology: React.FC<{
147147
const navigateToQuery = React.useCallback(
148148
(query: korrel8r.Query) => {
149149
try {
150-
navigate('/' + allDomains.queryToLink(query));
150+
const link = allDomains.queryToLink(query);
151+
navigate(link.startsWith('/') ? link : `/${link}`);
151152
} catch (e) {
152153
// eslint-disable-next-line no-console
153154
console.error(`navigateToQuery failed for: ${query.toString()}: `, e);
@@ -170,18 +171,27 @@ export const Korrel8rTopology: React.FC<{
170171
const nodeMenu = React.useCallback(
171172
(e: GraphElement<ElementModel, korrel8r.Node>): React.ReactElement[] => {
172173
const node = e.getData();
173-
return nodeQueries(node).map((qc) => (
174-
<ContextMenuItem
175-
key={qc.query.toString()}
176-
onClick={() => {
177-
navigateToQuery(qc.query);
178-
setSelectedIds([node.id]);
179-
navigator.clipboard.writeText(qc.query.toString());
180-
}}
181-
>
182-
<Badge>{`${qc.count}`}</Badge> {`${qc.query.selector}`}
183-
</ContextMenuItem>
184-
));
174+
const menu = [
175+
<ContextMenuItem isDisabled={true} key={node.class.toString()}>
176+
<Title headingLevel="h4">{node.class.toString()}</Title>
177+
</ContextMenuItem>,
178+
];
179+
nodeQueries(node).forEach((qc) =>
180+
menu.push(
181+
<ContextMenuItem
182+
key={qc.query.toString()}
183+
onClick={() => {
184+
navigateToQuery(qc.query);
185+
setSelectedIds([node.id]);
186+
navigator.clipboard.writeText(qc.query.toString());
187+
}}
188+
icon={<Badge>{`${qc.count}`}</Badge>}
189+
>
190+
{`${qc.query.selector}`}
191+
</ContextMenuItem>,
192+
),
193+
);
194+
return menu;
185195
},
186196
[navigateToQuery, setSelectedIds],
187197
);

web/src/korrel8r/k8s.ts

Lines changed: 50 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { getCachedResources } from '../getResources';
2-
import { Domain, Class, Query, URIRef, keyValueList } from './types';
2+
import { Class, Domain, Query, URIRef, keyValueList } from './types';
33

44
// k8s model type stored in browser cache.
55
type Model = {
@@ -18,17 +18,18 @@ type Selector = {
1818
fields?: { [key: string]: string };
1919
};
2020

21-
const pathRegex = new RegExp(
21+
const PATH_RE = new RegExp(
2222
'(?:k8s|search)/(?:(?:ns/([^/]+))|cluster|all-namespaces)(?:/([^/]+)(?:/([^/]+))?)?(/events)?/?$',
2323
);
24+
const CLASS_RE = new RegExp('^([^./]+)(?:.(v[0-9]+(?:(?:alpha|beta)[0-9]*)?))?(?:.([^/]*))?$');
2425

2526
export class K8sDomain extends Domain {
2627
constructor() {
2728
super('k8s');
2829
}
2930

3031
class(name: string): Class {
31-
const m = name.match(/^([^.]+)(?:\.([^.]*)(?:\.(.*))?)?$/) ?? [];
32+
const m = name.match(CLASS_RE);
3233
if (!m) throw this.badClass(name);
3334
const model = findGVK(m[3], m[2], m[1]);
3435
if (!model) throw this.badClass(name);
@@ -42,7 +43,7 @@ export class K8sDomain extends Domain {
4243
}
4344

4445
linkToQuery(link: URIRef): Query {
45-
const m = link.pathname.match(pathRegex);
46+
const m = link.pathname.match(PATH_RE);
4647
if (!m) throw this.badLink(link);
4748
const [, namespace, resource, name, events] = m;
4849
const model = findResource(resource, link.searchParams.get('kind'));
@@ -57,7 +58,7 @@ export class K8sDomain extends Domain {
5758
'involvedObject.kind': model.kind,
5859
},
5960
};
60-
return this.class('Event.v1').query(JSON.stringify(data));
61+
return this.modelClass(eventModel()).query(JSON.stringify(data));
6162
} else {
6263
const data = {
6364
namespace: namespace,
@@ -79,27 +80,28 @@ export class K8sDomain extends Domain {
7980
if (!m) throw this.badQuery(query);
8081
let model = findGVK(m[3], m[2], m[1]);
8182
if (!model) throw this.badQuery(query);
82-
let namespace = data.namespace,
83-
name = data.name,
84-
events = '';
85-
if (model.kind == 'Event' && model.apiVersion == 'v1' && !model.apiGroup) {
83+
let namespace = data.namespace;
84+
let name = data.name;
85+
let events = '';
86+
if (isEvent(model)) {
8687
// Special treatment for event objects: focus on the involved object, not the event.
8788
events = '/events';
89+
const about = eventAboutField(model);
8890
model = findGVK(
89-
data.fields['involvedObject.apiGroup'],
90-
data.fields['involvedObject.apiVersion'],
91-
data.fields['involvedObject.kind'],
91+
data.fields[`${about}.apiGroup`],
92+
data.fields[`${about}.apiVersion`],
93+
data.fields[`${about}.kind`],
9294
);
93-
if (!model) throw this.badQuery(query);
94-
namespace = data.fields['involvedObject.namespace'] || '';
95-
name = data.fields['involvedObject.name'] || '';
95+
if (!model)
96+
throw this.badQuery(query, `no resource for '${about}' field in ${query.selector}`);
97+
namespace = data.fields[`${about}.namespace`] || '';
98+
name = data.fields[`${about}.name`] || '';
9699
}
97100
// Prepare parts of the URL
98101
const nsPath = namespace ? `ns/${namespace}` : 'all-namespaces';
99102
const labelsParam = data.labels
100103
? `?labels=${encodeURIComponent(keyValueList(data.labels))}`
101104
: '';
102-
model;
103105
if (!name && !namespace && labelsParam) {
104106
// Search URL
105107
return (
@@ -124,15 +126,41 @@ export class K8sDomain extends Domain {
124126
}
125127
}
126128

129+
// Original k8s Event resource was in the core group, modern Event is in the events.k8s.io group.
130+
// Event.v1 has an 'involvedObject' field, Event.v1.events.k8s.io has a 'regarding' field.
131+
// Handle both variations.
132+
const EVENT = {
133+
group: 'events.k8s.io',
134+
version: 'v1',
135+
kind: 'Event',
136+
};
137+
function isEvent(m: Model): boolean {
138+
return (
139+
m.kind == EVENT.kind &&
140+
m.apiVersion == EVENT.version &&
141+
(!m.apiGroup || m.apiGroup === EVENT.group)
142+
);
143+
}
144+
function eventModel(): Model {
145+
return findGVK(EVENT.group, EVENT.version, EVENT.kind) || findGVK('', EVENT.version, EVENT.kind);
146+
}
147+
148+
function eventAboutField(m: Model): string {
149+
return m?.apiGroup === EVENT.group ? 'regarding' : 'involvedObject';
150+
}
151+
152+
// Find the cached resource model for a GVK. Same defaulting rules as korrel8r..
127153
function findGVK(group: string, version: string, kind: string): Model {
128-
const model = getCachedResources()?.models.find(
129-
(m: Model) =>
154+
version = version || 'v1';
155+
group = group || '';
156+
return getCachedResources()?.models.find((m: Model) => {
157+
return (
130158
m.kind === kind &&
131159
m.verbs.includes('watch') &&
132-
(!group || group === m.apiGroup) &&
133-
(!version || version === m.apiVersion),
134-
);
135-
return model || undefined;
160+
m.apiVersion === version &&
161+
(m.apiGroup || '') === group
162+
);
163+
});
136164
}
137165

138166
// Return a model for the resource (path) or undefined

0 commit comments

Comments
 (0)