Skip to content

Commit

Permalink
headlamp: Add rename feature for stateless cluster
Browse files Browse the repository at this point in the history
Signed-off-by: Kautilya Tripathi <ktripathi@microsoft.com>
  • Loading branch information
knrt10 committed May 20, 2024
1 parent 579ed3c commit a8c6172
Show file tree
Hide file tree
Showing 7 changed files with 268 additions and 30 deletions.
1 change: 1 addition & 0 deletions backend/cmd/cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,5 @@ type KubeconfigRequest struct {
type RenameClusterRequest struct {
NewClusterName string `json:"newClusterName"`
Source string `json:"source"`
Stateless bool `json:"stateless"`
}
51 changes: 50 additions & 1 deletion backend/cmd/headlamp.go
Original file line number Diff line number Diff line change
Expand Up @@ -1101,6 +1101,39 @@ func parseClusterFromKubeConfig(kubeConfigs []string) ([]Cluster, []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,
Expand Down Expand Up @@ -1305,7 +1338,7 @@ func (c *HeadlampConfig) getKubeConfigPath(source string) (string, error) {
func (c *HeadlampConfig) renameCluster(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
clusterName := vars["name"]
// Parse request body
// 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},
Expand All @@ -1315,6 +1348,22 @@ func (c *HeadlampConfig) renameCluster(w http.ResponseWriter, r *http.Request) {
return
}

// For stateless clusters we just need to remove cluster from cache
if reqBody.Stateless {
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)

return
}

w.WriteHeader(http.StatusCreated)
c.getConfig(w, r)

return
}

// Get path of kubeconfig from source
path, err := c.getKubeConfigPath(reqBody.Source)
if err != nil {
Expand Down
34 changes: 31 additions & 3 deletions backend/cmd/stateless.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,37 @@ func (c *HeadlampConfig) handleStatelessReq(r *http.Request, kubeConfig string)
for _, context := range contexts {
context := context

if context.Name != clusterName {
contextKey = clusterName
continue
info := context.KubeContext.Extensions["headlamp_info"]
if info != nil {

Check failure on line 43 in backend/cmd/stateless.go

View workflow job for this annotation

GitHub Actions / Lint & Build

`if info != nil` has complex nested blocks (complexity: 5) (nestif)
// 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")

return "", 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": context.Name},
err, "unmarshaling into CustomObject")

return "", err
}

// Check if the CustomName field is present
if customObj.CustomName != "" {
key = customObj.CustomName + userID
}
} else {

Check failure on line 68 in backend/cmd/stateless.go

View workflow job for this annotation

GitHub Actions / Lint & Build

elseif: can replace 'else {if cond {}}' with 'else if cond {}' (gocritic)
if context.Name != clusterName {
contextKey = clusterName
continue
}
}

// check context is present
Expand Down
33 changes: 25 additions & 8 deletions frontend/src/components/App/Settings/SettingsCluster.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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();
// 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);
Expand Down
49 changes: 33 additions & 16 deletions frontend/src/lib/k8s/apiProxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down Expand Up @@ -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;
}
}

Expand All @@ -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.
Expand Down
5 changes: 4 additions & 1 deletion frontend/src/lib/k8s/kubeconfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};
}>;
};
}>;
Expand Down
Loading

0 comments on commit a8c6172

Please sign in to comment.