Skip to content

Commit c891793

Browse files
committed
WIP - backend: Fix rename cluster issue for being able to rename a cluster to a name already in use on that config file
works: - the config path for renaming the config now uses a the accurate source path on user machines for backend logic (should work for multi config env) - the rename backend prevents renaming if the custom name from the request is already within the config file (either as a context name or a custom name) to do: - add some UI to the frontend that responds when the isunique checker returns the http error Signed-off-by: Vincent T <[email protected]>
1 parent 935b77a commit c891793

File tree

8 files changed

+191
-23
lines changed

8 files changed

+191
-23
lines changed

backend/cmd/headlamp.go

+93-17
Original file line numberDiff line numberDiff line change
@@ -1195,11 +1195,7 @@ func (c *HeadlampConfig) getClusters() []Cluster {
11951195

11961196
source := context.SourceStr()
11971197

1198-
// find the file name from the kubeconfig path
1199-
fileName := filepath.Base(kubeconfigPath)
1200-
1201-
// create the pathID string using a pipe as the delimiter
1202-
pathID := fmt.Sprintf("%s:%s:%s", context.SourceStr(), fileName, context.Name)
1198+
pathID := context.PathID
12031199

12041200
clusters = append(clusters, Cluster{
12051201
Name: context.Name,
@@ -1568,6 +1564,7 @@ func customNameToExtenstions(config *api.Config, contextName, newClusterName, pa
15681564
return nil
15691565
}
15701566

1567+
// MOON - 9. MAKE THIS LOOK BY THE CONTEXT PATH ID
15711568
// updateCustomContextToCache updates the custom context to the cache.
15721569
func (c *HeadlampConfig) updateCustomContextToCache(config *api.Config, clusterName string) []error {
15731570
contexts, errs := kubeconfig.LoadContextsFromAPIConfig(config, false)
@@ -1581,6 +1578,7 @@ func (c *HeadlampConfig) updateCustomContextToCache(config *api.Config, clusterN
15811578
for _, context := range contexts {
15821579
context := context
15831580

1581+
// MOON - 7. MAKE THIS LOOK BY THE CONTEXT PATH ID
15841582
// Remove the old context from the store
15851583
if err := c.kubeConfigStore.RemoveContext(clusterName); err != nil {
15861584
logger.Log(logger.LevelError, nil, err, "Removing context from the store")
@@ -1603,31 +1601,35 @@ func (c *HeadlampConfig) updateCustomContextToCache(config *api.Config, clusterN
16031601

16041602
// getPathAndLoadKubeconfig gets the path of the kubeconfig file and loads it.
16051603
func (c *HeadlampConfig) getPathAndLoadKubeconfig(source, clusterName string) (string, *api.Config, error) {
1606-
// Get path of kubeconfig from source
1607-
path, err := c.getKubeConfigPath(source)
1608-
if err != nil {
1609-
logger.Log(logger.LevelError, map[string]string{"cluster": clusterName},
1610-
err, "getting kubeconfig file")
1611-
1612-
return "", nil, err
1613-
}
16141604

16151605
// Load kubeconfig file
1616-
config, err := clientcmd.LoadFromFile(path)
1606+
config, err := clientcmd.LoadFromFile(source)
16171607
if err != nil {
16181608
logger.Log(logger.LevelError, map[string]string{"cluster": clusterName},
16191609
err, "loading kubeconfig file")
16201610

16211611
return "", nil, err
16221612
}
16231613

1624-
return path, config, nil
1614+
return source, config, nil
16251615
}
16261616

1617+
// MOON: - 1. ADD THE PATHID TO THE VARS SO WE CAN SORT BY PATH ID
1618+
16271619
// Handler for renaming a cluster.
16281620
func (c *HeadlampConfig) renameCluster(w http.ResponseWriter, r *http.Request) {
16291621
vars := mux.Vars(r)
16301622
clusterName := vars["name"]
1623+
1624+
// takes the actual path to the kubeconfig file from the url pathID
1625+
clusterPathID:= r.URL.Query().Get("clusterPathID")
1626+
1627+
pathID := clusterPathID
1628+
1629+
parts := strings.SplitN(pathID, ":", 2)
1630+
1631+
configPath := parts[0]
1632+
16311633
// Parse request body.
16321634
var reqBody RenameClusterRequest
16331635
if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil {
@@ -1646,15 +1648,50 @@ func (c *HeadlampConfig) renameCluster(w http.ResponseWriter, r *http.Request) {
16461648
}
16471649

16481650
// Get path of kubeconfig from source
1649-
path, config, err := c.getPathAndLoadKubeconfig(reqBody.Source, clusterName)
1651+
path, config, err := c.getPathAndLoadKubeconfig(configPath, clusterName)
16501652
if err != nil {
16511653
http.Error(w, "getting kubeconfig file", http.StatusInternalServerError)
16521654
return
16531655
}
16541656

1657+
// MOON - 2. WE WANT TO FIND THE CONTEXT WITH THE GIVEN PATHID AND NOT THE GIVEN NAME???
16551658
// Find the context with the given cluster name
16561659
contextName := clusterName
16571660

1661+
// loop through every context in the kubeconfig and collect all the names
1662+
// we do this because we want to be able to have the same name used for different config files
1663+
var contextNames []string
1664+
1665+
for name := range config.Contexts {
1666+
contextNames = append(contextNames, name)
1667+
1668+
logger.Log(logger.LevelInfo, map[string]string{"cluster added": name},
1669+
nil, "context name")
1670+
}
1671+
1672+
1673+
1674+
1675+
// Iterate over the contexts and add the custom names
1676+
for _, v := range config.Contexts {
1677+
info := v.Extensions["headlamp_info"]
1678+
if info != nil {
1679+
customObj, err := MarshalCustomObject(info, contextName)
1680+
if err != nil {
1681+
logger.Log(logger.LevelError, map[string]string{"cluster": contextName},
1682+
err, "marshaling custom object")
1683+
1684+
return
1685+
}
1686+
1687+
// Check if the CustomName field matches the cluster name
1688+
if customObj.CustomName != "" {
1689+
contextNames = append(contextNames, customObj.CustomName)
1690+
}
1691+
}
1692+
}
1693+
1694+
16581695
// Iterate over the contexts to find the context with the given cluster name
16591696
for k, v := range config.Contexts {
16601697
info := v.Extensions["headlamp_info"]
@@ -1667,8 +1704,18 @@ func (c *HeadlampConfig) renameCluster(w http.ResponseWriter, r *http.Request) {
16671704
return
16681705
}
16691706

1707+
// Check if the cluster name exists in the context names
1708+
isUnique := checkUniqueName(contextNames, reqBody.NewClusterName)
1709+
if (!isUnique) {
1710+
http.Error(w, "custom name already in use", http.StatusBadRequest)
1711+
logger.Log(logger.LevelError, map[string]string{"cluster": clusterName},
1712+
err, "cluster name already exists in the kubeconfig")
1713+
1714+
return
1715+
}
1716+
16701717
// Check if the CustomName field matches the cluster name
1671-
if customObj.CustomName != "" && customObj.CustomName == clusterName {
1718+
if customObj.CustomName != "" && customObj.CustomName == clusterName && isUnique {
16721719
contextName = k
16731720
}
16741721
}
@@ -1679,6 +1726,7 @@ func (c *HeadlampConfig) renameCluster(w http.ResponseWriter, r *http.Request) {
16791726
return
16801727
}
16811728

1729+
// MOON - 8. MAKE THIS LOOK BY THE CONTEXT PATH ID
16821730
if errs := c.updateCustomContextToCache(config, clusterName); len(errs) > 0 {
16831731
http.Error(w, "setting up contexts from kubeconfig", http.StatusBadRequest)
16841732
return
@@ -1688,6 +1736,34 @@ func (c *HeadlampConfig) renameCluster(w http.ResponseWriter, r *http.Request) {
16881736
c.getConfig(w, r)
16891737
}
16901738

1739+
// checkUniqueName returns false if 'newName' is already in 'names';
1740+
// otherwise returns true.
1741+
func checkUniqueName(names []string, newName string) bool {
1742+
for _, current := range names {
1743+
logger.Log(
1744+
logger.LevelInfo,
1745+
map[string]string{
1746+
"message": fmt.Sprintf("now comparing %s to %s", current, newName),
1747+
},
1748+
nil,
1749+
"comparing cluster names",
1750+
)
1751+
if current == newName {
1752+
logger.Log(
1753+
logger.LevelInfo,
1754+
map[string]string{
1755+
"message": fmt.Sprintf("duplicate cluster name found: %s", current),
1756+
},
1757+
nil,
1758+
"cluster name already in use",
1759+
)
1760+
return false
1761+
}
1762+
}
1763+
return true
1764+
}
1765+
1766+
16911767
func (c *HeadlampConfig) addClusterSetupRoute(r *mux.Router) {
16921768
// Do not add the route if dynamic clusters are disabled
16931769
if !c.enableDynamicClusters {

backend/cmd/stateless.go

+2
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import (
1313
"k8s.io/apimachinery/pkg/runtime"
1414
)
1515

16+
// MOON - 3. SINCE THIS USES THE CONTEXT NAME WE MAY NEED TO CHANGE IT TO THE CONTEXT PATH ID
17+
// MOON - 4. WE WILL NEED TO CHANGE THE TESTS LATER
1618
// MarshalCustomObject marshals the runtime.Unknown object into a CustomObject.
1719
func MarshalCustomObject(info runtime.Object, contextName string) (kubeconfig.CustomObject, error) {
1820
// Convert the runtime.Unknown object to a byte slice

backend/pkg/kubeconfig/contextStore.go

+1
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ func (c *contextStore) GetContext(name string) (*Context, error) {
8686
return context, nil
8787
}
8888

89+
// MOON - 6. CHANGE THE NAME PARAM TO THE PATH ID PARAM
8990
// RemoveContext removes a context from the store.
9091
func (c *contextStore) RemoveContext(name string) error {
9192
return c.cache.Delete(context.Background(), name)

backend/pkg/kubeconfig/kubeconfig.go

+12-1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"net/http/httputil"
99
"net/url"
1010
"os"
11+
"path/filepath"
1112
"runtime"
1213
"strings"
1314

@@ -46,6 +47,7 @@ type Context struct {
4647
Internal bool `json:"internal"`
4748
Error string `json:"error"`
4849
PathName string `json:"pathName"`
50+
PathID string `json:"pathID"`
4951
}
5052

5153
type OidcConfig struct {
@@ -353,7 +355,16 @@ func LoadContextsFromFile(kubeConfigPath string, source int) ([]Context, []Conte
353355

354356
// add the PathName to each context
355357
for i := range contexts {
356-
contexts[i].PathName = kubeConfigPath
358+
pathName := kubeConfigPath
359+
360+
contexts[i].PathName = pathName
361+
362+
// find the file name from the kubeconfig path
363+
fileName := filepath.Base(pathName)
364+
365+
// create the pathID string using a pipe as the delimiter
366+
pathID := fmt.Sprintf("%s:%s:%s", pathName, fileName, contexts[i].Name)
367+
contexts[i].PathID = pathID
357368
}
358369

359370
return contexts, contextErrors, nil

frontend/src/components/App/Settings/SettingsCluster.tsx

+8-2
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,10 @@ import helpers, { ClusterSettings } from '../../../helpers';
1818
import { useCluster, useClustersConf } from '../../../lib/k8s';
1919
import { deleteCluster, parseKubeConfig, renameCluster } from '../../../lib/k8s/apiProxy';
2020
import { setConfig, setStatelessConfig } from '../../../redux/configSlice';
21-
import { findKubeconfigByClusterName, updateStatelessClusterKubeconfig } from '../../../stateless/';
21+
import {
22+
findKubeconfigByClusterName,
23+
updateStatelessClusterKubeconfig,
24+
} from '../../../stateless/';
2225
import { Link, Loader, NameValueTable, SectionBox } from '../../common';
2326
import ConfirmButton from '../../common/ConfirmButton';
2427
import Empty from '../../common/EmptyContent';
@@ -98,10 +101,13 @@ export default function SettingsCluster() {
98101

99102
const clusterInfo = (clusterConf && clusterConf[cluster || '']) || null;
100103
const source = clusterInfo?.meta_data?.source || '';
104+
const pathID = clusterInfo?.meta_data?.pathID || '';
105+
106+
console.log('cluster conf: ', clusterConf);
101107

102108
const handleUpdateClusterName = (source: string) => {
103109
try {
104-
renameCluster(cluster || '', newClusterName, source)
110+
renameCluster(pathID, cluster || '', newClusterName, source)
105111
.then(async config => {
106112
if (cluster) {
107113
const kubeconfig = await findKubeconfigByClusterName(cluster);

frontend/src/lib/k8s/api/v1/clusterApi.ts

+10-3
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import store from '../../../../redux/stores/store';
44
import {
55
deleteClusterKubeconfig,
66
findKubeconfigByClusterName,
7+
findKubeconfigByClusterPathID,
78
storeStatelessClusterKubeconfig,
89
} from '../../../../stateless';
910
import { getCluster, getClusterGroup } from '../../../util';
@@ -135,17 +136,23 @@ export function getClusterDefaultNamespace(cluster: string, checkSettings?: bool
135136
* is the custom name of the cluster used by the user.
136137
* @param cluster
137138
*/
138-
export async function renameCluster(cluster: string, newClusterName: string, source: string) {
139+
export async function renameCluster(
140+
pathID: string,
141+
cluster: string,
142+
newClusterName: string,
143+
source: string
144+
) {
139145
let stateless = false;
140146
if (cluster) {
141-
const kubeconfig = await findKubeconfigByClusterName(cluster);
147+
const kubeconfig = await findKubeconfigByClusterPathID(pathID);
148+
// const kubeconfig = await findKubeconfigByClusterName(cluster);
142149
if (kubeconfig !== null) {
143150
stateless = true;
144151
}
145152
}
146153

147154
return request(
148-
`/cluster/${cluster}`,
155+
`/cluster/${cluster}?clusterPathID=${pathID}`,
149156
{
150157
method: 'PUT',
151158
headers: { ...getHeadlampAPIHeaders() },

frontend/src/lib/k8s/kubeconfig.ts

+2
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,8 @@ export interface KubeconfigObject {
138138
user: string;
139139
/** namespace is the default namespace. */
140140
namespace?: string;
141+
// this is needed for attaching pathID to the cluster for accurate cluster actions
142+
pathID?: string;
141143
/** Extensions holds additional information. This is useful for extenders so that reads and writes don't clobber unknown fields on the Context object. */
142144
extensions?: Array<{
143145
/** name is the nickname of the extension. */

frontend/src/stateless/index.ts

+63
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,69 @@ export function findKubeconfigByClusterName(clusterName: string): Promise<string
271271
});
272272
}
273273

274+
/**
275+
* Finds a kubeconfig by cluster name.
276+
* @param clusterName
277+
* @returns A promise that resolves with the kubeconfig, or null if not found.
278+
* @throws Error if IndexedDB is not supported.
279+
* @throws Error if the kubeconfig is invalid.
280+
*/
281+
export function findKubeconfigByClusterPathID(pathID: string): Promise<string | null> {
282+
return new Promise<string | null>(async (resolve, reject) => {
283+
try {
284+
const request = indexedDB.open('kubeconfigs', 1) as any;
285+
286+
// The onupgradeneeded event is fired when the database is created for the first time.
287+
request.onupgradeneeded = handleDatabaseUpgrade;
288+
289+
// The onsuccess event is fired when the database is opened.
290+
// This event is where you specify the actions to take when the database is opened.
291+
request.onsuccess = function handleDatabaseSuccess(event: DatabaseEvent) {
292+
const db = event.target.result;
293+
const transaction = db.transaction(['kubeconfigStore'], 'readonly');
294+
const store = transaction.objectStore('kubeconfigStore');
295+
296+
// The onsuccess event is fired when the request has succeeded.
297+
// This is where you handle the results of the request.
298+
// The result is the cursor. It is used to iterate through the object store.
299+
// The cursor is null when there are no more objects to iterate through.
300+
// The cursor is used to find the kubeconfig by cluster name.
301+
store.openCursor().onsuccess = function storeSuccess(event: Event) {
302+
const successEvent = event as CursorSuccessEvent;
303+
const cursor = successEvent.target.result;
304+
if (cursor) {
305+
const kubeconfigObject = cursor.value;
306+
const kubeconfig = kubeconfigObject.kubeconfig;
307+
308+
const parsedKubeconfig = jsyaml.load(atob(kubeconfig)) as KubeconfigObject;
309+
310+
console.log('parsedKubeconfig', parsedKubeconfig);
311+
312+
// Check for "headlamp_info" in extensions
313+
const matchingContext = parsedKubeconfig.contexts.find(
314+
context => context.context.pathID === pathID
315+
);
316+
317+
if (matchingContext) {
318+
resolve(kubeconfig);
319+
} else {
320+
cursor.continue();
321+
}
322+
} else {
323+
resolve(null); // No matching kubeconfig found
324+
}
325+
};
326+
};
327+
328+
// The onerror event is fired when the database is opened.
329+
// This is where you handle errors.
330+
request.onerror = handleDataBaseError;
331+
} catch (error) {
332+
reject(error);
333+
}
334+
});
335+
}
336+
274337
/**
275338
* In the backend we use a unique ID to identify a user. If there is no ID in localStorage
276339
* we generate a new one and store it in localStorage. We then combine it with the

0 commit comments

Comments
 (0)