From 20b2ca2ea536ec5814c746d34c398ec3f6912d3c Mon Sep 17 00:00:00 2001 From: Pavel Sviderski Date: Wed, 2 Apr 2025 16:31:26 +1000 Subject: [PATCH] feat(ctx): interactive ctx command to switch between cluster contexts --- cmd/uncloud/context/ls.go | 4 +- cmd/uncloud/context/root.go | 10 +++-- cmd/uncloud/context/use.go | 79 +++++++++++++++++++++++++++++++++++++ 3 files changed, 87 insertions(+), 6 deletions(-) create mode 100644 cmd/uncloud/context/use.go diff --git a/cmd/uncloud/context/ls.go b/cmd/uncloud/context/ls.go index 81b7ecc8..b91de526 100644 --- a/cmd/uncloud/context/ls.go +++ b/cmd/uncloud/context/ls.go @@ -15,8 +15,8 @@ func NewListCommand() *cobra.Command { var clusterContext string cmd := &cobra.Command{ - Use: "list", - Aliases: []string{"ls"}, + Use: "ls", + Aliases: []string{"list"}, Short: "List available cluster contexts.", RunE: func(cmd *cobra.Command, args []string) error { uncli := cmd.Context().Value("cli").(*cli.CLI) diff --git a/cmd/uncloud/context/root.go b/cmd/uncloud/context/root.go index 25cba7bc..dd333de8 100644 --- a/cmd/uncloud/context/root.go +++ b/cmd/uncloud/context/root.go @@ -1,22 +1,24 @@ package context import ( + "github.com/psviderski/uncloud/internal/cli" "github.com/spf13/cobra" ) func NewRootCommand() *cobra.Command { cmd := &cobra.Command{ - Use: "context", - Aliases: []string{"ctx"}, + Use: "ctx", + Aliases: []string{"context"}, Short: "Switch between different cluster contexts. Contains subcommands to manage contexts.", RunE: func(cmd *cobra.Command, args []string) error { - // TODO - return nil + uncli := cmd.Context().Value("cli").(*cli.CLI) + return selectContext(uncli) }, } cmd.AddCommand( NewListCommand(), + NewUseCommand(), ) return cmd diff --git a/cmd/uncloud/context/use.go b/cmd/uncloud/context/use.go new file mode 100644 index 00000000..fd34e03a --- /dev/null +++ b/cmd/uncloud/context/use.go @@ -0,0 +1,79 @@ +package context + +import ( + "fmt" + "maps" + "slices" + + "github.com/charmbracelet/huh" + "github.com/psviderski/uncloud/internal/cli" + "github.com/spf13/cobra" +) + +func NewUseCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "use [CONTEXT]", + Short: "Switch to a different cluster context.", + Long: "Switch to a different cluster context. If no context is provided, " + + "a list of available contexts will be displayed for selection.", + RunE: func(cmd *cobra.Command, args []string) error { + uncli := cmd.Context().Value("cli").(*cli.CLI) + + if len(args) == 1 { + return uncli.SetCurrentContext(args[0]) + } + + return selectContext(uncli) + }, + } + + return cmd +} + +func selectContext(uncli *cli.CLI) error { + if uncli.Config == nil { + return fmt.Errorf("context management is not available: Uncloud configuration file is not being used") + } + if len(uncli.Config.Contexts) == 0 { + return fmt.Errorf("no contexts found in Uncloud config (%s)", uncli.Config.Path) + } + + contextNames := slices.Sorted(maps.Keys(uncli.Config.Contexts)) + + var selected string + form := huh.NewForm( + huh.NewGroup( + huh.NewSelect[string](). + Title("Select a cluster context"). + Options(buildContextOptions(contextNames, uncli.Config.CurrentContext)...). + Value(&selected), + ), + ) + if err := form.Run(); err != nil { + return fmt.Errorf("select cluster context: %w", err) + } + + if err := uncli.SetCurrentContext(selected); err != nil { + return fmt.Errorf("set current cluster context: %w", err) + } + + fmt.Printf("Current cluster context is now '%s'.\n", selected) + return nil +} + +func buildContextOptions(contexts []string, current string) []huh.Option[string] { + options := make([]huh.Option[string], len(contexts)) + + for i, ctx := range contexts { + opt := huh.NewOption(ctx, ctx) + if ctx == current { + opt.Key += " (current)" + opt = opt.Selected(true) + } + + options[i] = opt + } + + options[1].Selected(true) + return options +}