Skip to content

Commit a96eb80

Browse files
Merge pull request #137 from openshift-cherrypick-robot/cherry-pick-136-to-release-0.4
COO-946: [release-0.4] fix(#134): Incorrect mapping of k8s event resources
2 parents 6169134 + d23dc54 commit a96eb80

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)