Skip to content

Commit 2596766

Browse files
authored
Merge pull request #2486 from headlamp-k8s/use-lists-queries
frontend: Use multiple queries in useKubeObjectList and add support for allowedNamespaces
2 parents cba5d81 + 6b4d7b8 commit 2596766

9 files changed

+581
-284
lines changed

frontend/src/helpers/testHelpers.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useKubeObjectList } from '../lib/k8s/api/v2/hooks';
1+
import { KubeObject } from '../lib/k8s/KubeObject';
22

33
export const useMockListQuery = {
44
noData: () =>
@@ -10,7 +10,7 @@ export const useMockListQuery = {
1010
yield null;
1111
yield null;
1212
},
13-
} as any as typeof useKubeObjectList),
13+
} as any as typeof KubeObject.useList),
1414
error: () =>
1515
({
1616
data: null,
@@ -20,7 +20,7 @@ export const useMockListQuery = {
2020
yield null;
2121
yield 'Phony error is phony!';
2222
},
23-
} as any as typeof useKubeObjectList),
23+
} as any as typeof KubeObject.useList),
2424
data: (items: any[]) =>
2525
(() => ({
2626
data: { kind: 'List', items },
@@ -30,5 +30,5 @@ export const useMockListQuery = {
3030
yield items;
3131
yield null;
3232
},
33-
})) as any as typeof useKubeObjectList,
33+
})) as any as typeof KubeObject.useList,
3434
};

frontend/src/lib/k8s/KubeObject.ts

+40-16
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,24 @@
11
import { OpPatch } from 'json-patch';
22
import { JSONPath } from 'jsonpath-plus';
33
import { cloneDeep, unset } from 'lodash';
4-
import React from 'react';
5-
import helpers from '../../helpers';
4+
import React, { useMemo } from 'react';
5+
import exportFunctions from '../../helpers';
66
import { getCluster } from '../cluster';
77
import { createRouteURL } from '../router';
88
import { timeAgo } from '../util';
99
import { useClusterGroup, useConnectApi } from '.';
10-
import { useKubeObject, useKubeObjectList } from './api/v2/hooks';
10+
import { useKubeObject } from './api/v2/hooks';
11+
import { makeListRequests, useKubeObjectList } from './api/v2/useKubeObjectList';
1112
import { ApiError, apiFactory, apiFactoryWithNamespace, post, QueryParameters } from './apiProxy';
1213
import { KubeEvent } from './event';
1314
import { KubeMetadata } from './KubeMetadata';
1415

15-
function getAllowedNamespaces() {
16-
const cluster = getCluster();
16+
function getAllowedNamespaces(cluster: string | null = getCluster()): string[] {
1717
if (!cluster) {
1818
return [];
1919
}
2020

21-
const clusterSettings = helpers.loadClusterSettings(cluster);
21+
const clusterSettings = exportFunctions.loadClusterSettings(cluster);
2222
return clusterSettings.allowedNamespaces || [];
2323
}
2424

@@ -286,24 +286,48 @@ export class KubeObject<T extends KubeObjectInterface | KubeEvent = any> {
286286
}
287287

288288
static useList<K extends KubeObject>(
289-
this: new (...args: any) => K,
289+
this: (new (...args: any) => K) & typeof KubeObject<any>,
290290
{
291291
cluster,
292292
clusters,
293293
namespace,
294294
...queryParams
295-
}: { cluster?: string; namespace?: string; clusters?: string[] } & QueryParameters = {}
295+
}: {
296+
cluster?: string;
297+
clusters?: string[];
298+
namespace?: string | string[];
299+
} & QueryParameters = {}
296300
) {
297-
const clusterGroup = useClusterGroup();
298-
const theClusters = clusters || clusterGroup;
299-
300-
return useKubeObjectList<K>({
301+
const fallbackClusters = useClusterGroup();
302+
303+
// Create requests for each cluster and namespace
304+
const requests = useMemo(() => {
305+
const clusterList = cluster
306+
? [cluster]
307+
: clusters || (fallbackClusters.length === 0 ? [''] : fallbackClusters);
308+
309+
const namespacesFromParams =
310+
typeof namespace === 'string'
311+
? [namespace]
312+
: Array.isArray(namespace)
313+
? namespace
314+
: undefined;
315+
316+
return makeListRequests(
317+
clusterList,
318+
getAllowedNamespaces,
319+
this.isNamespaced,
320+
namespacesFromParams
321+
);
322+
}, [cluster, clusters, fallbackClusters, namespace, this.isNamespaced]);
323+
324+
const result = useKubeObjectList<K>({
301325
queryParams: queryParams,
302-
kubeObjectClass: this as (new (...args: any) => K) & typeof KubeObject<any>,
303-
clusters: theClusters,
304-
cluster: cluster,
305-
namespace: namespace,
326+
kubeObjectClass: this,
327+
requests,
306328
});
329+
330+
return result;
307331
}
308332

309333
static useGet<K extends KubeObject>(

frontend/src/lib/k8s/api/v2/hooks.ts

+15-214
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { getCluster } from '../../../cluster';
44
import { ApiError, QueryParameters } from '../../apiProxy';
55
import { KubeObject, KubeObjectInterface } from '../../KubeObject';
66
import { clusterFetch } from './fetch';
7-
import { KubeList, KubeListUpdateEvent } from './KubeList';
7+
import { KubeListUpdateEvent } from './KubeList';
88
import { KubeObjectEndpoint } from './KubeObjectEndpoint';
99
import { makeUrl } from './makeUrl';
1010
import { useWebSocket } from './webSocket';
@@ -159,9 +159,13 @@ export function useKubeObject<K extends KubeObject>({
159159
* @throws Error
160160
* When no endpoints are working
161161
*/
162-
const getWorkingEndpoint = async (endpoints: KubeObjectEndpoint[], cluster: string) => {
162+
const getWorkingEndpoint = async (
163+
endpoints: KubeObjectEndpoint[],
164+
cluster: string,
165+
namespace?: string
166+
) => {
163167
const promises = endpoints.map(endpoint => {
164-
return clusterFetch(KubeObjectEndpoint.toUrl(endpoint), {
168+
return clusterFetch(KubeObjectEndpoint.toUrl(endpoint, namespace), {
165169
method: 'GET',
166170
cluster: cluster ?? getCluster() ?? '',
167171
}).then(it => {
@@ -179,225 +183,22 @@ const getWorkingEndpoint = async (endpoints: KubeObjectEndpoint[], cluster: stri
179183
*
180184
* @params endpoints - List of possible endpoints
181185
*/
182-
const useEndpoints = (endpoints: KubeObjectEndpoint[], cluster: string) => {
186+
export const useEndpoints = (
187+
endpoints: KubeObjectEndpoint[],
188+
cluster?: string,
189+
namespace?: string
190+
) => {
183191
const { data: endpoint } = useQuery({
184-
enabled: endpoints.length > 1,
192+
enabled: endpoints.length > 1 && cluster !== undefined,
185193
queryKey: ['endpoints', endpoints],
186194
queryFn: () =>
187-
getWorkingEndpoint(endpoints, cluster)
195+
getWorkingEndpoint(endpoints, cluster!, namespace)
188196
.then(endpoints => endpoints)
189197
.catch(() => null),
190198
});
191199

200+
if (cluster === null || cluster === undefined) return undefined;
192201
if (endpoints.length === 1) return endpoints[0];
193202

194203
return endpoint;
195204
};
196-
197-
/**
198-
* Returns a list of Kubernetes objects and watches for changes
199-
*
200-
* @private please use useKubeObjectList.
201-
*/
202-
function _useKubeObjectList<K extends KubeObject>({
203-
kubeObjectClass,
204-
namespace,
205-
cluster: maybeCluster,
206-
queryParams,
207-
}: {
208-
/** Class to instantiate the object with */
209-
kubeObjectClass: (new (...args: any) => K) & typeof KubeObject<any>;
210-
/** Object list namespace */
211-
namespace?: string;
212-
/** Object list cluster */
213-
cluster?: string;
214-
queryParams?: QueryParameters;
215-
}): [Array<K> | null, ApiError | null] & QueryListResponse<KubeList<K>, K, ApiError> {
216-
const cluster = maybeCluster ?? getCluster() ?? '';
217-
const endpoint = useEndpoints(kubeObjectClass.apiEndpoint.apiInfo, cluster);
218-
219-
const cleanedUpQueryParams = Object.fromEntries(
220-
Object.entries(queryParams ?? {}).filter(([, value]) => value !== undefined && value !== '')
221-
);
222-
223-
const queryKey = useMemo(
224-
() => ['list', cluster, endpoint, namespace ?? '', cleanedUpQueryParams],
225-
[endpoint, namespace, cleanedUpQueryParams]
226-
);
227-
228-
const client = useQueryClient();
229-
const query = useQuery<KubeList<any> | null | undefined, ApiError>({
230-
enabled: !!endpoint,
231-
placeholderData: null,
232-
queryKey,
233-
queryFn: async () => {
234-
if (!endpoint) return;
235-
const list: KubeList<any> = await clusterFetch(
236-
makeUrl([KubeObjectEndpoint.toUrl(endpoint!, namespace)], cleanedUpQueryParams),
237-
{
238-
cluster,
239-
}
240-
).then(it => it.json());
241-
list.items = list.items.map(item => {
242-
const itm = new kubeObjectClass({ ...item, kind: list.kind.replace('List', '') });
243-
itm.cluster = cluster;
244-
return itm;
245-
});
246-
247-
return list;
248-
},
249-
});
250-
251-
const items: Array<K> | null = query.error ? null : query.data?.items ?? null;
252-
const data: KubeList<K> | null = query.error ? null : query.data ?? null;
253-
254-
useWebSocket<KubeListUpdateEvent<K>>({
255-
url: () =>
256-
makeUrl([KubeObjectEndpoint.toUrl(endpoint!)], {
257-
...cleanedUpQueryParams,
258-
watch: 1,
259-
resourceVersion: data!.metadata.resourceVersion,
260-
}),
261-
cluster,
262-
enabled: !!endpoint && !!data,
263-
onMessage(update) {
264-
client.setQueryData(queryKey, (oldList: any) => {
265-
const newList = KubeList.applyUpdate(oldList, update, kubeObjectClass);
266-
return newList;
267-
});
268-
},
269-
});
270-
271-
// @ts-ignore
272-
return {
273-
items,
274-
data,
275-
error: query.error,
276-
isError: query.isError,
277-
isLoading: query.isLoading,
278-
isFetching: query.isFetching,
279-
isSuccess: query.isSuccess,
280-
status: query.status,
281-
*[Symbol.iterator](): ArrayIterator<ApiError | K[] | null> {
282-
yield items;
283-
yield query.error;
284-
},
285-
};
286-
}
287-
288-
/**
289-
* Returns a combined list of Kubernetes objects and watches for changes from the clusters given.
290-
*/
291-
export function useKubeObjectList<K extends KubeObject>({
292-
kubeObjectClass,
293-
namespace,
294-
cluster,
295-
clusters,
296-
queryParams,
297-
}: {
298-
/** Class to instantiate the object with */
299-
kubeObjectClass: (new (...args: any) => K) & typeof KubeObject<any>;
300-
/** Object list namespace */
301-
namespace?: string;
302-
cluster?: string;
303-
/** Object list clusters */
304-
clusters?: string[];
305-
queryParams?: QueryParameters;
306-
}): [Array<K> | null, ApiError | null] & QueryListResponse<KubeList<K>, K, ApiError> {
307-
if (clusters && clusters.length > 0) {
308-
return _useKubeObjectLists({
309-
kubeObjectClass,
310-
namespace,
311-
clusters: clusters,
312-
queryParams,
313-
});
314-
} else {
315-
return _useKubeObjectList({
316-
kubeObjectClass,
317-
namespace,
318-
cluster: cluster,
319-
queryParams,
320-
});
321-
}
322-
}
323-
324-
/**
325-
* Returns a combined list of Kubernetes objects and watches for changes from the clusters given.
326-
*
327-
* @private please use useKubeObjectList
328-
*/
329-
function _useKubeObjectLists<K extends KubeObject>({
330-
kubeObjectClass,
331-
namespace,
332-
clusters,
333-
queryParams,
334-
}: {
335-
/** Class to instantiate the object with */
336-
kubeObjectClass: (new (...args: any) => K) & typeof KubeObject<any>;
337-
/** Object list namespace */
338-
namespace?: string;
339-
/** Object list clusters */
340-
clusters: string[];
341-
queryParams?: QueryParameters;
342-
}): [Array<K> | null, ApiError | null] & QueryListResponse<KubeList<K>, K, ApiError> {
343-
const clusterResults: Record<string, ReturnType<typeof useKubeObjectList<K>>> = {};
344-
345-
for (const cluster of clusters) {
346-
clusterResults[cluster] = _useKubeObjectList({
347-
kubeObjectClass,
348-
namespace,
349-
cluster: cluster || undefined,
350-
queryParams,
351-
});
352-
}
353-
354-
let items = null;
355-
for (const cluster of clusters) {
356-
if (items === null) {
357-
items = clusterResults[cluster].items;
358-
} else {
359-
items = items.concat(clusterResults[cluster].items ?? []);
360-
}
361-
}
362-
363-
// data makes no sense really for multiple clusters, but useful for single cluster?
364-
const data =
365-
clusters.map(cluster => clusterResults[cluster].data).find(it => it !== null) ?? null;
366-
const error =
367-
clusters.map(cluster => clusterResults[cluster].error).find(it => it !== null) ?? null;
368-
const isError = clusters.some(cluster => clusterResults[cluster].isError);
369-
const isLoading = clusters.some(cluster => clusterResults[cluster].isLoading);
370-
const isFetching = clusters.some(cluster => clusterResults[cluster].isFetching);
371-
const isSuccess = clusters.every(cluster => clusterResults[cluster].isSuccess);
372-
// status makes no sense really for multiple clusters, but maybe useful for single cluster?
373-
const status =
374-
clusters.map(cluster => clusterResults[cluster].status).find(it => it !== null) ?? 'pending';
375-
376-
let clusterErrors: Record<string, ApiError | null> | null = {};
377-
clusters.forEach(cluster => {
378-
if (clusterErrors && clusterResults[cluster]?.error !== null) {
379-
clusterErrors[cluster] = clusterResults[cluster].error;
380-
}
381-
});
382-
if (Object.keys(clusterErrors).length === 0) {
383-
clusterErrors = null;
384-
}
385-
386-
// @ts-ignore
387-
return {
388-
items,
389-
data,
390-
error,
391-
isError,
392-
isLoading,
393-
isFetching,
394-
isSuccess,
395-
status,
396-
*[Symbol.iterator](): ArrayIterator<ApiError | K[] | null> {
397-
yield items;
398-
yield error;
399-
},
400-
clusterResults,
401-
clusterErrors,
402-
};
403-
}

frontend/src/lib/k8s/api/v2/makeUrl.test.ts

+6
Original file line numberDiff line numberDiff line change
@@ -51,4 +51,10 @@ describe('makeUrl', () => {
5151
const result = makeUrl(urlParts);
5252
expect(result).toBe('http://example.com/123/true/resource');
5353
});
54+
55+
it('should create a url from a single string', () => {
56+
expect(makeUrl('http://example.com/some/path', { watch: 1 })).toBe(
57+
'http://example.com/some/path?watch=1'
58+
);
59+
});
5460
});

0 commit comments

Comments
 (0)