Skip to content

Commit

Permalink
backend: Add route and logic for renaming cluster
Browse files Browse the repository at this point in the history
This adds route that handles logic to rename clusters. When user updates
their cluster name from UI, it sends the customName and source of the
cluster where it is fetched from. The backend checks the source and
updates the context in the source. It also maps the new customName to
original name of cluster in the source.

Fixes: #1653
Signed-off-by: Kautilya Tripathi <ktripathi@microsoft.com>
  • Loading branch information
knrt10 committed Apr 28, 2024
1 parent c5f310e commit ff9fa30
Show file tree
Hide file tree
Showing 4 changed files with 210 additions and 3 deletions.
6 changes: 6 additions & 0 deletions backend/cmd/cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,9 @@ type ClusterReq struct {
type KubeconfigRequest struct {
Kubeconfigs []string `json:"kubeconfigs"`
}

// RenameClusterRequest is the request body structure for renaming a cluster.
type RenameClusterRequest struct {
NewClusterName string `json:"newClusterName"`
Source string `json:"source"`
}
151 changes: 149 additions & 2 deletions backend/cmd/headlamp.go
Original file line number Diff line number Diff line change
Expand Up @@ -1056,8 +1056,9 @@ func (c *HeadlampConfig) getClusters() []Cluster {
Server: context.Cluster.Server,
AuthType: context.AuthType(),
Metadata: map[string]interface{}{
"source": context.SourceStr(),
"namespace": context.KubeContext.Namespace,
"source": context.SourceStr(),
"namespace": context.KubeContext.Namespace,
"extensions": context.KubeContext.Extensions,
},
})
}
Expand Down Expand Up @@ -1289,6 +1290,149 @@ func (c *HeadlampConfig) deleteCluster(w http.ResponseWriter, r *http.Request) {
c.getConfig(w, r)
}

// Get path of kubeconfig from source.
func (c *HeadlampConfig) getKubeConfigPath(source string) (string, error) {
if source == "kubeconfig" {
return c.kubeConfigPath, nil
}

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 {
logger.Log(logger.LevelError, map[string]string{"cluster": clusterName},
err, "decoding request body")
http.Error(w, err.Error(), http.StatusBadRequest)

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
}

// Check if the CustomName field matches the cluster name
if customObj.CustomName != "" && customObj.CustomName == clusterName {
contextName = k
}
}
}

// Get the context with the given cluster name
contextConfig, ok := config.Contexts[contextName]
if !ok {
logger.Log(logger.LevelError, map[string]string{"cluster": clusterName},
err, "getting context from kubeconfig")
http.Error(w, "getting context from kubeconfig", http.StatusInternalServerError)

return
}

// Create a CustomObject with CustomName field
customObj := &kubeconfig.CustomObject{
TypeMeta: v1.TypeMeta{},
ObjectMeta: v1.ObjectMeta{},
CustomName: reqBody.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},
err, "writing kubeconfig file")
http.Error(w, "writing kubeconfig file", http.StatusInternalServerError)

return
}

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)

return
}

for _, context := range contexts {
context := context

// Remove the old context from the store
if err := c.kubeConfigStore.RemoveContext(clusterName); err != nil {
errs = append(errs, err)
}

// Add the new context to the store
if err = c.kubeConfigStore.AddContext(&context); err != nil {
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
}

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

func (c *HeadlampConfig) addClusterSetupRoute(r *mux.Router) {
// Do not add the route if dynamic clusters are disabled
if !c.enableDynamicClusters {
Expand All @@ -1302,6 +1446,9 @@ func (c *HeadlampConfig) addClusterSetupRoute(r *mux.Router) {

// Delete a cluster
r.HandleFunc("/cluster/{name}", c.deleteCluster).Methods("DELETE")

// Rename a cluster
r.HandleFunc("/cluster/{name}", c.renameCluster).Methods("PUT")
}

/*
Expand Down
27 changes: 26 additions & 1 deletion backend/pkg/kubeconfig/contextStore.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package kubeconfig

import (
"context"
"encoding/json"
"time"

"github.com/headlamp-k8s/headlamp/backend/pkg/cache"
Expand Down Expand Up @@ -32,7 +33,31 @@ func NewContextStore() ContextStore {

// AddContext adds a context to the store.
func (c *contextStore) AddContext(headlampContext *Context) error {
return c.cache.Set(context.Background(), headlampContext.Name, headlampContext)
name := headlampContext.Name

if headlampContext.KubeContext != nil && headlampContext.KubeContext.Extensions["headlamp_info"] != nil {
info := headlampContext.KubeContext.Extensions["headlamp_info"]
// Convert the runtime.Unknown object to a byte slice
unknownBytes, err := json.Marshal(info)
if err != nil {
return err
}

// Now, decode the byte slice into your desired struct
var customObj CustomObject

err = json.Unmarshal(unknownBytes, &customObj)
if err != nil {
return err
}

// If the custom name is set, use it as the context name
if customObj.CustomName != "" {
name = customObj.CustomName
}
}

return c.cache.Set(context.Background(), name, headlampContext)
}

// GetContexts returns all contexts in the store.
Expand Down
29 changes: 29 additions & 0 deletions backend/pkg/kubeconfig/kubeconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ import (
"runtime"
"strings"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
k8sruntime "k8s.io/apimachinery/pkg/runtime"

"github.com/headlamp-k8s/headlamp/backend/pkg/logger"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
Expand Down Expand Up @@ -46,6 +49,32 @@ type OidcConfig struct {
Scopes []string
}

// CustomObject represents the custom object that holds the HeadlampInfo regarding custom name.
type CustomObject struct {
metav1.TypeMeta
metav1.ObjectMeta
CustomName string `json:"customName"`
}

// DeepCopyObject returns a copy of the CustomObject.
func (o *CustomObject) DeepCopyObject() k8sruntime.Object {
return o.DeepCopy()
}

// DeepCopy creates a deep copy of the CustomObject.
func (o *CustomObject) DeepCopy() *CustomObject {
if o == nil {
return nil
}

copied := &CustomObject{}
o.ObjectMeta.DeepCopyInto(&copied.ObjectMeta)
copied.TypeMeta = o.TypeMeta
copied.CustomName = o.CustomName

return copied
}

// ClientConfig returns a clientcmd.ClientConfig for the context.
func (c *Context) ClientConfig() clientcmd.ClientConfig {
// If the context is empty, return nil.
Expand Down

0 comments on commit ff9fa30

Please sign in to comment.