diff --git a/cmd/config/config.go b/cmd/config/config.go index 67006b9..49474a7 100644 --- a/cmd/config/config.go +++ b/cmd/config/config.go @@ -4,12 +4,11 @@ import ( "context" "fmt" "io" - "os" - "path" "time" "github.com/metal-stack/api/go/client" "github.com/metal-stack/cli/cmd/completion" + "github.com/metal-stack/metal-lib/pkg/genericcli" "github.com/metal-stack/metal-lib/pkg/genericcli/printers" "github.com/metal-stack/metal-lib/pkg/pointer" "github.com/spf13/afero" @@ -23,7 +22,8 @@ const ( BinaryName = "metalctlv2" // ConfigDir is the directory in either the homedir or in /etc where the cli searches for a file config.yaml // also used as prefix for environment based configuration, e.g. METAL_STACK_CLOUD_ will be the variable prefix. - ConfigDir = "metal-stack" + ConfigDir = "metal-stack" + keyTimeout = "timeout" ) type Config struct { @@ -35,7 +35,8 @@ type Config struct { ListPrinter printers.Printer DescribePrinter printers.Printer Completion *completion.Completion - Context Context + ContextManager *genericcli.ContextManager + Context genericcli.Context } func (c *Config) NewRequestContext() (context.Context, context.CancelFunc) { @@ -43,40 +44,15 @@ func (c *Config) NewRequestContext() (context.Context, context.CancelFunc) { if timeout == nil { timeout = pointer.Pointer(30 * time.Second) } - if viper.IsSet("timeout") { - timeout = pointer.Pointer(viper.GetDuration("timeout")) + if viper.IsSet(keyTimeout) { + timeout = pointer.Pointer(viper.GetDuration(keyTimeout)) } return context.WithTimeout(context.Background(), *timeout) } -func DefaultConfigDirectory() (string, error) { - h, err := os.UserHomeDir() - if err != nil { - return "", err - } - - return path.Join(h, "."+ConfigDir), nil -} - -func ConfigPath() (string, error) { - if viper.IsSet("config") { - return viper.GetString("config"), nil - } - - dir, err := DefaultConfigDirectory() - if err != nil { - return "", err - } - - return path.Join(dir, "config.yaml"), nil -} - func (c *Config) GetProject() string { - if viper.IsSet("project") { - return viper.GetString("project") - } - return c.Context.DefaultProject + return c.Context.GetProject() } func (c *Config) GetTenant() (string, error) { @@ -102,27 +78,13 @@ func (c *Config) GetTenant() (string, error) { } func (c *Config) GetToken() string { - if viper.IsSet("api-token") { - return viper.GetString("api-token") - } - return c.Context.Token + return c.Context.GetAPIToken() } func (c *Config) GetApiURL() string { - if viper.IsSet("api-url") { - return viper.GetString("api-url") - } - if c.Context.ApiURL != nil { - return *c.Context.ApiURL - } - - // fallback to the default specified by viper - return viper.GetString("api-url") + return c.Context.GetAPIURL() } func (c *Config) GetProvider() string { - if viper.IsSet("provider") { - return viper.GetString("provider") - } - return c.Context.Provider + return c.Context.GetProvider() } diff --git a/cmd/config/context.go b/cmd/config/context.go deleted file mode 100644 index 2ba9110..0000000 --- a/cmd/config/context.go +++ /dev/null @@ -1,150 +0,0 @@ -package config - -import ( - "errors" - "fmt" - "os" - "path" - "slices" - "time" - - "github.com/metal-stack/metal-lib/pkg/pointer" - "github.com/spf13/afero" - "github.com/spf13/cobra" - "github.com/spf13/viper" - "sigs.k8s.io/yaml" -) - -// Contexts contains all configuration contexts -type Contexts struct { - CurrentContext string `json:"current-context"` - PreviousContext string `json:"previous-context"` - Contexts []*Context `json:"contexts"` -} - -// Context configure -type Context struct { - Name string `json:"name"` - ApiURL *string `json:"api-url,omitempty"` - Token string `json:"api-token"` - DefaultProject string `json:"default-project"` - Timeout *time.Duration `json:"timeout,omitempty"` - Provider string `json:"provider"` -} - -func (cs *Contexts) Get(name string) (*Context, bool) { - for _, context := range cs.Contexts { - if context.Name == name { - return context, true - } - } - - return nil, false -} - -func (cs *Contexts) List() []*Context { - return append([]*Context{}, cs.Contexts...) -} - -func (cs *Contexts) Validate() error { - names := map[string]bool{} - for _, context := range cs.Contexts { - names[context.Name] = true - } - - if len(cs.Contexts) != len(names) { - return fmt.Errorf("context names must be unique") - } - - return nil -} - -func (cs *Contexts) Delete(name string) { - cs.Contexts = slices.DeleteFunc(cs.Contexts, func(ctx *Context) bool { - return ctx.Name == name - }) -} - -func (c *Config) GetContexts() (*Contexts, error) { - path, err := ConfigPath() - if err != nil { - return nil, err - } - - raw, err := afero.ReadFile(c.Fs, path) - if err != nil { - if errors.Is(err, os.ErrNotExist) { - return &Contexts{}, nil - } - - return nil, fmt.Errorf("unable to read config.yaml: %w", err) - } - - var ctxs Contexts - err = yaml.Unmarshal(raw, &ctxs) - return &ctxs, err -} - -func (c *Config) WriteContexts(ctxs *Contexts) error { - if err := ctxs.Validate(); err != nil { - return err - } - - raw, err := yaml.Marshal(ctxs) - if err != nil { - return err - } - - dest, err := ConfigPath() - if err != nil { - return err - } - - // when path is in the default path, we ensure the directory exists - if defaultPath, err := DefaultConfigDirectory(); err == nil && defaultPath == path.Dir(dest) { - err = c.Fs.MkdirAll(defaultPath, 0700) - if err != nil { - return fmt.Errorf("unable to ensure default config directory: %w", err) - } - } - - err = afero.WriteFile(c.Fs, dest, raw, 0600) - if err != nil { - return err - } - - return nil -} - -func (c *Config) MustDefaultContext() Context { - ctxs, err := c.GetContexts() - if err != nil { - return defaultCtx() - } - - ctx, ok := ctxs.Get(ctxs.CurrentContext) - if !ok { - return defaultCtx() - } - - return *ctx -} - -func defaultCtx() Context { - return Context{ - ApiURL: pointer.PointerOrNil(viper.GetString("api-url")), - Token: viper.GetString("api-token"), - } -} - -func (c *Config) ContextListCompletion(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - ctxs, err := c.GetContexts() - if err != nil { - return nil, cobra.ShellCompDirectiveError - } - var names []string - for _, ctx := range ctxs.Contexts { - names = append(names, ctx.Name) - } - return names, cobra.ShellCompDirectiveNoFileComp -} diff --git a/cmd/context.go b/cmd/context.go deleted file mode 100644 index 99d654f..0000000 --- a/cmd/context.go +++ /dev/null @@ -1,339 +0,0 @@ -package cmd - -import ( - "fmt" - - "github.com/fatih/color" - "github.com/metal-stack/cli/cmd/config" - "github.com/metal-stack/cli/cmd/sorters" - "github.com/metal-stack/metal-lib/pkg/genericcli" - "github.com/metal-stack/metal-lib/pkg/pointer" - "github.com/spf13/cobra" - "github.com/spf13/viper" -) - -type ctx struct { - c *config.Config -} - -func newContextCmd(c *config.Config) *cobra.Command { - w := &ctx{ - c: c, - } - - contextCmd := &cobra.Command{ - Use: "context", - Aliases: []string{"ctx"}, - Short: "manage cli contexts", - Long: "you can switch back and forth contexts with \"-\"", - RunE: func(cmd *cobra.Command, args []string) error { - if len(args) == 0 { - return w.list() - } - - return w.set(args) - }, - } - - contextListCmd := &cobra.Command{ - Use: "list", - Aliases: []string{"ls"}, - Short: "list the configured cli contexts", - RunE: func(cmd *cobra.Command, args []string) error { - return w.list() - }, - } - contextSwitchCmd := &cobra.Command{ - Use: "switch ", - Short: "switch the cli context", - Long: "you can switch back and forth contexts with \"-\"", - Aliases: []string{"set", "sw"}, - RunE: func(cmd *cobra.Command, args []string) error { - return w.set(args) - }, - ValidArgsFunction: c.ContextListCompletion, - } - contextShortCmd := &cobra.Command{ - Use: "show-current", - Short: "prints the current context name", - RunE: func(cmd *cobra.Command, args []string) error { - return w.short() - }, - } - contextSetProjectCmd := &cobra.Command{ - Use: "set-project ", - Short: "sets the default project to act on for cli commands", - RunE: func(cmd *cobra.Command, args []string) error { - return w.setProject(args) - }, - ValidArgsFunction: c.Completion.ProjectListCompletion, - } - contextRemoveCmd := &cobra.Command{ - Use: "remove ", - Aliases: []string{"rm", "delete"}, - Short: "remove a cli context", - RunE: func(cmd *cobra.Command, args []string) error { - return w.remove(args) - }, - ValidArgsFunction: c.ContextListCompletion, - } - - contextAddCmd := &cobra.Command{ - Use: "add ", - Aliases: []string{"create"}, - Short: "add a cli context", - RunE: func(cmd *cobra.Command, args []string) error { - return w.add(args) - }, - } - contextAddCmd.Flags().String("api-url", "", "sets the api-url for this context") - contextAddCmd.Flags().String("api-token", "", "sets the api-token for this context") - contextAddCmd.Flags().String("default-project", "", "sets a default project to act on") - contextAddCmd.Flags().Duration("timeout", 0, "sets a default request timeout") - contextAddCmd.Flags().Bool("activate", false, "immediately switches to the new context") - contextAddCmd.Flags().String("provider", "", "sets the login provider for this context") - - genericcli.Must(contextAddCmd.MarkFlagRequired("api-token")) - - contextUpdateCmd := &cobra.Command{ - Use: "update ", - Short: "update a cli context", - RunE: func(cmd *cobra.Command, args []string) error { - return w.update(args) - }, - ValidArgsFunction: c.ContextListCompletion, - } - contextUpdateCmd.Flags().String("api-url", "", "sets the api-url for this context") - contextUpdateCmd.Flags().String("api-token", "", "sets the api-token for this context") - contextUpdateCmd.Flags().String("default-project", "", "sets a default project to act on") - contextUpdateCmd.Flags().Duration("timeout", 0, "sets a default request timeout") - contextUpdateCmd.Flags().Bool("activate", false, "immediately switches to the new context") - contextUpdateCmd.Flags().String("provider", "", "sets the login provider for this context") - - genericcli.Must(contextUpdateCmd.RegisterFlagCompletionFunc("default-project", c.Completion.ProjectListCompletion)) - - contextCmd.AddCommand( - contextListCmd, - contextSwitchCmd, - contextAddCmd, - contextUpdateCmd, - contextRemoveCmd, - contextShortCmd, - contextSetProjectCmd, - ) - - return contextCmd -} - -func (c *ctx) list() error { - ctxs, err := c.c.GetContexts() - if err != nil { - return err - } - - err = sorters.ContextSorter().SortBy(ctxs.Contexts) - if err != nil { - return err - } - - return c.c.ListPrinter.Print(ctxs) -} - -func (c *ctx) short() error { - ctxs, err := c.c.GetContexts() - if err != nil { - return err - } - - _, _ = fmt.Fprint(c.c.Out, ctxs.CurrentContext) - - return nil -} - -func (c *ctx) add(args []string) error { - name, err := genericcli.GetExactlyOneArg(args) - if err != nil { - return fmt.Errorf("no context name given") - } - - ctxs, err := c.c.GetContexts() - if err != nil { - return err - } - - _, ok := ctxs.Get(name) - if ok { - return fmt.Errorf("context with name %q already exists", name) - } - - ctx := &config.Context{ - Name: name, - ApiURL: pointer.PointerOrNil(viper.GetString("api-url")), - Token: viper.GetString("api-token"), - DefaultProject: viper.GetString("default-project"), - Timeout: pointer.PointerOrNil(viper.GetDuration("timeout")), - Provider: viper.GetString("provider"), - } - - ctxs.Contexts = append(ctxs.Contexts, ctx) - - if viper.GetBool("activate") || ctxs.CurrentContext == "" { - ctxs.PreviousContext = ctxs.CurrentContext - ctxs.CurrentContext = ctx.Name - } - - err = c.c.WriteContexts(ctxs) - if err != nil { - return err - } - - _, _ = fmt.Fprintf(c.c.Out, "%s added context \"%s\"\n", color.GreenString("✔"), color.GreenString(ctx.Name)) - - return nil -} - -func (c *ctx) update(args []string) error { - name, err := genericcli.GetExactlyOneArg(args) - if err != nil { - return fmt.Errorf("no context name given") - } - - ctxs, err := c.c.GetContexts() - if err != nil { - return err - } - - ctx, ok := ctxs.Get(name) - if !ok { - return fmt.Errorf("no context with name %q found", name) - } - - if viper.IsSet("api-url") { - ctx.ApiURL = pointer.PointerOrNil(viper.GetString("api-url")) - } - if viper.IsSet("api-token") { - ctx.Token = viper.GetString("api-token") - } - if viper.IsSet("default-project") { - ctx.DefaultProject = viper.GetString("default-project") - } - if viper.IsSet("timeout") { - ctx.Timeout = pointer.PointerOrNil(viper.GetDuration("timeout")) - } - if viper.IsSet("provider") { - ctx.Provider = viper.GetString("provider") - } - if viper.GetBool("activate") { - ctxs.PreviousContext = ctxs.CurrentContext - ctxs.CurrentContext = ctx.Name - } - - err = c.c.WriteContexts(ctxs) - if err != nil { - return err - } - - _, _ = fmt.Fprintf(c.c.Out, "%s updated context \"%s\"\n", color.GreenString("✔"), color.GreenString(ctx.Name)) - - return nil -} - -func (c *ctx) remove(args []string) error { - name, err := genericcli.GetExactlyOneArg(args) - if err != nil { - return fmt.Errorf("no context name given") - } - - ctxs, err := c.c.GetContexts() - if err != nil { - return err - } - - ctx, ok := ctxs.Get(name) - if !ok { - return fmt.Errorf("no context with name %q found", name) - } - - ctxs.Delete(ctx.Name) - - err = c.c.WriteContexts(ctxs) - if err != nil { - return err - } - - _, _ = fmt.Fprintf(c.c.Out, "%s removed context \"%s\"\n", color.GreenString("✔"), color.GreenString(ctx.Name)) - - return nil -} - -func (c *ctx) set(args []string) error { - wantCtx, err := genericcli.GetExactlyOneArg(args) - if err != nil { - return fmt.Errorf("no context name given") - } - - ctxs, err := c.c.GetContexts() - if err != nil { - return err - } - - if wantCtx == "-" { - prev := ctxs.PreviousContext - if prev == "" { - return fmt.Errorf("no previous context found") - } - - curr := ctxs.CurrentContext - ctxs.PreviousContext = curr - ctxs.CurrentContext = prev - } else { - nextCtx := wantCtx - _, ok := ctxs.Get(nextCtx) - if !ok { - return fmt.Errorf("context %s not found", nextCtx) - } - if nextCtx == ctxs.CurrentContext { - _, _ = fmt.Fprintf(c.c.Out, "%s context \"%s\" already active\n", color.GreenString("✔"), color.GreenString(ctxs.CurrentContext)) - return nil - } - ctxs.PreviousContext = ctxs.CurrentContext - ctxs.CurrentContext = nextCtx - } - - err = c.c.WriteContexts(ctxs) - if err != nil { - return err - } - - _, _ = fmt.Fprintf(c.c.Out, "%s switched context to \"%s\"\n", color.GreenString("✔"), color.GreenString(ctxs.CurrentContext)) - - return nil -} - -func (c *ctx) setProject(args []string) error { - project, err := genericcli.GetExactlyOneArg(args) - if err != nil { - return err - } - - ctxs, err := c.c.GetContexts() - if err != nil { - return err - } - - ctx, ok := ctxs.Get(c.c.Context.Name) - if !ok { - return fmt.Errorf("no context currently active") - } - - ctx.DefaultProject = project - - err = c.c.WriteContexts(ctxs) - if err != nil { - return err - } - - _, _ = fmt.Fprintf(c.c.Out, "%s switched context default project to \"%s\"\n", color.GreenString("✔"), color.GreenString(ctx.DefaultProject)) - - return nil -} diff --git a/cmd/helpers.go b/cmd/helpers.go new file mode 100644 index 0000000..34c45f4 --- /dev/null +++ b/cmd/helpers.go @@ -0,0 +1,20 @@ +package cmd + +import ( + "fmt" + "os/exec" + "runtime" +) + +func openBrowser(url string) error { + switch runtime.GOOS { + case "linux": + return exec.Command("xdg-open", url).Run() + case "windows": + return exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Run() + case "darwin": + return exec.Command("open", url).Run() + default: + return fmt.Errorf("unsupported platform") + } +} diff --git a/cmd/login.go b/cmd/login.go index 960a172..90e15b1 100644 --- a/cmd/login.go +++ b/cmd/login.go @@ -1,12 +1,12 @@ package cmd import ( + "cmp" "context" "errors" "fmt" "net" "net/http" - "os/exec" "time" "github.com/fatih/color" @@ -42,6 +42,7 @@ func newLoginCmd(c *config.Config) *cobra.Command { genericcli.Must(loginCmd.Flags().MarkHidden("admin-role")) genericcli.Must(loginCmd.RegisterFlagCompletionFunc("provider", cobra.FixedCompletions([]string{"openid-connect"}, cobra.ShellCompDirectiveNoFileComp))) + genericcli.Must(loginCmd.RegisterFlagCompletionFunc("context", c.ContextManager.ContextListCompletion)) genericcli.Must(loginCmd.RegisterFlagCompletionFunc("admin-role", c.Completion.TokenAdminRoleCompletion)) return loginCmd @@ -53,33 +54,6 @@ func (l *login) login() error { return errors.New("provider must be specified") } - ctxs, err := l.c.GetContexts() - if err != nil { - return err - } - - ctxName := ctxs.CurrentContext - if viper.IsSet("context") { - ctxName = viper.GetString("context") - } - ctx, ok := ctxs.Get(ctxName) - if !ok { - newCtx := l.c.MustDefaultContext() - newCtx.Name = "default" - if viper.IsSet("context") { - newCtx.Name = viper.GetString("context") - } - newCtx.ApiURL = pointer.Pointer(l.c.GetApiURL()) - ctxs.Contexts = append(ctxs.Contexts, &newCtx) - ctx = &newCtx - } - - ctx.Provider = provider - - // switch into new context - ctxs.PreviousContext = ctxs.CurrentContext - ctxs.CurrentContext = ctx.Name - tokenChan := make(chan string) http.HandleFunc("/callback", func(w http.ResponseWriter, r *http.Request) { @@ -105,7 +79,7 @@ func (l *login) login() error { url := fmt.Sprintf("%s/auth/%s?redirect-url=http://%s/callback", l.c.GetApiURL(), provider, listener.Addr().String()) // TODO(vknabel): nicify please - err = exec.Command("xdg-open", url).Run() //nolint + err = openBrowser(url) if err != nil { return fmt.Errorf("error opening browser: %w", err) } @@ -140,9 +114,22 @@ func (l *login) login() error { token = tokenResp.Secret } - ctx.Token = token + var ctx *genericcli.Context + var defaultCtx bool + name := viper.GetString("context") - if ctx.DefaultProject == "" { + if viper.IsSet("context") { + ctx, err = l.c.ContextManager.Get(name) + if err != nil { + return err + } + } else { + ctx, err = l.c.ContextManager.GetCurrentContext() + defaultCtx = err != nil || ctx == nil + } + + var project string + if defaultCtx || ctx.DefaultProject == "" { mc, err := newApiClient(l.c.GetApiURL(), token) if err != nil { return err @@ -154,16 +141,40 @@ func (l *login) login() error { } if len(projects.Projects) > 0 { - ctx.DefaultProject = projects.Projects[0].Uuid + project = projects.Projects[0].Uuid + } + } + + if defaultCtx { + ctx, err = l.c.ContextManager.Create(&genericcli.Context{ + Name: cmp.Or(name, string(genericcli.DefaultContextName)), + APIURL: pointer.PointerOrNil((l.c.GetApiURL())), + APIToken: token, + DefaultProject: project, + // Timeout: &0, + Provider: provider, + IsCurrent: true, + }) + if err != nil { + return err } + _, _ = fmt.Fprintf(l.c.Out, "%s Context \"%s\" is actived \n", color.GreenString("✔"), color.GreenString(ctx.Name)) + _, _ = fmt.Fprintf(l.c.Out, "%s Login successful!\n", color.GreenString("✔")) + return nil } - err = l.c.WriteContexts(ctxs) + _, err = l.c.ContextManager.Update(&genericcli.ContextUpdateRequest{ + Name: ctx.Name, + APIURL: ctx.APIURL, + APIToken: &token, + DefaultProject: pointer.Pointer(cmp.Or(ctx.DefaultProject, project)), + Provider: &provider, + IsCurrent: true, + }) if err != nil { return err } - - _, _ = fmt.Fprintf(l.c.Out, "%s login successful! Updated and activated context \"%s\"\n", color.GreenString("✔"), color.GreenString(ctx.Name)) + _, _ = fmt.Fprintf(l.c.Out, "%s Login successful!\n", color.GreenString("✔")) return nil } diff --git a/cmd/logout.go b/cmd/logout.go index 1cd4f5b..7503b1e 100644 --- a/cmd/logout.go +++ b/cmd/logout.go @@ -6,7 +6,6 @@ import ( "fmt" "net" "net/http" - "os/exec" "time" "github.com/fatih/color" @@ -16,6 +15,11 @@ import ( "github.com/spf13/viper" ) +const ( + keyProvider = "provider" + keyContextName = "context-name" +) + type logout struct { c *config.Config } @@ -33,43 +37,20 @@ func newLogoutCmd(c *config.Config) *cobra.Command { }, } - logoutCmd.Flags().String("provider", "openid-connect", "the provider used to logout with") - logoutCmd.Flags().String("context-name", "", "the context into which the token gets injected, if not specified it uses the current context or creates a context named default in case there is no current context set") + logoutCmd.Flags().String(keyProvider, "openid-connect", "the provider used to logout with") + logoutCmd.Flags().String(keyContextName, "", "the context into which the token gets injected, if not specified it uses the current context or creates a context named default in case there is no current context set") - genericcli.Must(logoutCmd.RegisterFlagCompletionFunc("provider", cobra.FixedCompletions([]string{"openid-connect"}, cobra.ShellCompDirectiveNoFileComp))) + genericcli.Must(logoutCmd.RegisterFlagCompletionFunc(keyProvider, cobra.FixedCompletions([]string{"openid-connect"}, cobra.ShellCompDirectiveNoFileComp))) return logoutCmd } func (l *logout) logout() error { - provider := viper.GetString("provider") + provider := viper.GetString(keyProvider) if provider == "" { return errors.New("provider must be specified") } - ctxs, err := l.c.GetContexts() - if err != nil { - return err - } - - ctxName := ctxs.CurrentContext - if viper.IsSet("context-name") { - ctxName = viper.GetString("context-name") - } - - ctx, ok := ctxs.Get(ctxName) - if !ok { - defaultCtx := l.c.MustDefaultContext() - defaultCtx.Name = "default" - - ctxs.PreviousContext = ctxs.CurrentContext - ctxs.CurrentContext = ctx.Name - - ctxs.Contexts = append(ctxs.Contexts, &defaultCtx) - - ctx = &defaultCtx - } - listener, err := net.Listen("tcp", "localhost:0") if err != nil { return err @@ -87,7 +68,7 @@ func (l *logout) logout() error { url := fmt.Sprintf("%s/auth/logout/%s", l.c.GetApiURL(), provider) - err = exec.Command("xdg-open", url).Run() //nolint + err = openBrowser(url) if err != nil { return fmt.Errorf("error opening browser: %w", err) } diff --git a/cmd/root.go b/cmd/root.go index 5692737..542013f 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -6,6 +6,7 @@ import ( client "github.com/metal-stack/api/go/client" "github.com/metal-stack/metal-lib/pkg/genericcli" + "github.com/metal-stack/metal-lib/pkg/genericcli/printers" adminv2 "github.com/metal-stack/cli/cmd/admin/v2" apiv2 "github.com/metal-stack/cli/cmd/api/v2" @@ -19,6 +20,11 @@ import ( "golang.org/x/net/context" ) +const ( + keyAPIURL = "api-url" + keyAPIToken = "api-token" +) + func Execute() { cfg := &config.Config{ Fs: afero.NewOsFs(), @@ -66,8 +72,8 @@ func newRootCmd(c *config.Config) *cobra.Command { rootCmd.PersistentFlags().Bool("debug", false, "debug output") rootCmd.PersistentFlags().Duration("timeout", 0, "request timeout used for api requests") - rootCmd.PersistentFlags().String("api-url", "https://api.metal-stack.io", "the url to the metal-stack.io api") - rootCmd.PersistentFlags().String("api-token", "", "the token used for api requests") + rootCmd.PersistentFlags().String(keyAPIURL, "https://api.metal-stack.io", "the url to the metal-stack.io api") + rootCmd.PersistentFlags().String(keyAPIToken, "", "the token used for api requests") genericcli.Must(viper.BindPFlags(rootCmd.PersistentFlags())) @@ -82,8 +88,23 @@ func newRootCmd(c *config.Config) *cobra.Command { recursiveAutoGenDisable(rootCmd) }, } + contextConfig := &genericcli.ContextCmdConfig{ + BinaryName: config.BinaryName, + ConfigDirName: config.ConfigDir, + Fs: c.Fs, + DescribePrinter: func() printers.Printer { return c.DescribePrinter }, + ListPrinter: func() printers.Printer { return c.ListPrinter }, + ProjectListCompletion: c.Completion.ProjectListCompletion, + } + + c.ContextManager = genericcli.NewContextManager(contextConfig) - rootCmd.AddCommand(newContextCmd(c), markdownCmd, newLoginCmd(c), newLogoutCmd(c)) + rootCmd.AddCommand( + markdownCmd, + newLoginCmd(c), + newLogoutCmd(c), + genericcli.NewContextCmd(contextConfig), + ) adminv2.AddCmds(rootCmd, c) apiv2.AddCmds(rootCmd, c) @@ -91,7 +112,7 @@ func newRootCmd(c *config.Config) *cobra.Command { } func initConfigWithViperCtx(c *config.Config) error { - c.Context = c.MustDefaultContext() + c.Context = *c.ContextManager.GetContextCurrentOrDefault() listPrinter, err := newPrinterFromCLI(c.Out) if err != nil { diff --git a/cmd/sorters/context.go b/cmd/sorters/context.go deleted file mode 100644 index 9cf8886..0000000 --- a/cmd/sorters/context.go +++ /dev/null @@ -1,14 +0,0 @@ -package sorters - -import ( - "github.com/metal-stack/cli/cmd/config" - "github.com/metal-stack/metal-lib/pkg/multisort" -) - -func ContextSorter() *multisort.Sorter[*config.Context] { - return multisort.New(multisort.FieldMap[*config.Context]{ - "name": func(a, b *config.Context, descending bool) multisort.CompareResult { - return multisort.Compare(a.Name, b.Name, descending) - }, - }, multisort.Keys{{ID: "name"}}) -} diff --git a/cmd/tableprinters/common.go b/cmd/tableprinters/common.go index 19c30e7..3097df7 100644 --- a/cmd/tableprinters/common.go +++ b/cmd/tableprinters/common.go @@ -7,7 +7,7 @@ import ( "time" apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" - "github.com/metal-stack/cli/cmd/config" + "github.com/metal-stack/metal-lib/pkg/genericcli" "github.com/metal-stack/metal-lib/pkg/genericcli/printers" "github.com/metal-stack/metal-lib/pkg/pointer" ) @@ -27,9 +27,6 @@ func (t *TablePrinter) SetPrinter(printer *printers.TablePrinter) { func (t *TablePrinter) ToHeaderAndRows(data any, wide bool) ([]string, [][]string, error) { switch d := data.(type) { - case *config.Contexts: - return t.ContextTable(d, wide) - case *apiv2.IP: return t.IPTable(pointer.WrapInSlice(d), wide) case []*apiv2.IP: @@ -40,6 +37,9 @@ func (t *TablePrinter) ToHeaderAndRows(data any, wide bool) ([]string, [][]strin case []*apiv2.Image: return t.ImageTable(d, wide) + case []*genericcli.Context: + return genericcli.ContextTable(data, wide) + case *apiv2.Project: return t.ProjectTable(pointer.WrapInSlice(d), wide) case []*apiv2.Project: diff --git a/cmd/tableprinters/context.go b/cmd/tableprinters/context.go deleted file mode 100644 index 3cafaae..0000000 --- a/cmd/tableprinters/context.go +++ /dev/null @@ -1,40 +0,0 @@ -package tableprinters - -import ( - "github.com/fatih/color" - "github.com/metal-stack/cli/cmd/config" - "github.com/metal-stack/metal-lib/pkg/pointer" - "github.com/spf13/viper" -) - -func (t *TablePrinter) ContextTable(data *config.Contexts, wide bool) ([]string, [][]string, error) { - var ( - header = []string{"", "Name", "Provider", "Default Project"} - rows [][]string - ) - - if wide { - header = append(header, "API URL") - } - - for _, c := range data.Contexts { - active := "" - if c.Name == data.CurrentContext { - active = color.GreenString("✔") - } - - row := []string{active, c.Name, c.Provider, c.DefaultProject} - if wide { - url := pointer.SafeDeref(c.ApiURL) - if url == "" { - url = viper.GetString("api-url") - } - - row = append(row, url) - } - - rows = append(rows, row) - } - - return header, rows, nil -} diff --git a/go.mod b/go.mod index 9399bc5..59d2dee 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/fatih/color v1.18.0 github.com/google/go-cmp v0.7.0 github.com/metal-stack/api v0.0.35 - github.com/metal-stack/metal-lib v0.23.5 + github.com/metal-stack/metal-lib v0.23.6-0.20260112165159-e360b6db198d github.com/metal-stack/v v1.0.3 github.com/spf13/afero v1.15.0 github.com/spf13/cobra v1.10.1 diff --git a/go.sum b/go.sum index cbca136..eb5e4c8 100644 --- a/go.sum +++ b/go.sum @@ -57,8 +57,8 @@ github.com/metal-stack/api v0.0.35-0.20251124101516-a3076941d0f8 h1:PGBiFwASqhtm github.com/metal-stack/api v0.0.35-0.20251124101516-a3076941d0f8/go.mod h1:EBwS/oZr5tIcnV6hM7iK4aBQrw4wlU7vF5p+O1p3YIU= github.com/metal-stack/api v0.0.35 h1:XxxYKTscSeYJg/ftL519nY3FAZ01atPeyD7+Zz/amQQ= github.com/metal-stack/api v0.0.35/go.mod h1:EBwS/oZr5tIcnV6hM7iK4aBQrw4wlU7vF5p+O1p3YIU= -github.com/metal-stack/metal-lib v0.23.5 h1:ozrkB3DNr3Cqn8nkBvmzc/KKpYqC1j1mv2OVOj8i7Ac= -github.com/metal-stack/metal-lib v0.23.5/go.mod h1:7uyHIrE19dkLwCZyeh2jmd7IEq5pEpzrzUGLoMN1eqY= +github.com/metal-stack/metal-lib v0.23.6-0.20260112165159-e360b6db198d h1:Z0ypbdHgk2ZTh3vqms4QLXRpRwkzJPb3nSCwYL4kgII= +github.com/metal-stack/metal-lib v0.23.6-0.20260112165159-e360b6db198d/go.mod h1:7uyHIrE19dkLwCZyeh2jmd7IEq5pEpzrzUGLoMN1eqY= github.com/metal-stack/v v1.0.3 h1:Sh2oBlnxrCUD+mVpzfC8HiqL045YWkxs0gpTvkjppqs= github.com/metal-stack/v v1.0.3/go.mod h1:YTahEu7/ishwpYKnp/VaW/7nf8+PInogkfGwLcGPdXg= github.com/minio/minlz v1.0.1 h1:OUZUzXcib8diiX+JYxyRLIdomyZYzHct6EShOKtQY2A=