diff --git a/backend/cmd/cluster.go b/backend/cmd/cluster.go index f395aba5ae..92fec12c9c 100644 --- a/backend/cmd/cluster.go +++ b/backend/cmd/cluster.go @@ -33,4 +33,5 @@ type KubeconfigRequest struct { type RenameClusterRequest struct { NewClusterName string `json:"newClusterName"` Source string `json:"source"` + Stateless bool `json:"stateless"` } diff --git a/backend/cmd/headlamp.go b/backend/cmd/headlamp.go index a2629511c0..63555c25ed 100644 --- a/backend/cmd/headlamp.go +++ b/backend/cmd/headlamp.go @@ -1066,6 +1066,60 @@ func (c *HeadlampConfig) getClusters() []Cluster { return clusters } +// parseCustomNameClusters parses the custom name clusters from the kubeconfig. +func parseCustomNameClusters(contexts []kubeconfig.Context) ([]Cluster, []error) { + clusters := []Cluster{} + + var setupErrors []error + + for _, context := range contexts { + context := context + + info := context.KubeContext.Extensions["headlamp_info"] + if info != nil { + // Convert the runtime.Unknown object to a byte slice + unknownBytes, err := json.Marshal(info) + if err != nil { + logger.Log(logger.LevelError, map[string]string{"cluster": context.Name}, + err, "unmarshaling context data") + + setupErrors = append(setupErrors, err) + + continue + } + + // Now, decode the byte slice into CustomObject + var customObj kubeconfig.CustomObject + + err = json.Unmarshal(unknownBytes, &customObj) + if err != nil { + logger.Log(logger.LevelError, map[string]string{"cluster": context.Name}, + err, "unmarshaling into CustomObject") + + setupErrors = append(setupErrors, err) + + continue + } + + // Check if the CustomName field is present + if customObj.CustomName != "" { + context.Name = customObj.CustomName + } + } + + clusters = append(clusters, Cluster{ + Name: context.Name, + Server: context.Cluster.Server, + AuthType: context.AuthType(), + Metadata: map[string]interface{}{ + "source": "dynamic_cluster", + }, + }) + } + + return clusters, setupErrors +} + // parseClusterFromKubeConfig parses the kubeconfig and returns a list of contexts and errors. func parseClusterFromKubeConfig(kubeConfigs []string) ([]Cluster, []error) { clusters := []Cluster{} @@ -1099,17 +1153,7 @@ func parseClusterFromKubeConfig(kubeConfigs []string) ([]Cluster, []error) { continue } - for _, context := range contexts { - context := context - clusters = append(clusters, Cluster{ - Name: context.Name, - Server: context.Cluster.Server, - AuthType: context.AuthType(), - Metadata: map[string]interface{}{ - "source": "dynamic_cluster", - }, - }) - } + clusters, setupErrors = parseCustomNameClusters(contexts) } if len(setupErrors) > 0 { @@ -1299,15 +1343,9 @@ func (c *HeadlampConfig) getKubeConfigPath(source string) (string, error) { return defaultKubeConfigPersistenceFile() } -// Handler for renaming a cluster. -// -//nolint:funlen -func (c *HeadlampConfig) renameCluster(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - clusterName := vars["name"] - // Parse request body - var reqBody RenameClusterRequest - if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { +// Handler for renaming a stateless cluster. +func (c *HeadlampConfig) handleStatelessClusterRename(w http.ResponseWriter, r *http.Request, clusterName string) { + if err := c.kubeConfigStore.RemoveContext(clusterName); err != nil { logger.Log(logger.LevelError, map[string]string{"cluster": clusterName}, err, "decoding request body") http.Error(w, err.Error(), http.StatusBadRequest) @@ -1315,97 +1353,51 @@ func (c *HeadlampConfig) renameCluster(w http.ResponseWriter, r *http.Request) { return } - // Get path of kubeconfig from source - path, err := c.getKubeConfigPath(reqBody.Source) - if err != nil { - logger.Log(logger.LevelError, map[string]string{"cluster": clusterName}, - err, "getting kubeconfig file") - http.Error(w, "getting kubeconfig file", http.StatusInternalServerError) - - return - } - - // Load kubeconfig file - config, err := clientcmd.LoadFromFile(path) - if err != nil { - logger.Log(logger.LevelError, map[string]string{"cluster": clusterName}, - err, "loading kubeconfig file") - http.Error(w, "loading kubeconfig file", http.StatusInternalServerError) - - return - } - - // Find the context with the given cluster name - contextName := clusterName - - // Iterate over the contexts to find the context with the given cluster name - for k, v := range config.Contexts { - info := v.Extensions["headlamp_info"] - if info != nil { - // Convert the runtime.Unknown object to a byte slice - unknownBytes, err := json.Marshal(info) - if err != nil { - logger.Log(logger.LevelError, map[string]string{"cluster": contextName}, - err, "unmarshaling context data") - http.Error(w, "unmarshaling context data", http.StatusInternalServerError) - - return - } - - // Now, decode the byte slice into CustomObject - var customObj kubeconfig.CustomObject - - err = json.Unmarshal(unknownBytes, &customObj) - if err != nil { - logger.Log(logger.LevelError, map[string]string{"cluster": contextName}, - err, "unmarshaling into CustomObject") - http.Error(w, "unmarshaling into CustomObject", http.StatusInternalServerError) - - return - } + w.WriteHeader(http.StatusCreated) + c.getConfig(w, r) +} - // Check if the CustomName field matches the cluster name - if customObj.CustomName != "" && customObj.CustomName == clusterName { - contextName = k - } - } - } +// customNameToExtenstions writes the custom name to the Extensions map in the kubeconfig. +func customNameToExtenstions(config *api.Config, contextName, newClusterName, path string) error { + var err error // Get the context with the given cluster name contextConfig, ok := config.Contexts[contextName] if !ok { - logger.Log(logger.LevelError, map[string]string{"cluster": clusterName}, + logger.Log(logger.LevelError, map[string]string{"cluster": contextName}, err, "getting context from kubeconfig") - http.Error(w, "getting context from kubeconfig", http.StatusInternalServerError) - return + return err } // Create a CustomObject with CustomName field customObj := &kubeconfig.CustomObject{ TypeMeta: v1.TypeMeta{}, ObjectMeta: v1.ObjectMeta{}, - CustomName: reqBody.NewClusterName, + CustomName: newClusterName, } // Assign the CustomObject to the Extensions map contextConfig.Extensions["headlamp_info"] = customObj if err := clientcmd.WriteToFile(*config, path); err != nil { - logger.Log(logger.LevelError, map[string]string{"cluster": clusterName}, + logger.Log(logger.LevelError, map[string]string{"cluster": contextName}, err, "writing kubeconfig file") - http.Error(w, "writing kubeconfig file", http.StatusInternalServerError) - return + return err } + return nil +} + +// updateCustomContextToCache updates the custom context to the cache. +func (c *HeadlampConfig) updateCustomContextToCache(config *api.Config, clusterName string) []error { contexts, errs := kubeconfig.LoadContextsFromAPIConfig(config, false) if len(contexts) == 0 { - logger.Log(logger.LevelError, nil, errors.New("no contexts found in kubeconfig"), - "getting contexts from kubeconfig") - http.Error(w, "getting contexts from kubeconfig", http.StatusBadRequest) + logger.Log(logger.LevelError, nil, errs, "no contexts found in kubeconfig") + errs = append(errs, errors.New("no contexts found in kubeconfig")) - return + return errs } for _, context := range contexts { @@ -1413,19 +1405,102 @@ func (c *HeadlampConfig) renameCluster(w http.ResponseWriter, r *http.Request) { // Remove the old context from the store if err := c.kubeConfigStore.RemoveContext(clusterName); err != nil { + logger.Log(logger.LevelError, nil, err, "Removing context from the store") errs = append(errs, err) } // Add the new context to the store - if err = c.kubeConfigStore.AddContext(&context); err != nil { + if err := c.kubeConfigStore.AddContext(&context); err != nil { + logger.Log(logger.LevelError, nil, err, "Adding context to the store") errs = append(errs, err) } } if len(errs) > 0 { - logger.Log(logger.LevelError, nil, errs, "setting up contexts from kubeconfig") - http.Error(w, "setting up contexts from kubeconfig", http.StatusBadRequest) + return errs + } + + return nil +} + +// getPathAndLoadKubeconfig gets the path of the kubeconfig file and loads it. +func (c *HeadlampConfig) getPathAndLoadKubeconfig(source, clusterName string) (string, *api.Config, error) { + // Get path of kubeconfig from source + path, err := c.getKubeConfigPath(source) + if err != nil { + logger.Log(logger.LevelError, map[string]string{"cluster": clusterName}, + err, "getting kubeconfig file") + + return "", nil, err + } + + // Load kubeconfig file + config, err := clientcmd.LoadFromFile(path) + if err != nil { + logger.Log(logger.LevelError, map[string]string{"cluster": clusterName}, + err, "loading kubeconfig file") + + return "", nil, err + } + + return path, config, nil +} +// Handler for renaming a cluster. +func (c *HeadlampConfig) renameCluster(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + clusterName := vars["name"] + // Parse request body. + var reqBody RenameClusterRequest + if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { + logger.Log(logger.LevelError, map[string]string{"cluster": clusterName}, + err, "decoding request body") + http.Error(w, err.Error(), http.StatusBadRequest) + + return + } + + if reqBody.Stateless { + // For stateless clusters we just need to remove cluster from cache + c.handleStatelessClusterRename(w, r, clusterName) + } + + // Get path of kubeconfig from source + path, config, err := c.getPathAndLoadKubeconfig(reqBody.Source, clusterName) + if err != nil { + http.Error(w, "getting kubeconfig file", http.StatusInternalServerError) + return + } + + // Find the context with the given cluster name + contextName := clusterName + + // Iterate over the contexts to find the context with the given cluster name + for k, v := range config.Contexts { + info := v.Extensions["headlamp_info"] + if info != nil { + customObj, err := MarshalCustomObject(info, contextName) + if err != nil { + logger.Log(logger.LevelError, map[string]string{"cluster": contextName}, + err, "marshaling custom object") + + return + } + + // Check if the CustomName field matches the cluster name + if customObj.CustomName != "" && customObj.CustomName == clusterName { + contextName = k + } + } + } + + if err := customNameToExtenstions(config, contextName, reqBody.NewClusterName, path); err != nil { + http.Error(w, "writing custom extension to kubeconfig", http.StatusInternalServerError) + return + } + + if errs := c.updateCustomContextToCache(config, clusterName); len(errs) > 0 { + http.Error(w, "setting up contexts from kubeconfig", http.StatusBadRequest) return } diff --git a/backend/cmd/stateless.go b/backend/cmd/stateless.go index 448263a02b..7d2d7615d9 100644 --- a/backend/cmd/stateless.go +++ b/backend/cmd/stateless.go @@ -9,8 +9,60 @@ import ( "github.com/gorilla/mux" "github.com/headlamp-k8s/headlamp/backend/pkg/kubeconfig" "github.com/headlamp-k8s/headlamp/backend/pkg/logger" + "k8s.io/apimachinery/pkg/runtime" ) +// MarshalCustomObject marshals the runtime.Unknown object into a CustomObject. +func MarshalCustomObject(info runtime.Object, contextName string) (kubeconfig.CustomObject, error) { + // Convert the runtime.Unknown object to a byte slice + unknownBytes, err := json.Marshal(info) + if err != nil { + logger.Log(logger.LevelError, map[string]string{"cluster": contextName}, + err, "unmarshaling context data") + + return kubeconfig.CustomObject{}, err + } + + // Now, decode the byte slice into CustomObject + var customObj kubeconfig.CustomObject + + err = json.Unmarshal(unknownBytes, &customObj) + if err != nil { + logger.Log(logger.LevelError, map[string]string{"cluster": contextName}, + err, "unmarshaling into CustomObject") + + return kubeconfig.CustomObject{}, err + } + + return customObj, nil +} + +// setKeyInCache sets the context in the cache with the given key. +func (c *HeadlampConfig) setKeyInCache(key string, context kubeconfig.Context) error { + // check context is present + _, err := c.kubeConfigStore.GetContext(key) + if err != nil && err.Error() == "key not found" { + // To ensure stateless clusters are not visible to other users, they are marked as internal clusters. + // They are stored in the proxy cache and accessed through the /config endpoint. + context.Internal = true + if err = c.kubeConfigStore.AddContextWithKeyAndTTL(&context, key, ContextCacheTTL); err != nil { + logger.Log(logger.LevelError, map[string]string{"key": key}, + err, "adding context to cache") + + return err + } + } else { + if err = c.kubeConfigStore.UpdateTTL(key, ContextUpdateChacheTTL); err != nil { + logger.Log(logger.LevelError, map[string]string{"key": key}, + err, "updating context ttl") + + return err + } + } + + return nil +} + // Handles stateless cluster requests if kubeconfig is set and dynamic clusters are enabled. // It returns context key which is used to store the context in the cache. func (c *HeadlampConfig) handleStatelessReq(r *http.Request, kubeConfig string) (string, error) { @@ -39,30 +91,28 @@ func (c *HeadlampConfig) handleStatelessReq(r *http.Request, kubeConfig string) for _, context := range contexts { context := context - if context.Name != clusterName { - contextKey = clusterName - continue - } - - // check context is present - _, err := c.kubeConfigStore.GetContext(key) - if err != nil && err.Error() == "key not found" { - // To ensure stateless clusters are not visible to other users, they are marked as internal clusters. - // They are stored in the proxy cache and accessed through the /config endpoint. - context.Internal = true - if err = c.kubeConfigStore.AddContextWithKeyAndTTL(&context, key, ContextCacheTTL); err != nil { - logger.Log(logger.LevelError, map[string]string{"key": key}, - err, "adding context to cache") + info := context.KubeContext.Extensions["headlamp_info"] + if info != nil { + customObj, err := MarshalCustomObject(info, context.Name) + if err != nil { + logger.Log(logger.LevelError, map[string]string{"cluster": context.Name}, + err, "marshaling custom object") return "", err } - } else { - if err = c.kubeConfigStore.UpdateTTL(key, ContextUpdateChacheTTL); err != nil { - logger.Log(logger.LevelError, map[string]string{"key": key}, - err, "updating context ttl") - return "", err + // Check if the CustomName field is present + if customObj.CustomName != "" { + key = customObj.CustomName + userID } + } else if context.Name != clusterName { + contextKey = clusterName + continue + } + + // check context is present + if err := c.setKeyInCache(key, context); err != nil { + return "", err } contextKey = key diff --git a/frontend/src/components/App/Settings/SettingsCluster.tsx b/frontend/src/components/App/Settings/SettingsCluster.tsx index a10fbc7817..21f0a59366 100644 --- a/frontend/src/components/App/Settings/SettingsCluster.tsx +++ b/frontend/src/components/App/Settings/SettingsCluster.tsx @@ -7,8 +7,9 @@ import { useDispatch } from 'react-redux'; import { useHistory } from 'react-router-dom'; import helpers, { ClusterSettings } from '../../../helpers'; import { useCluster, useClustersConf } from '../../../lib/k8s'; -import { deleteCluster, renameCluster } from '../../../lib/k8s/apiProxy'; -import { setConfig } from '../../../redux/configSlice'; +import { deleteCluster, parseKubeConfig, renameCluster } from '../../../lib/k8s/apiProxy'; +import { setConfig, setStatelessConfig } from '../../../redux/configSlice'; +import { findKubeconfigByClusterName, updateStatelessClusterKubeconfig } from '../../../stateless/'; import { Link, NameValueTable, SectionBox } from '../../common'; import ConfirmButton from '../../common/ConfirmButton'; @@ -57,15 +58,31 @@ export default function SettingsCluster() { try { storeNewClusterName(newClusterName); renameCluster(cluster || '', newClusterName, source) - .then(config => { - dispatch(setConfig(config)); + .then(async config => { + if (cluster) { + const kubeconfig = await findKubeconfigByClusterName(cluster); + if (kubeconfig !== null) { + await updateStatelessClusterKubeconfig(kubeconfig, newClusterName, cluster); + // Make another request for updated kubeconfig + const updatedKubeconfig = await findKubeconfigByClusterName(cluster); + if (updatedKubeconfig !== null) { + parseKubeConfig({ kubeconfig: updatedKubeconfig }) + .then((config: any) => { + dispatch(setStatelessConfig(config)); + }) + .catch((err: Error) => { + console.error('Error updating cluster name:', err.message); + }); + } + } else { + dispatch(setConfig(config)); + } + } history.push('/'); window.location.reload(); }) .catch((err: Error) => { - if (err.message === 'Not Found') { - // TODO: create notification with error message - } + console.error('Error updating cluster name:', err.message); }); } catch (error) { console.error('Error updating cluster name:', error); @@ -372,7 +389,7 @@ export default function SettingsCluster() { }} confirmTitle={t('translation|Change name')} confirmDescription={t( - 'translation|This will add an extension field in your kubeconfig file for "{{ clusterName }}. Are you sure"?', + 'translation|Are you sure you want to change the name for "{{ clusterName }}"?', { clusterName: cluster } )} disabled={!newClusterName || !isValidCurrentName} diff --git a/frontend/src/i18n/locales/de/translation.json b/frontend/src/i18n/locales/de/translation.json index e9db6d77d7..447079d686 100644 --- a/frontend/src/i18n/locales/de/translation.json +++ b/frontend/src/i18n/locales/de/translation.json @@ -71,7 +71,7 @@ "Current name": "", "The current name of cluster. You can define custom modified name.": "", "Change name": "", - "This will add an extension field in your kubeconfig file for \"{{ clusterName }}. Are you sure\"?": "", + "Are you sure you want to change the name for \"{{ clusterName }}\"?": "", "Remove Cluster": "Cluster entfernen", "Server": "Server", "light theme": "helles Design", diff --git a/frontend/src/i18n/locales/de/translation_old.json b/frontend/src/i18n/locales/de/translation_old.json new file mode 100644 index 0000000000..a70d69de88 --- /dev/null +++ b/frontend/src/i18n/locales/de/translation_old.json @@ -0,0 +1,3 @@ +{ + "This will add an extension field in your kubeconfig file for \"{{ clusterName }}. Are you sure\"?": "" +} diff --git a/frontend/src/i18n/locales/en/translation.json b/frontend/src/i18n/locales/en/translation.json index c52469b937..4abfa03dbf 100644 --- a/frontend/src/i18n/locales/en/translation.json +++ b/frontend/src/i18n/locales/en/translation.json @@ -71,7 +71,7 @@ "Current name": "Current name", "The current name of cluster. You can define custom modified name.": "The current name of cluster. You can define custom modified name.", "Change name": "Change name", - "This will add an extension field in your kubeconfig file for \"{{ clusterName }}. Are you sure\"?": "This will add an extension field in your kubeconfig file for \"{{ clusterName }}. Are you sure\"?", + "Are you sure you want to change the name for \"{{ clusterName }}\"?": "Are you sure you want to change the name for \"{{ clusterName }}\"?", "Remove Cluster": "Remove Cluster", "Server": "Server", "light theme": "light theme", diff --git a/frontend/src/i18n/locales/en/translation_old.json b/frontend/src/i18n/locales/en/translation_old.json new file mode 100644 index 0000000000..c27703c67d --- /dev/null +++ b/frontend/src/i18n/locales/en/translation_old.json @@ -0,0 +1,3 @@ +{ + "This will add an extension field in your kubeconfig file for \"{{ clusterName }}. Are you sure\"?": "This will add an extension field in your kubeconfig file for \"{{ clusterName }}. Are you sure\"?" +} diff --git a/frontend/src/i18n/locales/es/translation.json b/frontend/src/i18n/locales/es/translation.json index 2f87cd7fc9..a4c0a6e57d 100644 --- a/frontend/src/i18n/locales/es/translation.json +++ b/frontend/src/i18n/locales/es/translation.json @@ -71,7 +71,7 @@ "Current name": "", "The current name of cluster. You can define custom modified name.": "", "Change name": "", - "This will add an extension field in your kubeconfig file for \"{{ clusterName }}. Are you sure\"?": "", + "Are you sure you want to change the name for \"{{ clusterName }}\"?": "", "Remove Cluster": "Eliminar cluster", "Server": "Servidor", "light theme": "tema claro", diff --git a/frontend/src/i18n/locales/es/translation_old.json b/frontend/src/i18n/locales/es/translation_old.json new file mode 100644 index 0000000000..a70d69de88 --- /dev/null +++ b/frontend/src/i18n/locales/es/translation_old.json @@ -0,0 +1,3 @@ +{ + "This will add an extension field in your kubeconfig file for \"{{ clusterName }}. Are you sure\"?": "" +} diff --git a/frontend/src/i18n/locales/fr/translation.json b/frontend/src/i18n/locales/fr/translation.json index 17463431bc..d04bbd7fe5 100644 --- a/frontend/src/i18n/locales/fr/translation.json +++ b/frontend/src/i18n/locales/fr/translation.json @@ -71,7 +71,7 @@ "Current name": "", "The current name of cluster. You can define custom modified name.": "", "Change name": "", - "This will add an extension field in your kubeconfig file for \"{{ clusterName }}. Are you sure\"?": "", + "Are you sure you want to change the name for \"{{ clusterName }}\"?": "", "Remove Cluster": "Supprimer le cluster", "Server": "Serveur", "light theme": "thème clair", diff --git a/frontend/src/i18n/locales/fr/translation_old.json b/frontend/src/i18n/locales/fr/translation_old.json new file mode 100644 index 0000000000..a70d69de88 --- /dev/null +++ b/frontend/src/i18n/locales/fr/translation_old.json @@ -0,0 +1,3 @@ +{ + "This will add an extension field in your kubeconfig file for \"{{ clusterName }}. Are you sure\"?": "" +} diff --git a/frontend/src/i18n/locales/pt/translation.json b/frontend/src/i18n/locales/pt/translation.json index acbd43a7ea..45fe21e5cd 100644 --- a/frontend/src/i18n/locales/pt/translation.json +++ b/frontend/src/i18n/locales/pt/translation.json @@ -71,7 +71,7 @@ "Current name": "", "The current name of cluster. You can define custom modified name.": "", "Change name": "", - "This will add an extension field in your kubeconfig file for \"{{ clusterName }}. Are you sure\"?": "", + "Are you sure you want to change the name for \"{{ clusterName }}\"?": "", "Remove Cluster": "Remover Cluster", "Server": "Servidor", "light theme": "tema claro", diff --git a/frontend/src/i18n/locales/pt/translation_old.json b/frontend/src/i18n/locales/pt/translation_old.json new file mode 100644 index 0000000000..a70d69de88 --- /dev/null +++ b/frontend/src/i18n/locales/pt/translation_old.json @@ -0,0 +1,3 @@ +{ + "This will add an extension field in your kubeconfig file for \"{{ clusterName }}. Are you sure\"?": "" +} diff --git a/frontend/src/lib/k8s/apiProxy.ts b/frontend/src/lib/k8s/apiProxy.ts index 3628d74341..90417694bb 100644 --- a/frontend/src/lib/k8s/apiProxy.ts +++ b/frontend/src/lib/k8s/apiProxy.ts @@ -1634,26 +1634,16 @@ export async function testClusterHealth(cluster?: string) { export async function setCluster(clusterReq: ClusterRequest) { const kubeconfig = clusterReq.kubeconfig; + let requestURL = '/cluster'; if (kubeconfig) { await storeStatelessClusterKubeconfig(kubeconfig); // We just send parsed kubeconfig from the backend to the frontend. - return request( - '/parseKubeConfig', - { - method: 'POST', - body: JSON.stringify(clusterReq), - headers: { - ...JSON_HEADERS, - }, - }, - false, - false - ); + requestURL = '/parseKubeConfig'; } return request( - '/cluster', + requestURL, { method: 'POST', body: JSON.stringify(clusterReq), @@ -1693,11 +1683,11 @@ export async function deleteCluster(cluster: string) { * @param cluster */ export async function renameCluster(cluster: string, newClusterName: string, source: string) { + let stateless = false; if (cluster) { const kubeconfig = await findKubeconfigByClusterName(cluster); if (kubeconfig !== null) { - // @TODO: Update kubeconfig in indexDB - return window.location.reload(); + stateless = true; } } @@ -1706,13 +1696,40 @@ export async function renameCluster(cluster: string, newClusterName: string, sou { method: 'PUT', headers: { ...getHeadlampAPIHeaders() }, - body: JSON.stringify({ newClusterName, source }), + body: JSON.stringify({ newClusterName, source, stateless }), }, false, false ); } +/** + * parseKubeConfig sends call to backend to parse kubeconfig and send back + * the parsed clusters and contexts. + * @param clusterReq - The cluster request object. + */ +export async function parseKubeConfig(clusterReq: ClusterRequest) { + const kubeconfig = clusterReq.kubeconfig; + + if (kubeconfig) { + return request( + '/parseKubeConfig', + { + method: 'POST', + body: JSON.stringify(clusterReq), + headers: { + ...JSON_HEADERS, + ...getHeadlampAPIHeaders(), + }, + }, + false, + false + ); + } + + return null; +} + // @todo: Move startPortForward, stopPortForward, and getPortForwardStatus to a portForward.ts // @todo: the return type is missing for the following functions. diff --git a/frontend/src/lib/k8s/kubeconfig.ts b/frontend/src/lib/k8s/kubeconfig.ts index ded95adc59..29163d7522 100644 --- a/frontend/src/lib/k8s/kubeconfig.ts +++ b/frontend/src/lib/k8s/kubeconfig.ts @@ -143,7 +143,10 @@ export interface KubeconfigObject { /** name is the nickname of the extension. */ name: string; /** Extension holds the extension information */ - extension: {}; + extension: { + /** customName is the custom name for the cluster. */ + customName?: string; + }; }>; }; }>; diff --git a/frontend/src/stateless/index.ts b/frontend/src/stateless/index.ts index 9d9da9efe5..360834e4eb 100644 --- a/frontend/src/stateless/index.ts +++ b/frontend/src/stateless/index.ts @@ -262,12 +262,18 @@ export function findKubeconfigByClusterName(clusterName: string): Promise + context.context.extensions?.find(extension => extension.name === 'headlamp_info') + ?.extension.customName === clusterName + ); const matchingKubeconfig = parsedKubeconfig.clusters.find( cluster => cluster.name === clusterName ); - if (matchingKubeconfig) { + if (matchingKubeconfig || matchingContext) { resolve(kubeconfig); } else { cursor.continue(); @@ -489,6 +495,122 @@ export async function deleteClusterKubeconfig(clusterName: string): Promise { + return new Promise(async (resolve, reject) => { + try { + const request = + process.env.NODE_ENV === 'test' + ? indexedDBtest.open('kubeconfigs', 1) + : indexedDB.open('kubeconfigs', 1); + // Parse the kubeconfig from base64 + const parsedKubeconfig = jsyaml.load(atob(kubeconfig)) as KubeconfigObject; + + // Find the context with the matching cluster name or custom name in headlamp_info + const matchingContext = parsedKubeconfig.contexts.find( + context => + context.context.cluster === clusterName || + context.context.extensions?.find(extension => extension.name === 'headlamp_info') + ?.extension.customName === clusterName + ); + + if (matchingContext) { + const extensions = matchingContext.context.extensions || []; + const headlampExtension = extensions.find(extension => extension.name === 'headlamp_info'); + + if (matchingContext.context.cluster === clusterName) { + // Push the new extension if the cluster name matches + extensions.push({ + extension: { + customName: customName, + }, + name: 'headlamp_info', + }); + } else if (headlampExtension) { + // Update the existing extension if found + headlampExtension.extension.customName = customName; + } + + // Ensure the extensions property is updated + matchingContext.context.extensions = extensions; + } else { + console.error('No context found matching the cluster name:', clusterName); + reject('No context found matching the cluster name'); + return; + } + + // Convert the updated kubeconfig back to base64 + const updatedKubeconfig = btoa(jsyaml.dump(parsedKubeconfig)); + + // 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. + request.onsuccess = function handleDatabaseSuccess(event: DatabaseEvent) { + const db = event.target.result; + if (db) { + const transaction = db.transaction(['kubeconfigStore'], 'readwrite'); + const store = transaction.objectStore('kubeconfigStore'); + + // Get the existing kubeconfig entry by clusterName + store.openCursor().onsuccess = function getSuccess(event: Event) { + const successEvent = event as CursorSuccessEvent; + const cursor = successEvent.target?.result; + if (cursor) { + // Update the kubeconfig entry with the new kubeconfig + cursor.value.kubeconfig = updatedKubeconfig; + // Put the updated kubeconfig entry back into IndexedDB + const putRequest = store.put(cursor.value); + // The onsuccess event is fired when the request has succeeded. + putRequest.onsuccess = function putSuccess() { + console.log('Updated kubeconfig with custom name and stored in IndexedDB'); + resolve(); // Resolve the promise when the kubeconfig is successfully updated and stored + }; + + // The onerror event is fired when the request has failed. + putRequest.onerror = function putError(event: Event) { + const errorEvent = event as DatabaseErrorEvent; + console.error(errorEvent.target ? errorEvent.target.error : 'An error occurred'); + reject(errorEvent.target ? errorEvent.target.error : 'An error occurred'); + }; + } else { + console.error('No kubeconfig entry found for cluster name:', clusterName); + reject('No kubeconfig entry found for cluster name'); + } + }; + + store.openCursor().onerror = function getError(event: Event) { + const errorEvent = event as DatabaseErrorEvent; + console.error(errorEvent.target ? errorEvent.target.error : 'An error occurred'); + reject(errorEvent.target ? errorEvent.target.error : 'An error occurred'); + }; + } 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; + } catch (error) { + reject(error); + } + }); +} + const exportFunctions = { storeStatelessClusterKubeconfig, getStatelessClusterKubeConfigs, @@ -497,6 +619,7 @@ const exportFunctions = { processClusterComparison, fetchStatelessClusterKubeConfigs, deleteClusterKubeconfig, + updateStatelessClusterKubeconfig, }; export default exportFunctions;