Skip to content

Commit

Permalink
frontend: Websocket backward compatibility
Browse files Browse the repository at this point in the history
This adds a new way to use new way to run websocket multiplexer. Default
way would be the legacy way which creates multiple websocket connection.
This adds a new flag `REACT_APP_ENABLE_WEBSOCKET_MULTIPLEXER` to run the
new API.

Signed-off-by: Kautilya Tripathi <[email protected]>
  • Loading branch information
knrt10 committed Nov 25, 2024
1 parent 7e8babd commit ceca8d2
Show file tree
Hide file tree
Showing 7 changed files with 319 additions and 34 deletions.
1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@
"build": "cross-env PUBLIC_URL=./ NODE_OPTIONS=--max-old-space-size=8096 vite build && npx shx rm -f build/frontend/index.baseUrl.html",
"pretest": "npm run make-version",
"test": "vitest",
"multiplexer": "cross-env REACT_APP_ENABLE_WEBSOCKET_MULTIPLEXER=true npm run start",
"lint": "eslint --cache -c package.json --ext .js,.ts,.tsx src/ ../app/electron ../plugins/headlamp-plugin --ignore-pattern ../plugins/headlamp-plugin/template --ignore-pattern ../plugins/headlamp-plugin/lib/",
"format": "prettier --config package.json --write --cache src ../app/electron ../app/tsconfig.json ../app/scripts ../plugins/headlamp-plugin/bin ../plugins/headlamp-plugin/config ../plugins/headlamp-plugin/template ../plugins/headlamp-plugin/test*.js ../plugins/headlamp-plugin/*.json ../plugins/headlamp-plugin/*.js",
"format-check": "prettier --config package.json --check --cache src ../app/electron ../app/tsconfig.json ../app/scripts ../plugins/headlamp-plugin/bin ../plugins/headlamp-plugin/config ../plugins/headlamp-plugin/template ../plugins/headlamp-plugin/test*.js ../plugins/headlamp-plugin/*.json ../plugins/headlamp-plugin/*.js",
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/common/Resource/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ function getFilesToVerify() {
const filesToVerify: string[] = [];
fs.readdirSync(__dirname).forEach(file => {
const fileNoSuffix = file.replace(/\.[^/.]+$/, '');
if (!avoidCheck.find(suffix => fileNoSuffix.endsWith(suffix))) {
if (fileNoSuffix && !avoidCheck.find(suffix => fileNoSuffix.endsWith(suffix))) {
filesToVerify.push(fileNoSuffix);
}
});
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/common/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ function getFilesToVerify() {
const filesToVerify: string[] = [];
fs.readdirSync(__dirname).forEach(file => {
const fileNoSuffix = file.replace(/\.[^/.]+$/, '');
if (!avoidCheck.find(suffix => fileNoSuffix.endsWith(suffix))) {
if (fileNoSuffix && !avoidCheck.find(suffix => fileNoSuffix.endsWith(suffix))) {
filesToVerify.push(fileNoSuffix);
}
});
Expand Down
9 changes: 9 additions & 0 deletions frontend/src/helpers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,14 @@ function loadTableSettings(tableId: string): { id: string; show: boolean }[] {
return settings;
}

/**
* @returns true if the websocket multiplexer is enabled.
* defaults to false
*/
export function getWebsocketMultiplexerStatus(): boolean {
return import.meta.env.REACT_APP_ENABLE_WEBSOCKET_MULTIPLEXER === 'true';
}

/**
* The backend token to use when making API calls from Headlamp when running as an app.
* The app opens the index.html?backendToken=... and passes the token to the frontend
Expand Down Expand Up @@ -393,6 +401,7 @@ const exportFunctions = {
storeClusterSettings,
loadClusterSettings,
getHeadlampAPIHeaders,
getWebsocketMultiplexerStatus,
storeTableSettings,
loadTableSettings,
};
Expand Down
108 changes: 104 additions & 4 deletions frontend/src/lib/k8s/api/v2/useKubeObjectList.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { QueryObserverOptions, useQueries, useQueryClient } from '@tanstack/react-query';
import { useEffect, useMemo, useRef, useState } from 'react';
import { getWebsocketMultiplexerStatus } from '../../../../helpers';
import { KubeObject, KubeObjectClass } from '../../KubeObject';
import { ApiError } from '../v1/clusterRequests';
import { QueryParameters } from '../v1/queryParameters';
import { clusterFetch } from './fetch';
import { QueryListResponse, useEndpoints } from './hooks';
import { KubeList } from './KubeList';
import { KubeList, KubeListUpdateEvent } from './KubeList';
import { KubeObjectEndpoint } from './KubeObjectEndpoint';
import { makeUrl } from './makeUrl';
import { BASE_WS_URL, WebSocketManager } from './webSocket';
import { BASE_WS_URL, useWebSockets, WebSocketManager } from './webSocket';

/**
* Object representing a List of Kube object
Expand Down Expand Up @@ -111,6 +112,44 @@ export function useWatchKubeObjectLists<K extends KubeObject>({
endpoint?: KubeObjectEndpoint | null;
/** Which clusters and namespaces to watch */
lists: Array<{ cluster: string; namespace?: string; resourceVersion: string }>;
}) {
const websocketMultiplexerStatus = getWebsocketMultiplexerStatus();

if (websocketMultiplexerStatus) {
return useWatchKubeObjectListsMultiplexed({
kubeObjectClass,
endpoint,
lists,
queryParams,
});
} else {
return useWatchKubeObjectListsLegacy({
kubeObjectClass,
endpoint,
lists,
queryParams,
});
}
}

/**
* Accepts a list of lists to watch.
* Upon receiving update it will modify query data for list query
* @param kubeObjectClass - KubeObject class of the watched resource list
* @param endpoint - Kube resource API endpoint information
* @param lists - Which clusters and namespaces to watch
* @param queryParams - Query parameters for the WebSocket connection URL
*/
function useWatchKubeObjectListsMultiplexed<K extends KubeObject>({
kubeObjectClass,
endpoint,
lists,
queryParams,
}: {
kubeObjectClass: (new (...args: any) => K) & typeof KubeObject<any>;
endpoint?: KubeObjectEndpoint | null;
lists: Array<{ cluster: string; namespace?: string; resourceVersion: string }>;
queryParams?: QueryParameters;
}) {
const client = useQueryClient();
const latestResourceVersions = useRef<Record<string, string>>({});
Expand All @@ -121,7 +160,6 @@ export function useWatchKubeObjectLists<K extends KubeObject>({

return lists.map(list => {
const key = `${list.cluster}:${list.namespace || ''}`;
// Only update resourceVersion if it's newer
if (
!latestResourceVersions.current[key] ||
parseInt(list.resourceVersion) > parseInt(latestResourceVersions.current[key])
Expand Down Expand Up @@ -153,7 +191,6 @@ export function useWatchKubeObjectLists<K extends KubeObject>({
WebSocketManager.subscribe(cluster, parsedUrl.pathname, parsedUrl.search.slice(1), update => {
if (!update || typeof update !== 'object') return;

// Update latest resourceVersion
if (update.object?.metadata?.resourceVersion) {
latestResourceVersions.current[key] = update.object.metadata.resourceVersion;
}
Expand Down Expand Up @@ -184,6 +221,69 @@ export function useWatchKubeObjectLists<K extends KubeObject>({
}, [connections, endpoint, client, kubeObjectClass, queryParams]);
}

/**
* Accepts a list of lists to watch.
* Upon receiving update it will modify query data for list query
* @param kubeObjectClass - KubeObject class of the watched resource list
* @param endpoint - Kube resource API endpoint information
* @param lists - Which clusters and namespaces to watch
* @param queryParams - Query parameters for the WebSocket connection URL
*/
function useWatchKubeObjectListsLegacy<K extends KubeObject>({
kubeObjectClass,
endpoint,
lists,
queryParams,
}: {
/** KubeObject class of the watched resource list */
kubeObjectClass: (new (...args: any) => K) & typeof KubeObject<any>;
/** Query parameters for the WebSocket connection URL */
queryParams?: QueryParameters;
/** Kube resource API endpoint information */
endpoint?: KubeObjectEndpoint | null;
/** Which clusters and namespaces to watch */
lists: Array<{ cluster: string; namespace?: string; resourceVersion: string }>;
}) {
const client = useQueryClient();

const connections = useMemo(() => {
if (!endpoint) return [];

return lists.map(({ cluster, namespace, resourceVersion }) => {
const url = makeUrl([KubeObjectEndpoint.toUrl(endpoint!, namespace)], {
...queryParams,
watch: 1,
resourceVersion,
});

return {
cluster,
url,
onMessage(update: KubeListUpdateEvent<K>) {
const key = kubeObjectListQuery<K>(
kubeObjectClass,
endpoint,
namespace,
cluster,
queryParams ?? {}
).queryKey;
client.setQueryData(key, (oldResponse: ListResponse<any> | undefined | null) => {
if (!oldResponse) return oldResponse;

const newList = KubeList.applyUpdate(oldResponse.list, update, kubeObjectClass);
return { ...oldResponse, list: newList };
});
},
};
});
}, [lists, kubeObjectClass, endpoint]);

useWebSockets<KubeListUpdateEvent<K>>({
enabled: !!endpoint,
connections,
});
}

/**
* Creates multiple requests to list Kube objects
* Handles multiple clusters, namespaces and allowed namespaces
Expand Down
Loading

0 comments on commit ceca8d2

Please sign in to comment.