Skip to content

Commit f8c7b7c

Browse files
Merge pull request #165 from alanconway/fix-api-resource
fix: COO-1178: 404 errors for api-resource and event URLs.
2 parents e4d5ed9 + d7acec3 commit f8c7b7c

5 files changed

Lines changed: 98 additions & 96 deletions

File tree

web/locales/en/plugin__troubleshooting-panel-console-plugin.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
{
22
"Advanced": "Advanced",
3+
"Ago": "Ago",
34
"Correlation result was empty.": "Correlation result was empty.",
45
"Create a graph of correlated items from resources in the current view.": "Create a graph of correlated items from resources in the curret view.",
56
"Distance": "Distance",
@@ -22,6 +23,7 @@
2223
"Return only results more recent than the specified duration.": "Return only results more recent than the specified duration.",
2324
"Search Type": "Search Type",
2425
"Selects the starting point for correlation search. This query is set automatically by the <1>Focus</1> button. You can edit it manually to specify a custom query.": "Selects the starting point for correlation search. This query is set automatically by the <1>Focus</1> button. You can edit it manually to specify a custom query.",
26+
"Since": "Since",
2527
"The current view does not support correlation.": "The current view does not support correlation.",
2628
"Time": "Time",
2729
"to": "to",

web/src/__tests__/all-domains.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ it.each([
7070
query:
7171
'log:infrastructure:{kubernetes_namespace_name="openshift-image-registry"}|json|kubernetes_labels_docker_registry="default"',
7272
},
73-
])('convert URL<=>link', ({ url, query, constraint }) => {
73+
])('round trip ${url} <=> ${query} ${constraint}', ({ url, query, constraint }) => {
7474
const d = new Domains(...allDomains);
7575
expect(d.linkToQuery(new URIRef(url))).toEqual(Query.parse(query));
7676
expect(d.queryToLink(Query.parse(query), Constraint.fromAPI(constraint))).toEqual(

web/src/__tests__/k8s.spec.ts

Lines changed: 35 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,13 @@ beforeAll(() => {
2727
path: 'events',
2828
verbs: ['watch'],
2929
},
30+
{
31+
kind: 'Event',
32+
apiVersion: 'v1',
33+
apiGroup: 'events.k8s.io',
34+
path: 'events',
35+
verbs: ['watch'],
36+
},
3037
{
3138
kind: 'Namespace',
3239
apiVersion: 'v1',
@@ -65,25 +72,37 @@ beforeAll(() => {
6572
window['SERVER_FLAGS'] = { consoleVersion: 'x.y.z' };
6673
});
6774

68-
describe('K8sNode.fromURL', () => {
75+
describe('K8sDomain.linkToQuery', () => {
6976
it.each([
70-
{ url: 'k8s/all-namespaces/apps~v1~Deployment', query: 'k8s:Deployment.v1.apps:{}' },
77+
{ url: '/k8s/all-namespaces/apps~v1~Deployment', query: 'k8s:Deployment.v1.apps:{}' },
78+
{ url: '/search/all-namespaces/apps~v1~Deployment', query: 'k8s:Deployment.v1.apps:{}' },
79+
{ url: '/api-resource/all-namespaces/apps~v1~Deployment', query: 'k8s:Deployment.v1.apps:{}' },
7180
{
7281
url: 'k8s/ns/korrel8r/apps~v1~Deployment',
7382
query: 'k8s:Deployment.v1.apps:{"namespace":"korrel8r"}',
7483
},
84+
85+
{ url: '/k8s/ns/x/apps~v1~Deployment', query: 'k8s:Deployment.v1.apps:{"namespace":"x"}' },
86+
{ url: '/search/ns/x/apps~v1~Deployment', query: 'k8s:Deployment.v1.apps:{"namespace":"x"}' },
7587
{
76-
url: 'k8s/ns/korrel8r/deployments/korrel8r',
77-
query: 'k8s:Deployment.v1.apps:{"namespace":"korrel8r","name":"korrel8r"}',
88+
url: '/api-resource/ns/x/apps~v1~Deployment',
89+
query: 'k8s:Deployment.v1.apps:{"namespace":"x"}',
7890
},
7991
{
80-
url: 'k8s/ns/default/core~v1~Pod/bad-deployment-000000000-00000',
92+
url: '/api-resource/ns/x/apps~v1~Deployment/instances',
93+
query: 'k8s:Deployment.v1.apps:{"namespace":"x"}',
94+
},
95+
{
96+
url: '/k8s/ns/x/deployments/y',
97+
query: 'k8s:Deployment.v1.apps:{"namespace":"x","name":"y"}',
98+
},
99+
{
100+
url: '/k8s/ns/default/core~v1~Pod/bad-deployment-000000000-00000',
81101
query: 'k8s:Pod.v1:{"namespace":"default","name":"bad-deployment-000000000-00000"}',
82102
},
83103
{
84-
url: 'k8s/ns/default/core~v1~Pod/bad-deployment-000000000-00000/events',
85-
query:
86-
'k8s:Event.v1:{"fields":{"involvedObject.namespace":"default","involvedObject.name":"bad-deployment-000000000-00000","involvedObject.apiVersion":"v1","involvedObject.kind":"Pod"}}',
104+
url: '/k8s/ns/default/core~v1~Pod/bad-deployment-000000000-00000/events',
105+
query: 'k8s:Pod.v1:{"namespace":"default","name":"bad-deployment-000000000-00000"}',
87106
},
88107
{
89108
url: `/k8s/ns/default/core~v1~Pod/foo`,
@@ -98,13 +117,14 @@ describe('K8sNode.fromURL', () => {
98117
{ url: `/search/all-namespaces?kind=core~v1~Pod`, query: `k8s:Pod.v1:{}` },
99118
{ url: `/k8s/all-namespaces/core~v1~Pod`, query: `k8s:Pod.v1:{}` },
100119
{ url: `/k8s/cluster/nodes/oscar7`, query: `k8s:Node.v1:{"name":"oscar7"}` },
120+
{ url: `/api-resource/cluster/core~v1~Node`, query: `k8s:Node.v1:{}` },
101121
{ url: '/k8s/ns/netobserv/core~v1~Pod', query: 'k8s:Pod.v1:{"namespace":"netobserv"}' },
102122
])('converts $url', ({ url, query }) =>
103123
expect(k8s.linkToQuery(new URIRef(url))).toEqual(Query.parse(query)),
104124
);
105125
});
106126

107-
describe('K8sNode.fromQuery', () => {
127+
describe('K8sDomain.queryToLink', () => {
108128
it.each([
109129
// Variations on query parameters.
110130
// Note "fields" are ignored.
@@ -123,6 +143,11 @@ describe('K8sNode.fromQuery', () => {
123143
'k8s:Event.v1:{"fields":{"involvedObject.namespace":"default","involvedObject.name":"bad-deployment-000000000-00000","involvedObject.apiVersion":"v1","involvedObject.kind":"Pod"}}',
124144
url: 'k8s/ns/default/core~v1~Pod/bad-deployment-000000000-00000/events',
125145
},
146+
{
147+
query:
148+
'k8s:Event.v1.events.k8s.io:{"fields":{"regarding.namespace":"default","regarding.name":"bad-deployment-000000000-00000","regarding.apiVersion":"v1","regarding.kind":"Pod"}}',
149+
url: 'k8s/ns/default/core~v1~Pod/bad-deployment-000000000-00000/events',
150+
},
126151
{
127152
query: `k8s:Pod:{ "namespace":"x", "name":"y", "labels":{ "a":"b" } }`,
128153
url: `k8s/ns/x/core~v1~Pod/y?labels=${encodeURIComponent('a=b')}`,
@@ -149,7 +174,7 @@ describe('K8sNode.fromQuery', () => {
149174
},
150175
{ query: `k8s:Pod:{}`, url: 'k8s/all-namespaces/core~v1~Pod' },
151176
])('converts $query to $url', ({ url, query }) => {
152-
expect(k8s.queryToLink(Query.parse(query))).toEqual(new URIRef(url));
177+
expect(k8s.queryToLink(Query.parse(query)).toString()).toEqual(new URIRef(url).toString());
153178
});
154179

155180
it.each([{ query: `foo:bar:baz` }])('raises error on $query', ({ query }) =>

web/src/__tests__/log.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { LogDomain } from '../korrel8r/log';
22
import { Constraint, Query, URIRef } from '../korrel8r/types';
33

4-
describe('LogDomain.fromURL', () => {
4+
describe('LogDomain.linkToQuery', () => {
55
it.each([
66
{
77
url: `monitoring/logs?q=${encodeURIComponent(
@@ -52,7 +52,7 @@ describe('LogDomain.fromURL', () => {
5252
);
5353
});
5454

55-
describe('LogDomain.fromQuery', () => {
55+
describe('LogDomain.queryToLink', () => {
5656
it.each([
5757
{
5858
url: `monitoring/logs?q=${encodeURIComponent(

web/src/korrel8r/k8s.ts

Lines changed: 58 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -18,103 +18,90 @@ type Selector = {
1818
fields?: { [key: string]: string };
1919
};
2020

21-
const PATH_RE = new RegExp(
22-
'(?:k8s|search)/(?:(?:ns/([^/]+))|cluster|all-namespaces)(?:/([^/]+)(?:/([^/]+))?)?(/events)?/?$',
21+
const pathRE = new RegExp(
22+
'(?<prefix>k8s|search|api-resource)' +
23+
'/((ns/(?<namespace>[^/]+))|cluster|all-namespaces)' +
24+
'(/(?<resource>[^/]+)(/(?<name>([^/]+))(?<events>/events)?)?)?',
2325
);
24-
const VERSION_RE = '(v[0-9]+(?:(?:alpha|beta)[0-9]*)?)';
25-
const CLASS_RE = new RegExp(`^([^./]+)(?:.${VERSION_RE})?(?:.([^/]*))?$`);
26+
const versionRE = /(?<version>v[0-9]+((alpha|beta)[0-9]*)?)/;
27+
const classRE = new RegExp(`^(?<kind>[^./]+)(\\.${versionRE.source})?(.(?<group>[^/]*))?$`);
2628

2729
export class K8sDomain extends Domain {
2830
constructor() {
2931
super('k8s');
3032
}
3133

3234
class(name: string): Class {
33-
const m = name.match(CLASS_RE);
34-
if (!m) throw this.badClass(name);
35-
const model = findGVK(m[3], m[2], m[1]);
35+
const model = this.classModel(name);
3636
if (!model) throw this.badClass(name);
3737
return this.modelClass(model);
3838
}
3939

40+
private classModel(name: string): Model | undefined {
41+
const g = name.match(classRE)?.groups;
42+
if (!g) return;
43+
return findGVK(g.group, g.version, g.kind);
44+
}
45+
4046
private modelClass(model: Model): Class {
4147
const version = model.apiVersion || 'v1';
4248
const dotGroup = model.apiGroup ? `.${model.apiGroup}` : '';
4349
return new Class(this.name, `${model.kind}.${version}${dotGroup}`);
4450
}
4551

4652
linkToQuery(link: URIRef): Query {
47-
const m = link.pathname.match(PATH_RE);
48-
if (!m) throw this.badLink(link);
49-
const [, namespace, resource, name, events] = m;
50-
const model = findResource(resource, link.searchParams.get('kind'));
51-
if (!model || !model.kind) throw this.badLink(link, `unknown resource "${resource}"`);
52-
if (events) {
53-
const event = eventModel();
54-
const about = eventAboutField(event);
55-
const apiVersion = `${model.apiGroup ? `${model.apiGroup}/` : ''}${model.apiVersion || 'v1'}`;
56-
const data = {
57-
fields: {
58-
[`${about}.namespace`]: namespace,
59-
[`${about}.name`]: name,
60-
[`${about}.apiVersion`]: apiVersion,
61-
[`${about}.kind`]: model.kind,
62-
},
63-
};
64-
return this.modelClass(event).query(JSON.stringify(data));
65-
} else {
66-
const data = {
67-
namespace: namespace,
68-
name: name,
69-
labels: K8sDomain.parseSelector(link.searchParams.get('labels')) || undefined,
70-
};
71-
return this.modelClass(model).query(JSON.stringify(data));
72-
}
53+
const g = link.pathname.match(pathRE)?.groups;
54+
if (!g) throw this.badLink(link);
55+
const resource = g.resource || link.searchParams.get('kind');
56+
const model = findResource(resource);
57+
if (!model?.kind) throw this.badLink(link, `unknown resource: ${resource}`);
58+
const name = g.prefix === 'api-resource' ? undefined : g.name;
59+
const data = {
60+
namespace: g.namespace,
61+
name,
62+
labels: K8sDomain.parseSelector(link.searchParams.get('labels')) || undefined,
63+
};
64+
return this.modelClass(model).query(JSON.stringify(data));
7365
}
7466

7567
// NOTE: k8s queries don't support query constraints, so neither do console k8s URIs.
7668
queryToLink(query: Query): URIRef {
77-
let data: Selector;
69+
let selector: Selector;
7870
try {
79-
data = JSON.parse(query.selector) as Selector;
71+
selector = JSON.parse(query.selector) as Selector;
8072
} catch (e) {
8173
throw this.badQuery(query, e.message);
8274
}
83-
const m = query.class.name.match(/^([^.]+)(?:\.([^.]*)(?:\.(.*))?)?$/) ?? [];
84-
if (!m) throw this.badQuery(query, 'incorrect format');
85-
let model = findGVK(m[3], m[2], m[1]);
86-
if (!model) throw this.badQuery(query, 'no matching resource');
87-
let namespace = data.namespace;
88-
let name = data.name;
75+
let model = this.classModel(query.class.name);
76+
if (!model) throw this.badQuery(query, `no resources match class`);
77+
let namespace = selector.namespace;
78+
let name = selector.name;
8979
let events = '';
9080
if (isEvent(model)) {
91-
// Special treatment for event objects: focus on the involved object, not the event.
92-
events = '/events';
81+
// Special case for events, use involved object with '/events' modifier.
82+
const eventClass = this.modelClass(model);
9383
const about = eventAboutField(model);
94-
const [group, version] = parseAPIVersion(data.fields[`${about}.apiVersion`]);
95-
const kind = data.fields[`${about}.kind`];
84+
const [group, version] = parseAPIVersion(selector.fields[`${about}.apiVersion`]);
85+
const kind = selector.fields[`${about}.kind`];
9686
model = findGVK(group, version, kind);
97-
if (!model)
98-
throw this.badQuery(
99-
query,
100-
`no resource for group=${group} version=${version} kind=${kind}`,
101-
);
102-
namespace = data.fields[`${about}.namespace`] || '';
103-
name = data.fields[`${about}.name`] || '';
87+
if (!model) throw this.badQuery(query, `no resource matching ${eventClass}.${about}`);
88+
namespace = selector.fields[`${about}.namespace`] || '';
89+
name = selector.fields[`${about}.name`] || '';
90+
events = '/events';
10491
}
10592
// Prepare parts of the URL
10693
const nsPath = namespace ? `ns/${namespace}` : 'all-namespaces';
10794
const kind = `${model.apiGroup || 'core'}~${model.apiVersion}~${model.kind}`;
10895
const params = {
109-
labels: keyValueList(data.labels) || undefined,
110-
fields: (!events && keyValueList(data.fields)) || undefined,
96+
labels: keyValueList(selector.labels) || undefined,
97+
fields: (!events && keyValueList(selector.fields)) || undefined,
11198
};
11299
if (!name && !namespace && (params.labels || params.fields)) {
113100
// Search URL
114101
return new URIRef(`search/${nsPath}`, { ...params, kind });
115102
} else {
116103
// Specific resource URL
117-
return new URIRef(`k8s/${nsPath}/${kind}${name ? `/${name}` : ''}${events}`, params);
104+
return new URIRef(`k8s/${nsPath}/${kind}${name ? `/${name}` : ''}${events}`, { ...params });
118105
}
119106
}
120107

@@ -146,54 +133,42 @@ function isEvent(m: Model): boolean {
146133
(!m.apiGroup || m.apiGroup === EVENT.group)
147134
);
148135
}
149-
function eventModel(): Model {
150-
return findGVK(EVENT.group, EVENT.version, EVENT.kind) || findGVK('', EVENT.version, EVENT.kind);
151-
}
152136

153137
function eventAboutField(m: Model): string {
154138
return m?.apiGroup === EVENT.group ? 'regarding' : 'involvedObject';
155139
}
156140

157-
// Find the cached resource model for a GVK. Same defaulting rules as korrel8r..
141+
// Find the cached resource model for a GVK. Same defaulting rules as korrel8r.
158142
function findGVK(group: string, version: string, kind: string): Model {
159143
version = version || 'v1';
160144
group = group || '';
161-
return getCachedResources()?.models.find((m: Model) => {
162-
return (
145+
return getCachedResources()?.models.find(
146+
(m: Model) =>
163147
m.kind === kind &&
164148
m.verbs.includes('watch') &&
165149
m.apiVersion === version &&
166-
(m.apiGroup || '') === group
167-
);
168-
});
150+
(m.apiGroup || '') === group,
151+
);
169152
}
170153

171-
// Return a model for the resource (path) or undefined
172-
function findResource(resource: string, kind: string): Model {
154+
// Return a model for the resource, can be G~V~K or path. Return undefined if not found.
155+
function findResource(resource: string): Model {
156+
if (!resource) return;
157+
// Try as a G~V~K string.
158+
const [g, v, k] = resource.split('~');
159+
if (k) return findGVK(g === 'core' ? '' : g, v, k);
160+
// Try as a resource path
173161
if (resource === 'projects') resource = 'namespaces'; // Alias
174-
if (resource && !resource.includes('~')) {
175-
// Try the resource as a straight resource name
176-
const model = getCachedResources()?.models.find(
177-
(m: Model) => m.path === resource && m.verbs.includes('watch'),
178-
);
179-
if (model) return model;
180-
}
181-
// Either kind or resource may be a g~v~k string.
182-
const parts = (resource || kind)?.split('~');
183-
if (!parts || parts.length !== 3 || !parts[2]) return;
184-
return findGVK(
185-
!parts[0] || parts[0] === 'core' ? undefined : parts[0],
186-
parts[1] || undefined,
187-
parts[2],
162+
return getCachedResources()?.models?.find(
163+
(m: Model) => m.path === resource && m.verbs.includes('watch'),
188164
);
189165
}
190166

191-
const VERSION_ONLY_RE = new RegExp(`^${VERSION_RE}$`);
192167
function parseAPIVersion(apiVersion: string): [group: string, version: string] | undefined {
193-
const gv = apiVersion.split('/') || [];
168+
const gv = apiVersion?.split('/') || [];
194169
switch (gv.length) {
195170
case 1:
196-
return gv[0].match(VERSION_ONLY_RE) ? ['', gv[0]] : [gv[0], ''];
171+
return gv[0].match(versionRE) ? ['', gv[0]] : [gv[0], ''];
197172
case 2:
198173
return [gv[0], gv[1]];
199174
default:

0 commit comments

Comments
 (0)