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 <[email protected]>
  • Loading branch information
knrt10 committed Jul 22, 2024
1 parent c1d663e commit 294f7f2
Show file tree
Hide file tree
Showing 17 changed files with 436 additions and 138 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"`
}
251 changes: 163 additions & 88 deletions backend/cmd/headlamp.go
Original file line number Diff line number Diff line change
Expand Up @@ -1067,6 +1067,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{}
Expand Down Expand Up @@ -1100,17 +1154,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 {
Expand Down Expand Up @@ -1300,133 +1344,164 @@ 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)

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 {
context := context

// 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
}

Expand Down
Loading

0 comments on commit 294f7f2

Please sign in to comment.