@kinvolk/headlamp-plugin
Version:
The needed infrastructure for building Headlamp plugins.
313 lines (312 loc) • 14.5 kB
JavaScript
/*
* Copyright 2025 The Kubernetes Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import _ from 'lodash';
import { addBackstageAuthHeaders } from '../helpers/addBackstageAuthHeaders';
import { request } from '../lib/k8s/api/v1/clusterRequests';
import { JSON_HEADERS } from '../lib/k8s/api/v1/constants';
import { setStatelessConfig } from '../redux/configSlice';
import store from '../redux/stores/store';
import { deleteClusterKubeconfig } from './deleteClusterKubeconfig';
import { findKubeconfigByClusterName } from './findKubeconfigByClusterName';
import { getUserIdFromLocalStorage } from './getUserIdFromLocalStorage';
import { updateStatelessClusterKubeconfig } from './updateStatelessClusterKubeconfig';
/** handleDatabaseUpgrade creates the object store if it doesn't exist.
* this upgrade is only called when the database is created.
* It is not called when the database is opened.
* @param event - The event that is fired when the IndexedDB is upgraded.
* @see storeStatelessClusterKubeconfig
* @see getStatelessClusterKubeConfigs
* @see findKubeconfigByClusterName
* **/
export function handleDatabaseUpgrade(event) {
const db = event.target ? event.target.result : null;
// Create the object store if it doesn't exist
if (db && !db.objectStoreNames.contains('kubeconfigStore')) {
db.createObjectStore('kubeconfigStore', { keyPath: 'id', autoIncrement: true });
}
}
/** handleDataBaseError handles errors when opening IndexedDB.
* @param event - The event that is fired when the IndexedDB is upgraded.
* @see storeStatelessClusterKubeconfig
* @see getStatelessClusterKubeConfigs
* @see findKubeconfigByClusterName
* */
export function handleDataBaseError(event, reject) {
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');
}
/**
* Store the kubeconfig for a stateless cluster in IndexedDB.
* @param kubeconfig - The kubeconfig to store.
* @returns promise that resolves when the kubeconfig is successfully added.
* @throws Error if IndexedDB is not supported.
* @throws Error if the kubeconfig is invalid.
*/
export function storeStatelessClusterKubeconfig(kubeconfig) {
return new Promise(async (resolve, reject) => {
const request = indexedDB.open('kubeconfigs', 1);
// The onupgradeneeded event is fired when the database is created for the first time.
request.onupgradeneeded = handleDatabaseUpgrade;
/** The onsuccess event is fired when the database is opened.
* This event is where you specify the actions to take when the database is opened.
* Once the database is opened, it creates a transaction and an object store.
* The transaction is used to add the kubeconfig to the object store.
* The object store is used to store the kubeconfig.
* */
request.onsuccess = function handleDatabaseSuccess(event) {
const db = event.target ? event.target.result : null;
if (db) {
const transaction = db.transaction(['kubeconfigStore'], 'readwrite');
const store = transaction.objectStore('kubeconfigStore');
const newItem = { kubeconfig: kubeconfig };
const addRequest = store.add(newItem);
// The onsuccess event is fired when the request has succeeded.
// This is where you handle the results of the request.
addRequest.onsuccess = function requestSuccess() {
console.debug('Kubeconfig added to IndexedDB');
resolve(); // Resolve the promise when the kubeconfig is successfully added
};
// The onerror event is fired when the request has failed.
// This is where you handle the error.
addRequest.onerror = function requestError(event) {
const errorEvent = event;
console.error(errorEvent.target ? errorEvent.target.error : 'An error occurred');
reject(errorEvent.target ? errorEvent.target.error : 'An error occurred'); // Reject the promise on error
};
}
else {
console.error('Failed to open IndexedDB');
reject('Failed to open IndexedDB');
}
};
// The onerror event is fired when the database is opened.
// This is where you handle errors.
request.onerror = handleDataBaseError;
});
}
/**
* Gets stateless cluster kubeconfigs from IndexedDB.
* @returns A promise that resolves with the kubeconfigs.
* @throws Error if IndexedDB is not supported.
* @throws Error if the kubeconfig is invalid.
*/
export function getStatelessClusterKubeConfigs() {
return new Promise(async (resolve, reject) => {
const request = indexedDB.open('kubeconfigs', 1);
// The onupgradeneeded event is fired when the database is created for the first time.
request.onupgradeneeded = handleDatabaseUpgrade;
/** The onsuccess event is fired when the database is opened.
* This event is where you specify the actions to take when the database is opened.
* Once the database is opened, it creates a transaction and an object store.
* It returns all the kubeconfigs from the object store.
* */
request.onsuccess = function handleDatabaseSuccess(event) {
const db = event.target ? event.target.result : null;
if (db) {
const transaction = db.transaction(['kubeconfigStore'], 'readonly');
const store = transaction.objectStore('kubeconfigStore');
const kubeconfigs = [];
/** The onsuccess event is fired when the request has succeeded.
* This is where you handle the results of the request.
* The result is the cursor. It is used to iterate through the object store.
* The cursor is null when there are no more objects to iterate through.
* */
store.openCursor().onsuccess = function storeSuccess(event) {
const successEvent = event;
const cursor = successEvent.target.result;
if (cursor) {
kubeconfigs.push(cursor.value.kubeconfig);
cursor.continue();
}
else {
// All kubeconfigs have been retrieved
resolve(kubeconfigs);
}
};
}
else {
reject('Failed to open IndexedDB');
}
};
// The onerror event is fired when the database is opened.
// This is where you handle errors.
request.onerror = handleDataBaseError;
});
}
/**
* Finds the kubeconfig and context with the matching cluster name or custom name in headlamp_info.
* @param clusterID The ID for a cluster, composed of the kubeconfig path and cluster name
* @param clusterName The name of the cluster to find.
* @param parsedKubeconfig The parsed kubeconfig object.
* @returns An object containing the matching kubeconfig and context.
*/
export function findMatchingContexts(clusterName, parsedKubeconfig, clusterID) {
let matchingContext;
let matchingKubeconfig;
// Note: currently clusterID is being used for non dynamic clusters only
if (clusterID) {
// Find source for the kubeconfig
const source = parsedKubeconfig.contexts.find(context => context.context.clusterID === clusterID)?.context.source;
// Find the context with the matching clusterID
if (source === 'kubeconfig') {
matchingKubeconfig = parsedKubeconfig.contexts.find(context => context.context.clusterID === clusterID);
}
}
else {
// Find the context with the matching cluster name or custom name in headlamp_info
matchingContext = parsedKubeconfig.contexts.find(context => context.name === clusterName ||
context.context.extensions?.find(extension => extension.name === 'headlamp_info')?.extension
.customName === clusterName);
matchingKubeconfig = parsedKubeconfig.contexts.find(context => context.name === clusterName);
}
return { matchingKubeconfig, matchingContext };
}
/**
* Finds and replaces a kubeconfig by cluster name.
* @param clusterName - The name of the cluster to find and replace.
* @param kubeconfig - The base64 encoded kubeconfig to replace the existing one with.
* @param create - If true, create a new kubeconfig if it doesn't exist. If false, only replace existing kubeconfigs.
* @returns A promise that resolves when the kubeconfig is successfully replaced.
* @throws Error if the kubeconfig replacement fails at any step.
*
* Note: If deletion of the existing kubeconfig fails, the operation will still proceed
* to store the new kubeconfig. This ensures the new configuration is applied even if
* cleanup of the old one encounters issues.
*/
export async function findAndReplaceKubeconfig(clusterName, kubeconfig, create = false) {
try {
// First try to find the existing kubeconfig
const existingKubeconfig = await findKubeconfigByClusterName(clusterName);
if (existingKubeconfig) {
// If found, delete the old one
// Note: If deletion fails, we continue with storing the new kubeconfig
// to ensure the new configuration is applied
try {
await deleteClusterKubeconfig(clusterName);
}
catch (deleteError) {
console.warn(`Failed to delete existing kubeconfig for cluster ${clusterName}, but continuing with replacement:`, deleteError);
}
// Store the new kubeconfig
await storeStatelessClusterKubeconfig(kubeconfig);
}
else if (create) {
// If not found and create is true, store the new kubeconfig
await storeStatelessClusterKubeconfig(kubeconfig);
}
else {
// If not found and create is false, throw error
throw new Error(`No existing kubeconfig found for cluster ${clusterName} and create is false`);
}
}
catch (error) {
console.error('Error in findAndReplaceKubeconfig:', error);
throw error;
}
}
/**
* Generates a cryptographically secure random token using the browser's crypto API.
* @param {number} length - The length of the token.
* @returns {string} - The generated token.
*/
export function generateSecureToken(length = 16) {
const buffer = new Uint8Array(length);
if (import.meta.env.NODE_ENV === 'test') {
// Use Math.random() in the testing environment
return Array.from(buffer, () => Math.floor(Math.random() * 16).toString(16)).join('');
}
window.crypto.getRandomValues(buffer);
return Array.from(buffer, byte => byte.toString(16).padStart(2, '0'))
.join('')
.slice(0, length);
}
/**
* Compares the cluster config from the backend and the redux store
* @param clusters
* @param clustersToConfig
* @returns true if the present stored config is different from the fetched one.
*/
export function isEqualClusterConfigs(currentConfig, newConfig) {
if (!currentConfig || !newConfig) {
return true; // Config is different if either is null/undefined
}
const currentKeys = Object.keys(currentConfig);
const newKeys = Object.keys(newConfig);
if (currentKeys.length !== newKeys.length) {
return true; // Different number of clusters
}
return currentKeys.some(key => {
if (!newConfig[key]) {
return true; // Cluster in current config doesn't exist in new config
}
const currentCluster = _.omit(currentConfig[key], ['useToken']);
const newCluster = _.omit(newConfig[key], ['useToken']);
return !_.isEqual(currentCluster, newCluster);
});
}
/**
* Parses the cluster config from the backend and updates the redux store
* if the present stored config is different from the fetched one.
*/
export async function fetchStatelessClusterKubeConfigs(dispatch) {
const config = await getStatelessClusterKubeConfigs();
const statelessClusters = store.getState().config.statelessClusters;
const headers = addBackstageAuthHeaders(JSON_HEADERS);
const clusterReq = {
kubeconfigs: config,
};
// Parses statelessCluster config
request('/parseKubeConfig', {
method: 'POST',
body: JSON.stringify(clusterReq),
headers: {
...headers,
},
}, false, false)
.then((config) => {
const clustersToConfig = {};
if (config?.clusters && Array.isArray(config.clusters)) {
config?.clusters.forEach((cluster) => {
clustersToConfig[cluster.name] = cluster;
});
}
const configToStore = {
statelessClusters: clustersToConfig,
};
if (statelessClusters === null) {
dispatch(setStatelessConfig({ ...configToStore }));
}
else if (Object.keys(clustersToConfig).length !== Object.keys(statelessClusters).length) {
dispatch(setStatelessConfig({ ...configToStore }));
}
})
.catch((err) => {
console.error('Error getting config:', err);
});
}
const exportFunctions = {
storeStatelessClusterKubeconfig,
getStatelessClusterKubeConfigs,
findKubeconfigByClusterName,
getUserIdFromLocalStorage,
isEqualClusterConfigs,
fetchStatelessClusterKubeConfigs,
deleteClusterKubeconfig,
updateStatelessClusterKubeconfig,
// @deprecated - use isEqualClusterConfigs instead
processClusterComparison: isEqualClusterConfigs,
};
export default exportFunctions;