Skip to content

Commit

Permalink
Store stateless clusters info in indexDB
Browse files Browse the repository at this point in the history
This removes initial storage of kubeconfig data from session storage to
indexDB. This change was made for maximum data retention since indexDB
has much more data storage then session storage. Also we are not storing
headlamp-userId in localstorage so that when a loggedin user open
another tab they are able to see their stateless clusters.

Signed-off-by: Kautilya Tripathi <[email protected]>
  • Loading branch information
knrt10 committed Nov 6, 2023
1 parent dbc6c93 commit f0acfdb
Show file tree
Hide file tree
Showing 5 changed files with 192 additions and 108 deletions.
1 change: 0 additions & 1 deletion backend/cmd/headlamp.go
Original file line number Diff line number Diff line change
Expand Up @@ -1149,7 +1149,6 @@ func handleClusterAPI(c *HeadlampConfig, router *mux.Router) { //nolint:funlen
}

// TODO: authentication add token to request and check.
// TODO: Store everything in indexDB rather then in session storage.

// Remove the "X-HEADLAMP-USER-ID" parameter from the websocket URL.
delete(queryParams, "X-HEADLAMP-USER-ID")
Expand Down
75 changes: 39 additions & 36 deletions frontend/src/components/App/Layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -207,45 +207,48 @@ export default function Layout({}: LayoutProps) {
* if the present stored config is different from the fetched one.
*/
const fetchStatelessConfig = () => {
const config = getStatelessClusterKubeConfigs();
const statelessClusters = store.getState().config.statelessClusters;
const JSON_HEADERS = { Accept: 'application/json', 'Content-Type': 'application/json' };
const clusterReq = {
kubeconfigs: config,
};
getStatelessClusterKubeConfigs(function (config) {
const statelessClusters = store.getState().config.statelessClusters;
const JSON_HEADERS = { Accept: 'application/json', 'Content-Type': 'application/json' };
const clusterReq = {
kubeconfigs: config,
};

// Parses statelessCluster config
request(
'/parseKubeConfig',
{
method: 'POST',
body: JSON.stringify(clusterReq),
headers: {
...JSON_HEADERS,
// Parses statelessCluster config
request(
'/parseKubeConfig',
{
method: 'POST',
body: JSON.stringify(clusterReq),
headers: {
...JSON_HEADERS,
},
},
},
false,
false
)
.then((config: Config) => {
const clustersToConfig: ConfigState['statelessClusters'] = {};
config?.clusters.forEach((cluster: Cluster) => {
clustersToConfig[cluster.name] = cluster;
});
false,
false
)
.then((config: Config) => {
const clustersToConfig: ConfigState['statelessClusters'] = {};
config?.clusters.forEach((cluster: Cluster) => {
clustersToConfig[cluster.name] = cluster;
});

const configToStore = {
...config,
statelessClusters: clustersToConfig,
};
if (statelessClusters === null) {
dispatch(setStatelessConfig({ ...configToStore }));
} else if (Object.keys(clustersToConfig).length !== Object.keys(statelessClusters).length) {
dispatch(setStatelessConfig({ ...configToStore }));
}
})
.catch((err: Error) => {
console.error('Error getting config:', err);
});
const configToStore = {
...config,
statelessClusters: clustersToConfig,
};
if (statelessClusters === null) {
dispatch(setStatelessConfig({ ...configToStore }));
} else if (
Object.keys(clustersToConfig).length !== Object.keys(statelessClusters).length
) {
dispatch(setStatelessConfig({ ...configToStore }));
}
})
.catch((err: Error) => {
console.error('Error getting config:', err);
});
});
};

return (
Expand Down
14 changes: 8 additions & 6 deletions frontend/src/lib/k8s/apiProxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -347,9 +347,9 @@ export async function clusterRequest(
let fullPath = path;
if (cluster) {
const token = getToken(cluster);
const matchingKubeconfig = findKubeconfigByClusterName(cluster);
if (matchingKubeconfig !== null) {
opts.headers['KUBECONFIG'] = matchingKubeconfig;
const kubeconfig = await findKubeconfigByClusterName(cluster);
if (kubeconfig !== null) {
opts.headers['KUBECONFIG'] = kubeconfig;
opts.headers['X-HEADLAMP-USER-ID'] = userID;
}

Expand Down Expand Up @@ -1177,8 +1177,10 @@ function connectStream(
if (cluster) {
fullPath = combinePath(`/${CLUSTERS_PREFIX}/${cluster}`, path);
// Include the userID as a query parameter if it's a stateless cluster
const matchingKubeconfig = findKubeconfigByClusterName(cluster);
if (matchingKubeconfig !== null) {
const kubeconfig = findKubeconfigByClusterName(cluster).then(kubeconfig => {
return kubeconfig;
});
if (kubeconfig !== null) {
const queryParams = `X-HEADLAMP-USER-ID=${userID}`;
url = combinePath(BASE_WS_URL, fullPath) + (fullPath.includes('?') ? '&' : '?') + queryParams;
} else {
Expand Down Expand Up @@ -1370,7 +1372,7 @@ export async function setCluster(clusterReq: ClusterRequest) {
const kubeconfig = clusterReq.kubeconfig;

if (kubeconfig) {
storeStatelessClusterKubeconfig(kubeconfig);
await storeStatelessClusterKubeconfig(kubeconfig);
return;
}

Expand Down
208 changes: 144 additions & 64 deletions frontend/src/stateless/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ import * as jsyaml from 'js-yaml';
import { v4 as uuidv4 } from 'uuid';

/**
* KubeconfigObject is the object that is stored in sessionStorage.
* KubeconfigObject is the object that is stored in indexDB as string format.
* It is a JSON encoded version of the kubeconfig file.
* It is used to store the kubeconfig for stateless clusters.
* This is basically a k8s client-go Kubeconfig object.
* This is basically a k8s client - go Kubeconfig object.
* @see storeStatelessClusterKubeconfig
* @see getStatelessClusterKubeConfigs
* @see findKubeconfigByClusterName
Expand Down Expand Up @@ -34,94 +34,174 @@ interface KubeconfigObject {
}

/**
* Export function to store cluster kubeconfig
* @param kubeconfig - kubeconfig file to store in sessionStorage
* Store the kubeconfig for a stateless cluster in IndexedDB.
* @param kubeconfig - The kubeconfig to store.
* @returns void
* @throws Error if IndexedDB is not supported.
* @throws Error if the kubeconfig is invalid.
*/
export function storeStatelessClusterKubeconfig(kubeconfig: string) {
// Get existing stored cluster kubeconfigs
const storedClusterKubeconfigsJSON = sessionStorage.getItem('clusterKubeconfigs');

let storedClusterKubeconfigs: string[] = [];
if (storedClusterKubeconfigsJSON) {
storedClusterKubeconfigs = JSON.parse(storedClusterKubeconfigsJSON);
}
export function storeStatelessClusterKubeconfig(kubeconfig: any): Promise<void> {
return new Promise<void>((resolve, reject) => {
const request = indexedDB.open('kubeconfigs', 1);

request.onupgradeneeded = function (event: any) {
const db = event.target ? event.target.result : null;
if (db) {
if (!db.objectStoreNames.contains('kubeconfigStore')) {
db.createObjectStore('kubeconfigStore', { keyPath: 'id', autoIncrement: true });
}
}
};

// Check if the kubeconfig is not already in the array, then add it
if (!storedClusterKubeconfigs.includes(kubeconfig)) {
storedClusterKubeconfigs.push(kubeconfig);
}
request.onsuccess = function (event: any) {
const db = event.target ? event.target.result : null;

// Store the updated cluster kubeconfigs back in sessionStorage
sessionStorage.setItem('clusterKubeconfigs', JSON.stringify(storedClusterKubeconfigs));
}
if (db) {
const transaction = db.transaction(['kubeconfigStore'], 'readwrite');
const store = transaction.objectStore('kubeconfigStore');

/**
* Export function to get stored cluster kubeconfigs
* @returns stored cluster kubeconfigs
*/
export function getStatelessClusterKubeConfigs(): string[] {
// Get stored cluster kubeconfigs
const storedClusterKubeconfigsJSON = sessionStorage.getItem('clusterKubeconfigs');
const newItem = { kubeconfig: kubeconfig };
const addRequest = store.add(newItem);

if (storedClusterKubeconfigsJSON) {
const storedClusterKubeconfigs: string[] = JSON.parse(storedClusterKubeconfigsJSON);
addRequest.onsuccess = function () {
console.log('Kubeconfig added to IndexedDB');
resolve(); // Resolve the promise when the kubeconfig is successfully added
};

return storedClusterKubeconfigs;
}
addRequest.onerror = function (event: any) {
console.error(event.target ? event.target.error : 'An error occurred');
reject(event.target ? event.target.error : 'An error occurred'); // Reject the promise on error
};
} else {
console.error('Failed to open IndexedDB');
reject('Failed to open IndexedDB');
}
};

return [];
request.onerror = function (event: any) {
console.error(
event.target ? event.target.error : 'An error occurred while opening IndexedDB'
);
reject(event.target ? event.target.error : 'An error occurred while opening IndexedDB');
};
});
}

/**
* Export function to find kubeconfig by cluster name
* @param clusterName - cluster name to find kubeconfig for
* @returns kubeconfig for the cluster
* @returns null if kubeconfig not found
* Gets stateless cluster kubeconfigs from IndexedDB.
* @param callback - A callback function that will be called with the kubeconfigs.
* @returns void
* @throws Error if IndexedDB is not supported.
* @throws Error if the kubeconfig is invalid.
*/
export function findKubeconfigByClusterName(clusterName: string) {
// Get stored cluster kubeconfigs
const storedClusterKubeconfigsJSON = sessionStorage.getItem('clusterKubeconfigs');

if (storedClusterKubeconfigsJSON) {
const storedClusterKubeconfigs: string[] = JSON.parse(storedClusterKubeconfigsJSON);
export function getStatelessClusterKubeConfigs(callback: (kubeconfigs: string[]) => void) {
const request = indexedDB.open('kubeconfigs', 1);

request.onupgradeneeded = function (event: any) {
const db = event.target ? event.target.result : null;
// Create the object store if it doesn't exist
if (!db.objectStoreNames.contains('kubeconfigStore')) {
db.createObjectStore('kubeconfigStore', { keyPath: 'id', autoIncrement: true });
}
};

request.onsuccess = function (event: any) {
const db = event.target.result;
const transaction = db.transaction(['kubeconfigStore'], 'readonly');
const store = transaction.objectStore('kubeconfigStore');

const kubeconfigs: string[] = [];

store.openCursor().onsuccess = function (event: any) {
const cursor = event.target.result;
if (cursor) {
kubeconfigs.push(cursor.value.kubeconfig);
cursor.continue();
} else {
// All kubeconfigs have been retrieved
callback(kubeconfigs);
}
};
};

// Check each kubeconfig for the target clusterName
for (const kubeconfig of storedClusterKubeconfigs) {
const kubeconfigObject = jsyaml.load(atob(kubeconfig)) as KubeconfigObject;
request.onerror = function (event: any) {
console.error('Error opening the database:', event.target.error);
};
}

// Check if the kubeconfig contains a cluster with the target clusterName
const matchingKubeconfig = kubeconfigObject.clusters.find(
cluster => cluster.name === clusterName
);
if (matchingKubeconfig) {
// Encode the kubeconfig back to base64 if needed
const encodedKubeconfig = btoa(JSON.stringify(kubeconfigObject));
return encodedKubeconfig;
}
/**
* Finds a kubeconfig by cluster name.
* @param clusterName
* @returns A promise that resolves with the kubeconfig, or null if not found.
* @throws Error if IndexedDB is not supported.
* @throws Error if the kubeconfig is invalid.
*/
export function findKubeconfigByClusterName(clusterName: string): Promise<string | null> {
return new Promise<string | null>(async (resolve, reject) => {
try {
const request = indexedDB.open('kubeconfigs', 1);

request.onupgradeneeded = function (event: any) {
const db = event.target ? event.target.result : null;
// Create the object store if it doesn't exist
if (!db.objectStoreNames.contains('kubeconfigStore')) {
db.createObjectStore('kubeconfigStore', { keyPath: 'id', autoIncrement: true });
}
};

request.onsuccess = function (event: any) {
const db = event.target.result;
const transaction = db.transaction(['kubeconfigStore'], 'readonly');
const store = transaction.objectStore('kubeconfigStore');

store.openCursor().onsuccess = function (event: any) {
const cursor = event.target.result;
if (cursor) {
const kubeconfigObject = cursor.value;
const kubeconfig = kubeconfigObject.kubeconfig;

const parsedKubeconfig = jsyaml.load(atob(kubeconfig)) as KubeconfigObject;

const matchingKubeconfig = parsedKubeconfig.clusters.find(
cluster => cluster.name === clusterName
);

if (matchingKubeconfig) {
resolve(kubeconfig);
} else {
cursor.continue();
}
} else {
resolve(null); // No matching kubeconfig found
}
};
};

request.onerror = function (event: any) {
console.error('Error opening the database:', event.target.error);
reject(event.target.error);
};
} catch (error) {
reject(error);
}
}

return null; // If not found
});
}

/**
* In the backend we use a unique ID to identify a user. We combine it with the
* cluster name and this headlamp-userId to create a unique ID for a cluster. If we don't
* In the backend we use a unique ID to identify a user.We combine it with the
* cluster name and this headlamp - userId to create a unique ID for a cluster.If we don't
* do it then if 2 different users have a cluster with the same name, then the
* proxy will be overwritten.
* @returns headlamp-userId from sessionStorage
* @returns headlamp - userId from localStorage
*/
export function getUserId(): string {
// Retrieve headlamp-userId ID from sessionStorage
let headlampUserId = sessionStorage.getItem('headlamp-userId');
let headlampUserId = localStorage.getItem('headlamp-userId');

// If no headlampUserId ID exists, generate one
if (!headlampUserId) {
headlampUserId = uuidv4();

if (headlampUserId) {
// Store headlampUserId in sessionStorage
sessionStorage.setItem('headlamp-userId', headlampUserId);
localStorage.setItem('headlamp-userId', headlampUserId);
}
}

Expand Down
2 changes: 1 addition & 1 deletion plugins/headlamp-plugin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"bin": "bin/headlamp-plugin.js",
"scripts": {
"prepack": "(node -e \"if (! require('fs').existsSync('./lib/components')) {process.exit(1)} \" || (echo 'lib dir is empty. Remember to run `npm run build` before packing' && exit 1))",
"build": "npx shx rm -rf lib types lib/assets lib/components lib/helpers lib/i18n lib/lib lib/plugin lib/redux lib/resources && npx shx cp -r ../../frontend/src/react-app-env.d.ts ../../frontend/src/assets ../../frontend/src/components ../../frontend/src/helpers ../../frontend/src/i18n ../../frontend/src/lib ../../frontend/src/plugin ../../frontend/src/redux ../../frontend/src/resources src/ && tsc --build ./tsconfig.json && npx shx cp -r src/additional.d.ts lib/ && npx shx cp -r ../../frontend/src/assets lib/ && npx shx cp -r ../../frontend/src/resources lib/ && npx shx cp -r ../../frontend/src/i18n/locales lib/i18n",
"build": "npx shx rm -rf lib types lib/assets lib/components lib/helpers lib/i18n lib/lib lib/plugin lib/redux lib/resources && npx shx cp -r ../../frontend/src/react-app-env.d.ts ../../frontend/src/assets ../../frontend/src/components ../../frontend/src/helpers ../../frontend/src/stateless ../../frontend/src/i18n ../../frontend/src/lib ../../frontend/src/plugin ../../frontend/src/redux ../../frontend/src/resources src/ && tsc --build ./tsconfig.json && npx shx cp -r src/additional.d.ts lib/ && npx shx cp -r ../../frontend/src/assets lib/ && npx shx cp -r ../../frontend/src/resources lib/ && npx shx cp -r ../../frontend/src/i18n/locales lib/i18n",
"update-dependencies": "node dependencies-sync.js update",
"check-dependencies": "node dependencies-sync.js check"
},
Expand Down

0 comments on commit f0acfdb

Please sign in to comment.