From ec27fd5e685302a0e1485f5794f4b5defab9dda7 Mon Sep 17 00:00:00 2001 From: Philipp Mukosey Date: Mon, 20 Oct 2025 10:55:10 +0200 Subject: [PATCH 01/13] Migrate context command to metal-lib --- cmd/admin/v1/commands.go | 5 +- cmd/admin/v1/image.go | 8 +- cmd/admin/v1/token.go | 9 +- cmd/api/v1/commands.go | 4 +- cmd/api/v1/health.go | 4 +- cmd/api/v1/image.go | 8 +- cmd/api/v1/ip.go | 8 +- cmd/api/v1/methods.go | 4 +- cmd/api/v1/project.go | 8 +- cmd/api/v1/tenant.go | 8 +- cmd/api/v1/token.go | 8 +- cmd/api/v1/user.go | 8 +- cmd/api/v1/version.go | 4 +- cmd/common_test.go | 14 +- cmd/completion/completion.go | 18 -- cmd/config/context.go | 149 --------------- cmd/context.go | 339 ----------------------------------- cmd/login.go | 6 +- cmd/logout.go | 6 +- cmd/root.go | 15 +- cmd/sorters/context.go | 14 -- cmd/tableprinters/common.go | 4 +- cmd/tableprinters/context.go | 4 +- 23 files changed, 69 insertions(+), 586 deletions(-) delete mode 100644 cmd/completion/completion.go delete mode 100644 cmd/context.go delete mode 100644 cmd/sorters/context.go diff --git a/cmd/admin/v1/commands.go b/cmd/admin/v1/commands.go index ddfef8c..cbf3602 100644 --- a/cmd/admin/v1/commands.go +++ b/cmd/admin/v1/commands.go @@ -1,11 +1,12 @@ package v1 import ( - "github.com/metal-stack/cli/cmd/config" "github.com/spf13/cobra" + + clitypes "github.com/metal-stack/metal-lib/pkg/commands/types" ) -func AddCmds(cmd *cobra.Command, c *config.Config) { +func AddCmds(cmd *cobra.Command, c *clitypes.Config) { adminCmd := &cobra.Command{ Use: "admin", Short: "admin commands", diff --git a/cmd/admin/v1/image.go b/cmd/admin/v1/image.go index be9e7d0..6d30630 100644 --- a/cmd/admin/v1/image.go +++ b/cmd/admin/v1/image.go @@ -7,7 +7,7 @@ import ( adminv2 "github.com/metal-stack/api/go/metalstack/admin/v2" apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" - "github.com/metal-stack/cli/cmd/config" + clitypes "github.com/metal-stack/metal-lib/pkg/commands/types" "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" @@ -17,17 +17,17 @@ import ( ) type image struct { - c *config.Config + c *clitypes.Config } -func newImageCmd(c *config.Config) *cobra.Command { +func newImageCmd(c *clitypes.Config) *cobra.Command { w := &image{ c: c, } gcli := genericcli.NewGenericCLI(w).WithFS(c.Fs) cmdsConfig := &genericcli.CmdsConfig[*adminv2.ImageServiceCreateRequest, *adminv2.ImageServiceUpdateRequest, *apiv2.Image]{ - BinaryName: config.BinaryName, + BinaryName: clitypes.BinaryName, GenericCLI: gcli, Singular: "image", Plural: "images", diff --git a/cmd/admin/v1/token.go b/cmd/admin/v1/token.go index 91f0bbd..65ed024 100644 --- a/cmd/admin/v1/token.go +++ b/cmd/admin/v1/token.go @@ -5,7 +5,8 @@ import ( adminv2 "github.com/metal-stack/api/go/metalstack/admin/v2" apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" - "github.com/metal-stack/cli/cmd/config" + clitypes "github.com/metal-stack/metal-lib/pkg/commands/types" + "github.com/metal-stack/cli/cmd/sorters" "github.com/metal-stack/metal-lib/pkg/genericcli" "github.com/metal-stack/metal-lib/pkg/genericcli/printers" @@ -15,16 +16,16 @@ import ( ) type token struct { - c *config.Config + c *clitypes.Config } -func newTokenCmd(c *config.Config) *cobra.Command { +func newTokenCmd(c *clitypes.Config) *cobra.Command { w := &token{ c: c, } cmdsConfig := &genericcli.CmdsConfig[any, any, *apiv2.Token]{ - BinaryName: config.BinaryName, + BinaryName: clitypes.BinaryName, GenericCLI: genericcli.NewGenericCLI[any, any, *apiv2.Token](w).WithFS(c.Fs), Singular: "token", Plural: "tokens", diff --git a/cmd/api/v1/commands.go b/cmd/api/v1/commands.go index 1e62d60..d0d7752 100644 --- a/cmd/api/v1/commands.go +++ b/cmd/api/v1/commands.go @@ -1,11 +1,11 @@ package v1 import ( - "github.com/metal-stack/cli/cmd/config" + clitypes "github.com/metal-stack/metal-lib/pkg/commands/types" "github.com/spf13/cobra" ) -func AddCmds(cmd *cobra.Command, c *config.Config) { +func AddCmds(cmd *cobra.Command, c *clitypes.Config) { cmd.AddCommand(newVersionCmd(c)) cmd.AddCommand(newHealthCmd(c)) cmd.AddCommand(newTokenCmd(c)) diff --git a/cmd/api/v1/health.go b/cmd/api/v1/health.go index eac6678..ff01654 100644 --- a/cmd/api/v1/health.go +++ b/cmd/api/v1/health.go @@ -4,11 +4,11 @@ import ( "fmt" v1 "github.com/metal-stack/api/go/metalstack/api/v2" - "github.com/metal-stack/cli/cmd/config" + clitypes "github.com/metal-stack/metal-lib/pkg/commands/types" "github.com/spf13/cobra" ) -func newHealthCmd(c *config.Config) *cobra.Command { +func newHealthCmd(c *clitypes.Config) *cobra.Command { healthCmd := &cobra.Command{ Use: "health", Short: "print the client and server health information", diff --git a/cmd/api/v1/image.go b/cmd/api/v1/image.go index 14da8c9..d211ca3 100644 --- a/cmd/api/v1/image.go +++ b/cmd/api/v1/image.go @@ -5,7 +5,7 @@ import ( "strings" apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" - "github.com/metal-stack/cli/cmd/config" + clitypes "github.com/metal-stack/metal-lib/pkg/commands/types" "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" @@ -14,10 +14,10 @@ import ( ) type image struct { - c *config.Config + c *clitypes.Config } -func newImageCmd(c *config.Config) *cobra.Command { +func newImageCmd(c *clitypes.Config) *cobra.Command { w := &image{ c: c, } @@ -25,7 +25,7 @@ func newImageCmd(c *config.Config) *cobra.Command { gcli := genericcli.NewGenericCLI(w).WithFS(c.Fs) cmdsConfig := &genericcli.CmdsConfig[any, any, *apiv2.Image]{ - BinaryName: config.BinaryName, + BinaryName: clitypes.BinaryName, GenericCLI: gcli, Singular: "image", Plural: "images", diff --git a/cmd/api/v1/ip.go b/cmd/api/v1/ip.go index 3c757fa..2f3da68 100644 --- a/cmd/api/v1/ip.go +++ b/cmd/api/v1/ip.go @@ -4,9 +4,9 @@ import ( "fmt" apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" - "github.com/metal-stack/cli/cmd/config" "github.com/metal-stack/cli/cmd/sorters" "github.com/metal-stack/cli/pkg/helpers" + clitypes "github.com/metal-stack/metal-lib/pkg/commands/types" "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" @@ -15,16 +15,16 @@ import ( ) type ip struct { - c *config.Config + c *clitypes.Config } -func newIPCmd(c *config.Config) *cobra.Command { +func newIPCmd(c *clitypes.Config) *cobra.Command { w := &ip{ c: c, } cmdsConfig := &genericcli.CmdsConfig[*apiv2.IPServiceCreateRequest, *apiv2.IPServiceUpdateRequest, *apiv2.IP]{ - BinaryName: config.BinaryName, + BinaryName: clitypes.BinaryName, GenericCLI: genericcli.NewGenericCLI(w).WithFS(c.Fs), Singular: "ip", Plural: "ips", diff --git a/cmd/api/v1/methods.go b/cmd/api/v1/methods.go index ff71c49..15685fe 100644 --- a/cmd/api/v1/methods.go +++ b/cmd/api/v1/methods.go @@ -5,13 +5,13 @@ import ( "sort" apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" - "github.com/metal-stack/cli/cmd/config" + clitypes "github.com/metal-stack/metal-lib/pkg/commands/types" "github.com/metal-stack/metal-lib/pkg/genericcli/printers" "github.com/spf13/cobra" "github.com/spf13/viper" ) -func newMethodsCmd(c *config.Config) *cobra.Command { +func newMethodsCmd(c *clitypes.Config) *cobra.Command { methodCmd := &cobra.Command{ Use: "api-methods", Short: "show available api-methods of the metal-stack.io api", diff --git a/cmd/api/v1/project.go b/cmd/api/v1/project.go index 023708e..541a712 100644 --- a/cmd/api/v1/project.go +++ b/cmd/api/v1/project.go @@ -6,8 +6,8 @@ import ( "github.com/dustin/go-humanize" "github.com/fatih/color" apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" - "github.com/metal-stack/cli/cmd/config" "github.com/metal-stack/cli/cmd/sorters" + clitypes "github.com/metal-stack/metal-lib/pkg/commands/types" "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" @@ -16,16 +16,16 @@ import ( ) type project struct { - c *config.Config + c *clitypes.Config } -func newProjectCmd(c *config.Config) *cobra.Command { +func newProjectCmd(c *clitypes.Config) *cobra.Command { w := &project{ c: c, } cmdsConfig := &genericcli.CmdsConfig[*apiv2.ProjectServiceCreateRequest, *apiv2.ProjectServiceUpdateRequest, *apiv2.Project]{ - BinaryName: config.BinaryName, + BinaryName: clitypes.BinaryName, GenericCLI: genericcli.NewGenericCLI(w).WithFS(c.Fs), Singular: "project", Plural: "projects", diff --git a/cmd/api/v1/tenant.go b/cmd/api/v1/tenant.go index c26fe7e..2f41d0c 100644 --- a/cmd/api/v1/tenant.go +++ b/cmd/api/v1/tenant.go @@ -6,8 +6,8 @@ import ( "github.com/dustin/go-humanize" "github.com/fatih/color" apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" - "github.com/metal-stack/cli/cmd/config" "github.com/metal-stack/cli/cmd/sorters" + clitypes "github.com/metal-stack/metal-lib/pkg/commands/types" "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" @@ -16,16 +16,16 @@ import ( ) type tenant struct { - c *config.Config + c *clitypes.Config } -func newTenantCmd(c *config.Config) *cobra.Command { +func newTenantCmd(c *clitypes.Config) *cobra.Command { w := &tenant{ c: c, } cmdsConfig := &genericcli.CmdsConfig[*apiv2.TenantServiceCreateRequest, *apiv2.TenantServiceUpdateRequest, *apiv2.Tenant]{ - BinaryName: config.BinaryName, + BinaryName: clitypes.BinaryName, GenericCLI: genericcli.NewGenericCLI(w).WithFS(c.Fs), Singular: "tenant", Plural: "tenants", diff --git a/cmd/api/v1/token.go b/cmd/api/v1/token.go index 7ca3cc7..9161ea5 100644 --- a/cmd/api/v1/token.go +++ b/cmd/api/v1/token.go @@ -6,8 +6,8 @@ import ( "time" apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" - "github.com/metal-stack/cli/cmd/config" "github.com/metal-stack/cli/cmd/sorters" + clitypes "github.com/metal-stack/metal-lib/pkg/commands/types" "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" @@ -17,16 +17,16 @@ import ( ) type token struct { - c *config.Config + c *clitypes.Config } -func newTokenCmd(c *config.Config) *cobra.Command { +func newTokenCmd(c *clitypes.Config) *cobra.Command { w := &token{ c: c, } cmdsConfig := &genericcli.CmdsConfig[*apiv2.TokenServiceCreateRequest, *apiv2.TokenServiceUpdateRequest, *apiv2.Token]{ - BinaryName: config.BinaryName, + BinaryName: clitypes.BinaryName, GenericCLI: genericcli.NewGenericCLI(w).WithFS(c.Fs), Singular: "token", Plural: "tokens", diff --git a/cmd/api/v1/user.go b/cmd/api/v1/user.go index 329944e..09540e2 100644 --- a/cmd/api/v1/user.go +++ b/cmd/api/v1/user.go @@ -4,17 +4,17 @@ import ( "fmt" apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" - "github.com/metal-stack/cli/cmd/config" + clitypes "github.com/metal-stack/metal-lib/pkg/commands/types" "github.com/metal-stack/metal-lib/pkg/genericcli" "github.com/metal-stack/metal-lib/pkg/genericcli/printers" "github.com/spf13/cobra" ) type user struct { - c *config.Config + c *clitypes.Config } -func newUserCmd(c *config.Config) *cobra.Command { +func newUserCmd(c *clitypes.Config) *cobra.Command { w := &user{ c: c, } @@ -22,7 +22,7 @@ func newUserCmd(c *config.Config) *cobra.Command { gcli := genericcli.NewGenericCLI(w).WithFS(c.Fs) cmdsConfig := &genericcli.CmdsConfig[any, any, *apiv2.User]{ - BinaryName: config.BinaryName, + BinaryName: clitypes.BinaryName, GenericCLI: gcli, Singular: "user", Plural: "users", diff --git a/cmd/api/v1/version.go b/cmd/api/v1/version.go index c62219f..b0b42de 100644 --- a/cmd/api/v1/version.go +++ b/cmd/api/v1/version.go @@ -4,7 +4,7 @@ import ( "fmt" v1 "github.com/metal-stack/api/go/metalstack/api/v2" - "github.com/metal-stack/cli/cmd/config" + clitypes "github.com/metal-stack/metal-lib/pkg/commands/types" "github.com/metal-stack/v" "github.com/spf13/cobra" ) @@ -14,7 +14,7 @@ type version struct { Server *v1.Version } -func newVersionCmd(c *config.Config) *cobra.Command { +func newVersionCmd(c *clitypes.Config) *cobra.Command { versionCmd := &cobra.Command{ Use: "version", Short: "print the client and server version information", diff --git a/cmd/common_test.go b/cmd/common_test.go index ce7f960..301e85c 100644 --- a/cmd/common_test.go +++ b/cmd/common_test.go @@ -16,8 +16,8 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" apitests "github.com/metal-stack/api/go/tests" - "github.com/metal-stack/cli/cmd/completion" - "github.com/metal-stack/cli/cmd/config" + "github.com/metal-stack/metal-lib/pkg/commands/helpers/completion" + clitypes "github.com/metal-stack/metal-lib/pkg/commands/types" "github.com/metal-stack/metal-lib/pkg/pointer" "github.com/metal-stack/metal-lib/pkg/testcommon" "github.com/spf13/afero" @@ -61,7 +61,7 @@ func (c *Test[R]) TestCmd(t *testing.T) { _, _, conf := c.newMockConfig(t) cmd := newRootCmd(conf) - os.Args = append([]string{config.BinaryName}, c.Cmd(c.Want)...) + os.Args = append([]string{clitypes.BinaryName}, c.Cmd(c.Want)...) err := cmd.Execute() if diff := cmp.Diff(c.WantErr, err, testcommon.IgnoreUnexported(), testcommon.ErrorStringComparer()); diff != "" { @@ -75,7 +75,7 @@ func (c *Test[R]) TestCmd(t *testing.T) { _, out, conf := c.newMockConfig(t) cmd := newRootCmd(conf) - os.Args = append([]string{config.BinaryName}, c.Cmd(c.Want)...) + os.Args = append([]string{clitypes.BinaryName}, c.Cmd(c.Want)...) os.Args = append(os.Args, format.Args()...) err := cmd.Execute() @@ -86,7 +86,7 @@ func (c *Test[R]) TestCmd(t *testing.T) { } } -func (c *Test[R]) newMockConfig(t *testing.T) (any, *bytes.Buffer, *config.Config) { +func (c *Test[R]) newMockConfig(t *testing.T) (any, *bytes.Buffer, *clitypes.Config) { mock := apitests.New(t) fs := afero.NewMemMapFs() @@ -101,7 +101,7 @@ func (c *Test[R]) newMockConfig(t *testing.T) (any, *bytes.Buffer, *config.Confi var ( out bytes.Buffer - config = &config.Config{ + config = &clitypes.Config{ Fs: fs, Out: &out, In: in, @@ -128,7 +128,7 @@ func AssertExhaustiveArgs(t *testing.T, args []string, exclude ...string) { return fmt.Errorf("not exhaustive: does not contain %q", prefix) } - root := newRootCmd(&config.Config{}) + root := newRootCmd(&clitypes.Config{}) cmd, args, err := root.Find(args) require.NoError(t, err) diff --git a/cmd/completion/completion.go b/cmd/completion/completion.go deleted file mode 100644 index 0c5a3d3..0000000 --- a/cmd/completion/completion.go +++ /dev/null @@ -1,18 +0,0 @@ -package completion - -import ( - "context" - - "github.com/metal-stack/api/go/client" - "github.com/spf13/cobra" -) - -type Completion struct { - Client client.Client - Project string - Ctx context.Context -} - -func OutputFormatListCompletion(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return []string{"table", "wide", "markdown", "json", "yaml", "template"}, cobra.ShellCompDirectiveNoFileComp -} diff --git a/cmd/config/context.go b/cmd/config/context.go index 2ba9110..d912156 100644 --- a/cmd/config/context.go +++ b/cmd/config/context.go @@ -1,150 +1 @@ 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/login.go b/cmd/login.go index 739d77d..0276ec6 100644 --- a/cmd/login.go +++ b/cmd/login.go @@ -11,7 +11,7 @@ import ( "github.com/fatih/color" apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" - "github.com/metal-stack/cli/cmd/config" + clitypes "github.com/metal-stack/metal-lib/pkg/commands/types" "github.com/metal-stack/metal-lib/pkg/genericcli" "github.com/metal-stack/metal-lib/pkg/pointer" "github.com/spf13/cobra" @@ -20,10 +20,10 @@ import ( ) type login struct { - c *config.Config + c *clitypes.Config } -func newLoginCmd(c *config.Config) *cobra.Command { +func newLoginCmd(c *clitypes.Config) *cobra.Command { w := &login{ c: c, } diff --git a/cmd/logout.go b/cmd/logout.go index 19788e2..b35138d 100644 --- a/cmd/logout.go +++ b/cmd/logout.go @@ -10,17 +10,17 @@ import ( "time" "github.com/fatih/color" - "github.com/metal-stack/cli/cmd/config" + clitypes "github.com/metal-stack/metal-lib/pkg/commands/types" "github.com/metal-stack/metal-lib/pkg/genericcli" "github.com/spf13/cobra" "github.com/spf13/viper" ) type logout struct { - c *config.Config + c *clitypes.Config } -func newLogoutCmd(c *config.Config) *cobra.Command { +func newLogoutCmd(c *clitypes.Config) *cobra.Command { w := &logout{ c: c, } diff --git a/cmd/root.go b/cmd/root.go index 9bb0ce1..afee376 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -5,13 +5,14 @@ import ( "os" client "github.com/metal-stack/api/go/client" + "github.com/metal-stack/metal-lib/pkg/commands" + "github.com/metal-stack/metal-lib/pkg/commands/helpers/completion" + clitypes "github.com/metal-stack/metal-lib/pkg/commands/types" "github.com/metal-stack/metal-lib/pkg/genericcli" adminv2 "github.com/metal-stack/cli/cmd/admin/v1" apiv2 "github.com/metal-stack/cli/cmd/api/v1" - "github.com/metal-stack/cli/cmd/completion" - "github.com/metal-stack/cli/cmd/config" "github.com/spf13/afero" "github.com/spf13/cobra" "github.com/spf13/cobra/doc" @@ -20,7 +21,7 @@ import ( ) func Execute() { - cfg := &config.Config{ + cfg := &clitypes.Config{ Fs: afero.NewOsFs(), Out: os.Stdout, PromptOut: os.Stdout, @@ -40,9 +41,9 @@ func Execute() { } } -func newRootCmd(c *config.Config) *cobra.Command { +func newRootCmd(c *clitypes.Config) *cobra.Command { rootCmd := &cobra.Command{ - Use: config.BinaryName, + Use: clitypes.BinaryName, Aliases: []string{"m"}, Short: "cli for managing entities in metal-stack", Long: "", @@ -83,14 +84,14 @@ func newRootCmd(c *config.Config) *cobra.Command { }, } - rootCmd.AddCommand(newContextCmd(c), markdownCmd, newLoginCmd(c), newLogoutCmd(c)) + rootCmd.AddCommand(commands.NewContextCmd(c), markdownCmd, newLoginCmd(c), newLogoutCmd(c)) adminv2.AddCmds(rootCmd, c) apiv2.AddCmds(rootCmd, c) return rootCmd } -func initConfigWithViperCtx(c *config.Config) error { +func initConfigWithViperCtx(c *clitypes.Config) error { c.Context = c.MustDefaultContext() listPrinter, err := newPrinterFromCLI(c.Out) 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..fe56011 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" + clitypes "github.com/metal-stack/metal-lib/pkg/commands/types" "github.com/metal-stack/metal-lib/pkg/genericcli/printers" "github.com/metal-stack/metal-lib/pkg/pointer" ) @@ -27,7 +27,7 @@ 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: + case *clitypes.Contexts: return t.ContextTable(d, wide) case *apiv2.IP: diff --git a/cmd/tableprinters/context.go b/cmd/tableprinters/context.go index 3cafaae..ac8e77d 100644 --- a/cmd/tableprinters/context.go +++ b/cmd/tableprinters/context.go @@ -2,12 +2,12 @@ package tableprinters import ( "github.com/fatih/color" - "github.com/metal-stack/cli/cmd/config" + clitypes "github.com/metal-stack/metal-lib/pkg/commands/types" "github.com/metal-stack/metal-lib/pkg/pointer" "github.com/spf13/viper" ) -func (t *TablePrinter) ContextTable(data *config.Contexts, wide bool) ([]string, [][]string, error) { +func (t *TablePrinter) ContextTable(data *clitypes.Contexts, wide bool) ([]string, [][]string, error) { var ( header = []string{"", "Name", "Provider", "Default Project"} rows [][]string From 179f24b2b4cf0298660d1c96f9e5fb7fb364accc Mon Sep 17 00:00:00 2001 From: Philipp Mukosey Date: Wed, 22 Oct 2025 14:01:27 +0200 Subject: [PATCH 02/13] Revert "Migrate context command to metal-lib" This reverts commit ec27fd5e685302a0e1485f5794f4b5defab9dda7. --- cmd/admin/v1/commands.go | 5 +- cmd/admin/v1/image.go | 8 +- cmd/admin/v1/token.go | 9 +- cmd/api/v1/commands.go | 4 +- cmd/api/v1/health.go | 4 +- cmd/api/v1/image.go | 8 +- cmd/api/v1/ip.go | 8 +- cmd/api/v1/methods.go | 4 +- cmd/api/v1/project.go | 8 +- cmd/api/v1/tenant.go | 8 +- cmd/api/v1/token.go | 8 +- cmd/api/v1/user.go | 8 +- cmd/api/v1/version.go | 4 +- cmd/common_test.go | 14 +- cmd/completion/completion.go | 18 ++ cmd/config/context.go | 149 +++++++++++++++ cmd/context.go | 339 +++++++++++++++++++++++++++++++++++ cmd/login.go | 6 +- cmd/logout.go | 6 +- cmd/root.go | 15 +- cmd/sorters/context.go | 14 ++ cmd/tableprinters/common.go | 4 +- cmd/tableprinters/context.go | 4 +- 23 files changed, 586 insertions(+), 69 deletions(-) create mode 100644 cmd/completion/completion.go create mode 100644 cmd/context.go create mode 100644 cmd/sorters/context.go diff --git a/cmd/admin/v1/commands.go b/cmd/admin/v1/commands.go index cbf3602..ddfef8c 100644 --- a/cmd/admin/v1/commands.go +++ b/cmd/admin/v1/commands.go @@ -1,12 +1,11 @@ package v1 import ( + "github.com/metal-stack/cli/cmd/config" "github.com/spf13/cobra" - - clitypes "github.com/metal-stack/metal-lib/pkg/commands/types" ) -func AddCmds(cmd *cobra.Command, c *clitypes.Config) { +func AddCmds(cmd *cobra.Command, c *config.Config) { adminCmd := &cobra.Command{ Use: "admin", Short: "admin commands", diff --git a/cmd/admin/v1/image.go b/cmd/admin/v1/image.go index 6d30630..be9e7d0 100644 --- a/cmd/admin/v1/image.go +++ b/cmd/admin/v1/image.go @@ -7,7 +7,7 @@ import ( adminv2 "github.com/metal-stack/api/go/metalstack/admin/v2" apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" - clitypes "github.com/metal-stack/metal-lib/pkg/commands/types" + "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" @@ -17,17 +17,17 @@ import ( ) type image struct { - c *clitypes.Config + c *config.Config } -func newImageCmd(c *clitypes.Config) *cobra.Command { +func newImageCmd(c *config.Config) *cobra.Command { w := &image{ c: c, } gcli := genericcli.NewGenericCLI(w).WithFS(c.Fs) cmdsConfig := &genericcli.CmdsConfig[*adminv2.ImageServiceCreateRequest, *adminv2.ImageServiceUpdateRequest, *apiv2.Image]{ - BinaryName: clitypes.BinaryName, + BinaryName: config.BinaryName, GenericCLI: gcli, Singular: "image", Plural: "images", diff --git a/cmd/admin/v1/token.go b/cmd/admin/v1/token.go index 65ed024..91f0bbd 100644 --- a/cmd/admin/v1/token.go +++ b/cmd/admin/v1/token.go @@ -5,8 +5,7 @@ import ( adminv2 "github.com/metal-stack/api/go/metalstack/admin/v2" apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" - clitypes "github.com/metal-stack/metal-lib/pkg/commands/types" - + "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/genericcli/printers" @@ -16,16 +15,16 @@ import ( ) type token struct { - c *clitypes.Config + c *config.Config } -func newTokenCmd(c *clitypes.Config) *cobra.Command { +func newTokenCmd(c *config.Config) *cobra.Command { w := &token{ c: c, } cmdsConfig := &genericcli.CmdsConfig[any, any, *apiv2.Token]{ - BinaryName: clitypes.BinaryName, + BinaryName: config.BinaryName, GenericCLI: genericcli.NewGenericCLI[any, any, *apiv2.Token](w).WithFS(c.Fs), Singular: "token", Plural: "tokens", diff --git a/cmd/api/v1/commands.go b/cmd/api/v1/commands.go index d0d7752..1e62d60 100644 --- a/cmd/api/v1/commands.go +++ b/cmd/api/v1/commands.go @@ -1,11 +1,11 @@ package v1 import ( - clitypes "github.com/metal-stack/metal-lib/pkg/commands/types" + "github.com/metal-stack/cli/cmd/config" "github.com/spf13/cobra" ) -func AddCmds(cmd *cobra.Command, c *clitypes.Config) { +func AddCmds(cmd *cobra.Command, c *config.Config) { cmd.AddCommand(newVersionCmd(c)) cmd.AddCommand(newHealthCmd(c)) cmd.AddCommand(newTokenCmd(c)) diff --git a/cmd/api/v1/health.go b/cmd/api/v1/health.go index ff01654..eac6678 100644 --- a/cmd/api/v1/health.go +++ b/cmd/api/v1/health.go @@ -4,11 +4,11 @@ import ( "fmt" v1 "github.com/metal-stack/api/go/metalstack/api/v2" - clitypes "github.com/metal-stack/metal-lib/pkg/commands/types" + "github.com/metal-stack/cli/cmd/config" "github.com/spf13/cobra" ) -func newHealthCmd(c *clitypes.Config) *cobra.Command { +func newHealthCmd(c *config.Config) *cobra.Command { healthCmd := &cobra.Command{ Use: "health", Short: "print the client and server health information", diff --git a/cmd/api/v1/image.go b/cmd/api/v1/image.go index d211ca3..14da8c9 100644 --- a/cmd/api/v1/image.go +++ b/cmd/api/v1/image.go @@ -5,7 +5,7 @@ import ( "strings" apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" - clitypes "github.com/metal-stack/metal-lib/pkg/commands/types" + "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" @@ -14,10 +14,10 @@ import ( ) type image struct { - c *clitypes.Config + c *config.Config } -func newImageCmd(c *clitypes.Config) *cobra.Command { +func newImageCmd(c *config.Config) *cobra.Command { w := &image{ c: c, } @@ -25,7 +25,7 @@ func newImageCmd(c *clitypes.Config) *cobra.Command { gcli := genericcli.NewGenericCLI(w).WithFS(c.Fs) cmdsConfig := &genericcli.CmdsConfig[any, any, *apiv2.Image]{ - BinaryName: clitypes.BinaryName, + BinaryName: config.BinaryName, GenericCLI: gcli, Singular: "image", Plural: "images", diff --git a/cmd/api/v1/ip.go b/cmd/api/v1/ip.go index 2f3da68..3c757fa 100644 --- a/cmd/api/v1/ip.go +++ b/cmd/api/v1/ip.go @@ -4,9 +4,9 @@ import ( "fmt" apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" + "github.com/metal-stack/cli/cmd/config" "github.com/metal-stack/cli/cmd/sorters" "github.com/metal-stack/cli/pkg/helpers" - clitypes "github.com/metal-stack/metal-lib/pkg/commands/types" "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" @@ -15,16 +15,16 @@ import ( ) type ip struct { - c *clitypes.Config + c *config.Config } -func newIPCmd(c *clitypes.Config) *cobra.Command { +func newIPCmd(c *config.Config) *cobra.Command { w := &ip{ c: c, } cmdsConfig := &genericcli.CmdsConfig[*apiv2.IPServiceCreateRequest, *apiv2.IPServiceUpdateRequest, *apiv2.IP]{ - BinaryName: clitypes.BinaryName, + BinaryName: config.BinaryName, GenericCLI: genericcli.NewGenericCLI(w).WithFS(c.Fs), Singular: "ip", Plural: "ips", diff --git a/cmd/api/v1/methods.go b/cmd/api/v1/methods.go index 15685fe..ff71c49 100644 --- a/cmd/api/v1/methods.go +++ b/cmd/api/v1/methods.go @@ -5,13 +5,13 @@ import ( "sort" apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" - clitypes "github.com/metal-stack/metal-lib/pkg/commands/types" + "github.com/metal-stack/cli/cmd/config" "github.com/metal-stack/metal-lib/pkg/genericcli/printers" "github.com/spf13/cobra" "github.com/spf13/viper" ) -func newMethodsCmd(c *clitypes.Config) *cobra.Command { +func newMethodsCmd(c *config.Config) *cobra.Command { methodCmd := &cobra.Command{ Use: "api-methods", Short: "show available api-methods of the metal-stack.io api", diff --git a/cmd/api/v1/project.go b/cmd/api/v1/project.go index 541a712..023708e 100644 --- a/cmd/api/v1/project.go +++ b/cmd/api/v1/project.go @@ -6,8 +6,8 @@ import ( "github.com/dustin/go-humanize" "github.com/fatih/color" apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" + "github.com/metal-stack/cli/cmd/config" "github.com/metal-stack/cli/cmd/sorters" - clitypes "github.com/metal-stack/metal-lib/pkg/commands/types" "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" @@ -16,16 +16,16 @@ import ( ) type project struct { - c *clitypes.Config + c *config.Config } -func newProjectCmd(c *clitypes.Config) *cobra.Command { +func newProjectCmd(c *config.Config) *cobra.Command { w := &project{ c: c, } cmdsConfig := &genericcli.CmdsConfig[*apiv2.ProjectServiceCreateRequest, *apiv2.ProjectServiceUpdateRequest, *apiv2.Project]{ - BinaryName: clitypes.BinaryName, + BinaryName: config.BinaryName, GenericCLI: genericcli.NewGenericCLI(w).WithFS(c.Fs), Singular: "project", Plural: "projects", diff --git a/cmd/api/v1/tenant.go b/cmd/api/v1/tenant.go index 2f41d0c..c26fe7e 100644 --- a/cmd/api/v1/tenant.go +++ b/cmd/api/v1/tenant.go @@ -6,8 +6,8 @@ import ( "github.com/dustin/go-humanize" "github.com/fatih/color" apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" + "github.com/metal-stack/cli/cmd/config" "github.com/metal-stack/cli/cmd/sorters" - clitypes "github.com/metal-stack/metal-lib/pkg/commands/types" "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" @@ -16,16 +16,16 @@ import ( ) type tenant struct { - c *clitypes.Config + c *config.Config } -func newTenantCmd(c *clitypes.Config) *cobra.Command { +func newTenantCmd(c *config.Config) *cobra.Command { w := &tenant{ c: c, } cmdsConfig := &genericcli.CmdsConfig[*apiv2.TenantServiceCreateRequest, *apiv2.TenantServiceUpdateRequest, *apiv2.Tenant]{ - BinaryName: clitypes.BinaryName, + BinaryName: config.BinaryName, GenericCLI: genericcli.NewGenericCLI(w).WithFS(c.Fs), Singular: "tenant", Plural: "tenants", diff --git a/cmd/api/v1/token.go b/cmd/api/v1/token.go index 9161ea5..7ca3cc7 100644 --- a/cmd/api/v1/token.go +++ b/cmd/api/v1/token.go @@ -6,8 +6,8 @@ import ( "time" apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" + "github.com/metal-stack/cli/cmd/config" "github.com/metal-stack/cli/cmd/sorters" - clitypes "github.com/metal-stack/metal-lib/pkg/commands/types" "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" @@ -17,16 +17,16 @@ import ( ) type token struct { - c *clitypes.Config + c *config.Config } -func newTokenCmd(c *clitypes.Config) *cobra.Command { +func newTokenCmd(c *config.Config) *cobra.Command { w := &token{ c: c, } cmdsConfig := &genericcli.CmdsConfig[*apiv2.TokenServiceCreateRequest, *apiv2.TokenServiceUpdateRequest, *apiv2.Token]{ - BinaryName: clitypes.BinaryName, + BinaryName: config.BinaryName, GenericCLI: genericcli.NewGenericCLI(w).WithFS(c.Fs), Singular: "token", Plural: "tokens", diff --git a/cmd/api/v1/user.go b/cmd/api/v1/user.go index 09540e2..329944e 100644 --- a/cmd/api/v1/user.go +++ b/cmd/api/v1/user.go @@ -4,17 +4,17 @@ import ( "fmt" apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" - clitypes "github.com/metal-stack/metal-lib/pkg/commands/types" + "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/spf13/cobra" ) type user struct { - c *clitypes.Config + c *config.Config } -func newUserCmd(c *clitypes.Config) *cobra.Command { +func newUserCmd(c *config.Config) *cobra.Command { w := &user{ c: c, } @@ -22,7 +22,7 @@ func newUserCmd(c *clitypes.Config) *cobra.Command { gcli := genericcli.NewGenericCLI(w).WithFS(c.Fs) cmdsConfig := &genericcli.CmdsConfig[any, any, *apiv2.User]{ - BinaryName: clitypes.BinaryName, + BinaryName: config.BinaryName, GenericCLI: gcli, Singular: "user", Plural: "users", diff --git a/cmd/api/v1/version.go b/cmd/api/v1/version.go index b0b42de..c62219f 100644 --- a/cmd/api/v1/version.go +++ b/cmd/api/v1/version.go @@ -4,7 +4,7 @@ import ( "fmt" v1 "github.com/metal-stack/api/go/metalstack/api/v2" - clitypes "github.com/metal-stack/metal-lib/pkg/commands/types" + "github.com/metal-stack/cli/cmd/config" "github.com/metal-stack/v" "github.com/spf13/cobra" ) @@ -14,7 +14,7 @@ type version struct { Server *v1.Version } -func newVersionCmd(c *clitypes.Config) *cobra.Command { +func newVersionCmd(c *config.Config) *cobra.Command { versionCmd := &cobra.Command{ Use: "version", Short: "print the client and server version information", diff --git a/cmd/common_test.go b/cmd/common_test.go index 301e85c..ce7f960 100644 --- a/cmd/common_test.go +++ b/cmd/common_test.go @@ -16,8 +16,8 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" apitests "github.com/metal-stack/api/go/tests" - "github.com/metal-stack/metal-lib/pkg/commands/helpers/completion" - clitypes "github.com/metal-stack/metal-lib/pkg/commands/types" + "github.com/metal-stack/cli/cmd/completion" + "github.com/metal-stack/cli/cmd/config" "github.com/metal-stack/metal-lib/pkg/pointer" "github.com/metal-stack/metal-lib/pkg/testcommon" "github.com/spf13/afero" @@ -61,7 +61,7 @@ func (c *Test[R]) TestCmd(t *testing.T) { _, _, conf := c.newMockConfig(t) cmd := newRootCmd(conf) - os.Args = append([]string{clitypes.BinaryName}, c.Cmd(c.Want)...) + os.Args = append([]string{config.BinaryName}, c.Cmd(c.Want)...) err := cmd.Execute() if diff := cmp.Diff(c.WantErr, err, testcommon.IgnoreUnexported(), testcommon.ErrorStringComparer()); diff != "" { @@ -75,7 +75,7 @@ func (c *Test[R]) TestCmd(t *testing.T) { _, out, conf := c.newMockConfig(t) cmd := newRootCmd(conf) - os.Args = append([]string{clitypes.BinaryName}, c.Cmd(c.Want)...) + os.Args = append([]string{config.BinaryName}, c.Cmd(c.Want)...) os.Args = append(os.Args, format.Args()...) err := cmd.Execute() @@ -86,7 +86,7 @@ func (c *Test[R]) TestCmd(t *testing.T) { } } -func (c *Test[R]) newMockConfig(t *testing.T) (any, *bytes.Buffer, *clitypes.Config) { +func (c *Test[R]) newMockConfig(t *testing.T) (any, *bytes.Buffer, *config.Config) { mock := apitests.New(t) fs := afero.NewMemMapFs() @@ -101,7 +101,7 @@ func (c *Test[R]) newMockConfig(t *testing.T) (any, *bytes.Buffer, *clitypes.Con var ( out bytes.Buffer - config = &clitypes.Config{ + config = &config.Config{ Fs: fs, Out: &out, In: in, @@ -128,7 +128,7 @@ func AssertExhaustiveArgs(t *testing.T, args []string, exclude ...string) { return fmt.Errorf("not exhaustive: does not contain %q", prefix) } - root := newRootCmd(&clitypes.Config{}) + root := newRootCmd(&config.Config{}) cmd, args, err := root.Find(args) require.NoError(t, err) diff --git a/cmd/completion/completion.go b/cmd/completion/completion.go new file mode 100644 index 0000000..0c5a3d3 --- /dev/null +++ b/cmd/completion/completion.go @@ -0,0 +1,18 @@ +package completion + +import ( + "context" + + "github.com/metal-stack/api/go/client" + "github.com/spf13/cobra" +) + +type Completion struct { + Client client.Client + Project string + Ctx context.Context +} + +func OutputFormatListCompletion(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return []string{"table", "wide", "markdown", "json", "yaml", "template"}, cobra.ShellCompDirectiveNoFileComp +} diff --git a/cmd/config/context.go b/cmd/config/context.go index d912156..2ba9110 100644 --- a/cmd/config/context.go +++ b/cmd/config/context.go @@ -1 +1,150 @@ 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 new file mode 100644 index 0000000..99d654f --- /dev/null +++ b/cmd/context.go @@ -0,0 +1,339 @@ +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/login.go b/cmd/login.go index 0276ec6..739d77d 100644 --- a/cmd/login.go +++ b/cmd/login.go @@ -11,7 +11,7 @@ import ( "github.com/fatih/color" apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" - clitypes "github.com/metal-stack/metal-lib/pkg/commands/types" + "github.com/metal-stack/cli/cmd/config" "github.com/metal-stack/metal-lib/pkg/genericcli" "github.com/metal-stack/metal-lib/pkg/pointer" "github.com/spf13/cobra" @@ -20,10 +20,10 @@ import ( ) type login struct { - c *clitypes.Config + c *config.Config } -func newLoginCmd(c *clitypes.Config) *cobra.Command { +func newLoginCmd(c *config.Config) *cobra.Command { w := &login{ c: c, } diff --git a/cmd/logout.go b/cmd/logout.go index b35138d..19788e2 100644 --- a/cmd/logout.go +++ b/cmd/logout.go @@ -10,17 +10,17 @@ import ( "time" "github.com/fatih/color" - clitypes "github.com/metal-stack/metal-lib/pkg/commands/types" + "github.com/metal-stack/cli/cmd/config" "github.com/metal-stack/metal-lib/pkg/genericcli" "github.com/spf13/cobra" "github.com/spf13/viper" ) type logout struct { - c *clitypes.Config + c *config.Config } -func newLogoutCmd(c *clitypes.Config) *cobra.Command { +func newLogoutCmd(c *config.Config) *cobra.Command { w := &logout{ c: c, } diff --git a/cmd/root.go b/cmd/root.go index afee376..9bb0ce1 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -5,14 +5,13 @@ import ( "os" client "github.com/metal-stack/api/go/client" - "github.com/metal-stack/metal-lib/pkg/commands" - "github.com/metal-stack/metal-lib/pkg/commands/helpers/completion" - clitypes "github.com/metal-stack/metal-lib/pkg/commands/types" "github.com/metal-stack/metal-lib/pkg/genericcli" adminv2 "github.com/metal-stack/cli/cmd/admin/v1" apiv2 "github.com/metal-stack/cli/cmd/api/v1" + "github.com/metal-stack/cli/cmd/completion" + "github.com/metal-stack/cli/cmd/config" "github.com/spf13/afero" "github.com/spf13/cobra" "github.com/spf13/cobra/doc" @@ -21,7 +20,7 @@ import ( ) func Execute() { - cfg := &clitypes.Config{ + cfg := &config.Config{ Fs: afero.NewOsFs(), Out: os.Stdout, PromptOut: os.Stdout, @@ -41,9 +40,9 @@ func Execute() { } } -func newRootCmd(c *clitypes.Config) *cobra.Command { +func newRootCmd(c *config.Config) *cobra.Command { rootCmd := &cobra.Command{ - Use: clitypes.BinaryName, + Use: config.BinaryName, Aliases: []string{"m"}, Short: "cli for managing entities in metal-stack", Long: "", @@ -84,14 +83,14 @@ func newRootCmd(c *clitypes.Config) *cobra.Command { }, } - rootCmd.AddCommand(commands.NewContextCmd(c), markdownCmd, newLoginCmd(c), newLogoutCmd(c)) + rootCmd.AddCommand(newContextCmd(c), markdownCmd, newLoginCmd(c), newLogoutCmd(c)) adminv2.AddCmds(rootCmd, c) apiv2.AddCmds(rootCmd, c) return rootCmd } -func initConfigWithViperCtx(c *clitypes.Config) error { +func initConfigWithViperCtx(c *config.Config) error { c.Context = c.MustDefaultContext() listPrinter, err := newPrinterFromCLI(c.Out) diff --git a/cmd/sorters/context.go b/cmd/sorters/context.go new file mode 100644 index 0000000..9cf8886 --- /dev/null +++ b/cmd/sorters/context.go @@ -0,0 +1,14 @@ +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 fe56011..19c30e7 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" - clitypes "github.com/metal-stack/metal-lib/pkg/commands/types" + "github.com/metal-stack/cli/cmd/config" "github.com/metal-stack/metal-lib/pkg/genericcli/printers" "github.com/metal-stack/metal-lib/pkg/pointer" ) @@ -27,7 +27,7 @@ func (t *TablePrinter) SetPrinter(printer *printers.TablePrinter) { func (t *TablePrinter) ToHeaderAndRows(data any, wide bool) ([]string, [][]string, error) { switch d := data.(type) { - case *clitypes.Contexts: + case *config.Contexts: return t.ContextTable(d, wide) case *apiv2.IP: diff --git a/cmd/tableprinters/context.go b/cmd/tableprinters/context.go index ac8e77d..3cafaae 100644 --- a/cmd/tableprinters/context.go +++ b/cmd/tableprinters/context.go @@ -2,12 +2,12 @@ package tableprinters import ( "github.com/fatih/color" - clitypes "github.com/metal-stack/metal-lib/pkg/commands/types" + "github.com/metal-stack/cli/cmd/config" "github.com/metal-stack/metal-lib/pkg/pointer" "github.com/spf13/viper" ) -func (t *TablePrinter) ContextTable(data *clitypes.Contexts, wide bool) ([]string, [][]string, error) { +func (t *TablePrinter) ContextTable(data *config.Contexts, wide bool) ([]string, [][]string, error) { var ( header = []string{"", "Name", "Provider", "Default Project"} rows [][]string From 180be4d326320cf11ff0484985ee4276b64141d5 Mon Sep 17 00:00:00 2001 From: Philipp Mukosey Date: Wed, 22 Oct 2025 14:05:36 +0200 Subject: [PATCH 03/13] Use context command templates from metal-stack/metal-lib@0704144 --- cmd/context.go | 37 +++++++++++-------------------------- 1 file changed, 11 insertions(+), 26 deletions(-) diff --git a/cmd/context.go b/cmd/context.go index 99d654f..e61d4ab 100644 --- a/cmd/context.go +++ b/cmd/context.go @@ -6,6 +6,7 @@ import ( "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/cmd" "github.com/metal-stack/metal-lib/pkg/genericcli" "github.com/metal-stack/metal-lib/pkg/pointer" "github.com/spf13/cobra" @@ -21,28 +22,20 @@ func newContextCmd(c *config.Config) *cobra.Command { c: c, } - contextCmd := &cobra.Command{ - Use: "context", - Aliases: []string{"ctx"}, - Short: "manage cli contexts", - Long: "you can switch back and forth contexts with \"-\"", + contextCmd := cmd.ContextBaseCmd(&cmd.CmdConfig{ 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", + contextListCmd := cmd.ContextListCmd(&cmd.CmdConfig{ RunE: func(cmd *cobra.Command, args []string) error { return w.list() }, - } + }) contextSwitchCmd := &cobra.Command{ Use: "switch ", Short: "switch the cli context", @@ -68,24 +61,18 @@ func newContextCmd(c *config.Config) *cobra.Command { }, ValidArgsFunction: c.Completion.ProjectListCompletion, } - contextRemoveCmd := &cobra.Command{ - Use: "remove ", - Aliases: []string{"rm", "delete"}, - Short: "remove a cli context", + contextRemoveCmd := cmd.ContextRemoveCmd(&cmd.CmdConfig{ 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", + contextAddCmd := cmd.ContextAddCmd(&cmd.CmdConfig{ 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") @@ -95,14 +82,12 @@ func newContextCmd(c *config.Config) *cobra.Command { genericcli.Must(contextAddCmd.MarkFlagRequired("api-token")) - contextUpdateCmd := &cobra.Command{ - Use: "update ", - Short: "update a cli context", + contextUpdateCmd := cmd.ContextUpdateCmd(&cmd.CmdConfig{ 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") From 56b65b24a0b0aa6f1f01e702b3f0f891e3cca5aa Mon Sep 17 00:00:00 2001 From: Philipp Mukosey Date: Wed, 22 Oct 2025 15:36:24 +0200 Subject: [PATCH 04/13] Use MutateFns from metal-stack/metal-lib@e7d4324 --- cmd/context.go | 77 ++++++++++++++++++++++++++------------------------ 1 file changed, 40 insertions(+), 37 deletions(-) diff --git a/cmd/context.go b/cmd/context.go index e61d4ab..e395c1f 100644 --- a/cmd/context.go +++ b/cmd/context.go @@ -22,15 +22,6 @@ func newContextCmd(c *config.Config) *cobra.Command { c: c, } - contextCmd := cmd.ContextBaseCmd(&cmd.CmdConfig{ - RunE: func(cmd *cobra.Command, args []string) error { - if len(args) == 0 { - return w.list() - } - return w.set(args) - }, - }) - contextListCmd := cmd.ContextListCmd(&cmd.CmdConfig{ RunE: func(cmd *cobra.Command, args []string) error { return w.list() @@ -72,42 +63,54 @@ func newContextCmd(c *config.Config) *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { return w.add(args) }, + MutateFn: func(cmd *cobra.Command) { + cmd.Flags().String("api-url", "", "sets the api-url for this context") + cmd.Flags().String("api-token", "", "sets the api-token for this context") + cmd.Flags().String("default-project", "", "sets a default project to act on") + cmd.Flags().Duration("timeout", 0, "sets a default request timeout") + cmd.Flags().Bool("activate", false, "immediately switches to the new context") + cmd.Flags().String("provider", "", "sets the login provider for this context") + + genericcli.Must(cmd.MarkFlagRequired("api-token")) + }, }) - 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 := cmd.ContextUpdateCmd(&cmd.CmdConfig{ RunE: func(cmd *cobra.Command, args []string) error { return w.update(args) }, ValidArgsFunction: c.ContextListCompletion, + MutateFn: func(cmd *cobra.Command) { + cmd.Flags().String("api-url", "", "sets the api-url for this context") + cmd.Flags().String("api-token", "", "sets the api-token for this context") + cmd.Flags().String("default-project", "", "sets a default project to act on") + cmd.Flags().Duration("timeout", 0, "sets a default request timeout") + cmd.Flags().Bool("activate", false, "immediately switches to the new context") + cmd.Flags().String("provider", "", "sets the login provider for this context") + + genericcli.Must(cmd.RegisterFlagCompletionFunc("default-project", c.Completion.ProjectListCompletion)) + }, + }) + + return cmd.ContextBaseCmd(&cmd.CmdConfig{ + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) == 0 { + return w.list() + } + return w.set(args) + }, + MutateFn: func(cmd *cobra.Command) { + cmd.AddCommand( + contextListCmd, + contextSwitchCmd, + contextAddCmd, + contextUpdateCmd, + contextRemoveCmd, + contextShortCmd, + contextSetProjectCmd, + ) + }, }) - 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 { From fb2d9e19f470ad4cbe814f1f3978a1c7e067e12b Mon Sep 17 00:00:00 2001 From: Philipp Mukosey Date: Wed, 29 Oct 2025 15:43:50 +0100 Subject: [PATCH 05/13] Use genericCLI context cmd from metal-stack/metal-lib@96b15bf --- cmd/config/config.go | 52 ++---- cmd/config/context.go | 150 ---------------- cmd/context.go | 327 ----------------------------------- cmd/login.go | 15 +- cmd/logout.go | 8 +- cmd/root.go | 22 ++- cmd/sorters/context.go | 14 -- cmd/tableprinters/common.go | 4 - cmd/tableprinters/context.go | 40 ----- 9 files changed, 44 insertions(+), 588 deletions(-) delete mode 100644 cmd/config/context.go delete mode 100644 cmd/context.go delete mode 100644 cmd/sorters/context.go delete mode 100644 cmd/tableprinters/context.go diff --git a/cmd/config/config.go b/cmd/config/config.go index 67006b9..c611e1e 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" @@ -35,7 +34,8 @@ type Config struct { ListPrinter printers.Printer DescribePrinter printers.Printer Completion *completion.Completion - Context Context + Context genericcli.Context + ContextConfig genericcli.ContextConfig } func (c *Config) NewRequestContext() (context.Context, context.CancelFunc) { @@ -43,35 +43,13 @@ 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(genericcli.KeyTimeout) { + timeout = pointer.Pointer(viper.GetDuration(genericcli.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") @@ -102,27 +80,27 @@ func (c *Config) GetTenant() (string, error) { } func (c *Config) GetToken() string { - if viper.IsSet("api-token") { - return viper.GetString("api-token") + if viper.IsSet(genericcli.KeyAPIToken) { + return viper.GetString(genericcli.KeyAPIToken) } - return c.Context.Token + return c.Context.APIToken } func (c *Config) GetApiURL() string { - if viper.IsSet("api-url") { - return viper.GetString("api-url") + if viper.IsSet(genericcli.KeyAPIURL) { + return viper.GetString(genericcli.KeyAPIURL) } - if c.Context.ApiURL != nil { - return *c.Context.ApiURL + if c.Context.APIURL != nil { + return *c.Context.APIURL } // fallback to the default specified by viper - return viper.GetString("api-url") + return viper.GetString(genericcli.KeyAPIURL) } func (c *Config) GetProvider() string { - if viper.IsSet("provider") { - return viper.GetString("provider") + if viper.IsSet(genericcli.KeyProvider) { + return viper.GetString(genericcli.KeyProvider) } return c.Context.Provider } 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 e395c1f..0000000 --- a/cmd/context.go +++ /dev/null @@ -1,327 +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/cmd" - "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, - } - - contextListCmd := cmd.ContextListCmd(&cmd.CmdConfig{ - 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 := cmd.ContextRemoveCmd(&cmd.CmdConfig{ - RunE: func(cmd *cobra.Command, args []string) error { - return w.remove(args) - }, - ValidArgsFunction: c.ContextListCompletion, - }) - - contextAddCmd := cmd.ContextAddCmd(&cmd.CmdConfig{ - RunE: func(cmd *cobra.Command, args []string) error { - return w.add(args) - }, - MutateFn: func(cmd *cobra.Command) { - cmd.Flags().String("api-url", "", "sets the api-url for this context") - cmd.Flags().String("api-token", "", "sets the api-token for this context") - cmd.Flags().String("default-project", "", "sets a default project to act on") - cmd.Flags().Duration("timeout", 0, "sets a default request timeout") - cmd.Flags().Bool("activate", false, "immediately switches to the new context") - cmd.Flags().String("provider", "", "sets the login provider for this context") - - genericcli.Must(cmd.MarkFlagRequired("api-token")) - }, - }) - - contextUpdateCmd := cmd.ContextUpdateCmd(&cmd.CmdConfig{ - RunE: func(cmd *cobra.Command, args []string) error { - return w.update(args) - }, - ValidArgsFunction: c.ContextListCompletion, - MutateFn: func(cmd *cobra.Command) { - cmd.Flags().String("api-url", "", "sets the api-url for this context") - cmd.Flags().String("api-token", "", "sets the api-token for this context") - cmd.Flags().String("default-project", "", "sets a default project to act on") - cmd.Flags().Duration("timeout", 0, "sets a default request timeout") - cmd.Flags().Bool("activate", false, "immediately switches to the new context") - cmd.Flags().String("provider", "", "sets the login provider for this context") - - genericcli.Must(cmd.RegisterFlagCompletionFunc("default-project", c.Completion.ProjectListCompletion)) - }, - }) - - return cmd.ContextBaseCmd(&cmd.CmdConfig{ - RunE: func(cmd *cobra.Command, args []string) error { - if len(args) == 0 { - return w.list() - } - return w.set(args) - }, - MutateFn: func(cmd *cobra.Command) { - cmd.AddCommand( - contextListCmd, - contextSwitchCmd, - contextAddCmd, - contextUpdateCmd, - contextRemoveCmd, - contextShortCmd, - contextSetProjectCmd, - ) - }, - }) -} - -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/login.go b/cmd/login.go index 739d77d..f3c4272 100644 --- a/cmd/login.go +++ b/cmd/login.go @@ -53,7 +53,7 @@ func (l *login) login() error { return errors.New("provider must be specified") } - ctxs, err := l.c.GetContexts() + ctxs, err := l.c.ContextConfig.GetContexts() if err != nil { return err } @@ -62,14 +62,14 @@ func (l *login) login() error { if viper.IsSet("context") { ctxName = viper.GetString("context") } - ctx, ok := ctxs.Get(ctxName) + ctx, ok := ctxs.GetByName(ctxName) if !ok { - newCtx := l.c.MustDefaultContext() + newCtx := l.c.ContextConfig.MustDefaultContext() newCtx.Name = "default" if viper.IsSet("context") { newCtx.Name = viper.GetString("context") } - newCtx.ApiURL = pointer.Pointer(l.c.GetApiURL()) + newCtx.APIURL = pointer.Pointer(l.c.GetApiURL()) ctxs.Contexts = append(ctxs.Contexts, &newCtx) ctx = &newCtx } @@ -77,8 +77,7 @@ func (l *login) login() error { ctx.Provider = provider // switch into new context - ctxs.PreviousContext = ctxs.CurrentContext - ctxs.CurrentContext = ctx.Name + ctxs.PreviousContext, ctxs.CurrentContext = ctxs.CurrentContext, ctx.Name tokenChan := make(chan string) @@ -140,7 +139,7 @@ func (l *login) login() error { token = tokenResp.Secret } - ctx.Token = token + ctx.APIToken = token if ctx.DefaultProject == "" { mc, err := newApiClient(l.c.GetApiURL(), token) @@ -158,7 +157,7 @@ func (l *login) login() error { } } - err = l.c.WriteContexts(ctxs) + err = l.c.ContextConfig.WriteContexts(ctxs) if err != nil { return err } diff --git a/cmd/logout.go b/cmd/logout.go index 19788e2..e2ffaf7 100644 --- a/cmd/logout.go +++ b/cmd/logout.go @@ -42,12 +42,12 @@ func newLogoutCmd(c *config.Config) *cobra.Command { } func (l *logout) logout() error { - provider := viper.GetString("provider") + provider := viper.GetString(genericcli.KeyProvider) if provider == "" { return errors.New("provider must be specified") } - ctxs, err := l.c.GetContexts() + ctxs, err := l.c.ContextConfig.GetContexts() if err != nil { return err } @@ -57,9 +57,9 @@ func (l *logout) logout() error { ctxName = viper.GetString("context-name") } - ctx, ok := ctxs.Get(ctxName) + ctx, ok := ctxs.GetByName(ctxName) if !ok { - defaultCtx := l.c.MustDefaultContext() + defaultCtx := l.c.ContextConfig.MustDefaultContext() defaultCtx.Name = "default" ctxs.PreviousContext = ctxs.CurrentContext diff --git a/cmd/root.go b/cmd/root.go index 9bb0ce1..623d493 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/v1" apiv2 "github.com/metal-stack/cli/cmd/api/v1" @@ -66,8 +67,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(genericcli.KeyAPIURL, "https://api.metal-stack.io", "the url to the metal-stack.io api") + rootCmd.PersistentFlags().String(genericcli.KeyAPIToken, "", "the token used for api requests") genericcli.Must(viper.BindPFlags(rootCmd.PersistentFlags())) @@ -83,7 +84,20 @@ func newRootCmd(c *config.Config) *cobra.Command { }, } - rootCmd.AddCommand(newContextCmd(c), markdownCmd, newLoginCmd(c), newLogoutCmd(c)) + c.ContextConfig = genericcli.ContextConfig{ + BinaryName: config.BinaryName, + ConfigDirName: config.ConfigDir, + Fs: c.Fs, + DescribePrinter: func() printers.Printer { return c.DescribePrinter }, + ProjectListCompletion: c.Completion.ProjectListCompletion, + } + + rootCmd.AddCommand( + markdownCmd, + newLoginCmd(c), + newLogoutCmd(c), + genericcli.NewContextCmd(&c.ContextConfig), + ) adminv2.AddCmds(rootCmd, c) apiv2.AddCmds(rootCmd, c) @@ -91,7 +105,7 @@ func newRootCmd(c *config.Config) *cobra.Command { } func initConfigWithViperCtx(c *config.Config) error { - c.Context = c.MustDefaultContext() + c.Context = c.ContextConfig.MustDefaultContext() 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..f25be28 100644 --- a/cmd/tableprinters/common.go +++ b/cmd/tableprinters/common.go @@ -7,7 +7,6 @@ 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/printers" "github.com/metal-stack/metal-lib/pkg/pointer" ) @@ -27,9 +26,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: 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 -} From 7d6507e67a9d36fea467bc4fed2f2d20b18ff343 Mon Sep 17 00:00:00 2001 From: Philipp Mukosey Date: Tue, 4 Nov 2025 19:05:20 +0100 Subject: [PATCH 06/13] Add missing constant --- cmd/config/config.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cmd/config/config.go b/cmd/config/config.go index c611e1e..db46a13 100644 --- a/cmd/config/config.go +++ b/cmd/config/config.go @@ -22,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 { From 4d56f16f649660724632836118a212dad8714cde Mon Sep 17 00:00:00 2001 From: Philipp Mukosey Date: Fri, 7 Nov 2025 20:35:02 +0100 Subject: [PATCH 07/13] Use genericCLI context cmd from metal-stack/metal-lib@52dbce9 --- cmd/config/config.go | 31 ++++---------- cmd/login.go | 81 ++++++++++++++++++++++--------------- cmd/logout.go | 38 +++++------------ cmd/root.go | 19 ++++++--- cmd/tableprinters/common.go | 4 ++ 5 files changed, 82 insertions(+), 91 deletions(-) diff --git a/cmd/config/config.go b/cmd/config/config.go index db46a13..49474a7 100644 --- a/cmd/config/config.go +++ b/cmd/config/config.go @@ -35,8 +35,8 @@ type Config struct { ListPrinter printers.Printer DescribePrinter printers.Printer Completion *completion.Completion + ContextManager *genericcli.ContextManager Context genericcli.Context - ContextConfig genericcli.ContextConfig } func (c *Config) NewRequestContext() (context.Context, context.CancelFunc) { @@ -44,18 +44,15 @@ func (c *Config) NewRequestContext() (context.Context, context.CancelFunc) { if timeout == nil { timeout = pointer.Pointer(30 * time.Second) } - if viper.IsSet(genericcli.KeyTimeout) { - timeout = pointer.Pointer(viper.GetDuration(genericcli.KeyTimeout)) + if viper.IsSet(keyTimeout) { + timeout = pointer.Pointer(viper.GetDuration(keyTimeout)) } return context.WithTimeout(context.Background(), *timeout) } 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) { @@ -81,27 +78,13 @@ func (c *Config) GetTenant() (string, error) { } func (c *Config) GetToken() string { - if viper.IsSet(genericcli.KeyAPIToken) { - return viper.GetString(genericcli.KeyAPIToken) - } - return c.Context.APIToken + return c.Context.GetAPIToken() } func (c *Config) GetApiURL() string { - if viper.IsSet(genericcli.KeyAPIURL) { - return viper.GetString(genericcli.KeyAPIURL) - } - if c.Context.APIURL != nil { - return *c.Context.APIURL - } - - // fallback to the default specified by viper - return viper.GetString(genericcli.KeyAPIURL) + return c.Context.GetAPIURL() } func (c *Config) GetProvider() string { - if viper.IsSet(genericcli.KeyProvider) { - return viper.GetString(genericcli.KeyProvider) - } - return c.Context.Provider + return c.Context.GetProvider() } diff --git a/cmd/login.go b/cmd/login.go index f3c4272..61d5cde 100644 --- a/cmd/login.go +++ b/cmd/login.go @@ -1,6 +1,7 @@ package cmd import ( + "cmp" "context" "errors" "fmt" @@ -53,32 +54,6 @@ func (l *login) login() error { return errors.New("provider must be specified") } - ctxs, err := l.c.ContextConfig.GetContexts() - if err != nil { - return err - } - - ctxName := ctxs.CurrentContext - if viper.IsSet("context") { - ctxName = viper.GetString("context") - } - ctx, ok := ctxs.GetByName(ctxName) - if !ok { - newCtx := l.c.ContextConfig.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) { @@ -104,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 = exec.Command("xdg-open", url).Run() //nolint // TODO probably broken on MAC? if err != nil { return fmt.Errorf("error opening browser: %w", err) } @@ -139,9 +114,25 @@ func (l *login) login() error { token = tokenResp.Secret } - ctx.APIToken = token + var ctx *genericcli.Context + var defaultCtx bool + name := viper.GetString("context") + + 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 + } + + fmt.Println(defaultCtx) + fmt.Println(ctx) - if ctx.DefaultProject == "" { + var project string + if defaultCtx || ctx.DefaultProject == "" { mc, err := newApiClient(l.c.GetApiURL(), token) if err != nil { return err @@ -153,16 +144,40 @@ func (l *login) login() error { } if len(projects.Projects) > 0 { - ctx.DefaultProject = projects.Projects[0].Uuid + project = projects.Projects[0].Uuid } } - err = l.c.ContextConfig.WriteContexts(ctxs) + 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.ContextManager.Update(&genericcli.ContextUpdateRequest{ + Name: ctx.Name, + APIURL: ctx.APIURL, + APIToken: &token, + DefaultProject: pointer.Pointer(cmp.Or(ctx.DefaultProject, project)), + Provider: &provider, + Activate: 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 e2ffaf7..0329bd5 100644 --- a/cmd/logout.go +++ b/cmd/logout.go @@ -16,6 +16,11 @@ import ( "github.com/spf13/viper" ) +const ( + keyProvider = "provider" + keyContextName = "context-name" +) + type logout struct { c *config.Config } @@ -33,43 +38,20 @@ func newLogoutCmd(c *config.Config) *cobra.Command { }, } - logoutCmd.Flags().String("provider", "oidc", "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, "oidc", "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{"oidc"}, cobra.ShellCompDirectiveNoFileComp))) + genericcli.Must(logoutCmd.RegisterFlagCompletionFunc(keyProvider, cobra.FixedCompletions([]string{"oidc"}, cobra.ShellCompDirectiveNoFileComp))) return logoutCmd } func (l *logout) logout() error { - provider := viper.GetString(genericcli.KeyProvider) + provider := viper.GetString(keyProvider) if provider == "" { return errors.New("provider must be specified") } - ctxs, err := l.c.ContextConfig.GetContexts() - if err != nil { - return err - } - - ctxName := ctxs.CurrentContext - if viper.IsSet("context-name") { - ctxName = viper.GetString("context-name") - } - - ctx, ok := ctxs.GetByName(ctxName) - if !ok { - defaultCtx := l.c.ContextConfig.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 +69,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 = exec.Command("xdg-open", url).Run() //nolint // TODO probably broken on MAC? if err != nil { return fmt.Errorf("error opening browser: %w", err) } diff --git a/cmd/root.go b/cmd/root.go index 623d493..66e7f67 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -20,6 +20,11 @@ import ( "golang.org/x/net/context" ) +const ( + keyAPIURL = "api-url" + keyAPIToken = "api-token" +) + func Execute() { cfg := &config.Config{ Fs: afero.NewOsFs(), @@ -67,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(genericcli.KeyAPIURL, "https://api.metal-stack.io", "the url to the metal-stack.io api") - rootCmd.PersistentFlags().String(genericcli.KeyAPIToken, "", "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())) @@ -83,20 +88,22 @@ func newRootCmd(c *config.Config) *cobra.Command { recursiveAutoGenDisable(rootCmd) }, } - - c.ContextConfig = genericcli.ContextConfig{ + contextConfig := &genericcli.ContextConfig{ 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( markdownCmd, newLoginCmd(c), newLogoutCmd(c), - genericcli.NewContextCmd(&c.ContextConfig), + genericcli.NewContextCmd(contextConfig), ) adminv2.AddCmds(rootCmd, c) apiv2.AddCmds(rootCmd, c) @@ -105,7 +112,7 @@ func newRootCmd(c *config.Config) *cobra.Command { } func initConfigWithViperCtx(c *config.Config) error { - c.Context = c.ContextConfig.MustDefaultContext() + c.Context = *c.ContextManager.GetContextCurrentOrDefault() listPrinter, err := newPrinterFromCLI(c.Out) if err != nil { diff --git a/cmd/tableprinters/common.go b/cmd/tableprinters/common.go index f25be28..3097df7 100644 --- a/cmd/tableprinters/common.go +++ b/cmd/tableprinters/common.go @@ -7,6 +7,7 @@ import ( "time" apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" + "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" ) @@ -36,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: From edc799fa7f2d999908f2b7f7c2b8871cd4dfaa85 Mon Sep 17 00:00:00 2001 From: Philipp Mukosey Date: Fri, 7 Nov 2025 20:37:08 +0100 Subject: [PATCH 08/13] Add context list completion to login cmd --- cmd/login.go | 1 + 1 file changed, 1 insertion(+) diff --git a/cmd/login.go b/cmd/login.go index 61d5cde..f9f0e5b 100644 --- a/cmd/login.go +++ b/cmd/login.go @@ -43,6 +43,7 @@ func newLoginCmd(c *config.Config) *cobra.Command { genericcli.Must(loginCmd.Flags().MarkHidden("admin-role")) genericcli.Must(loginCmd.RegisterFlagCompletionFunc("provider", cobra.FixedCompletions([]string{"oidc"}, cobra.ShellCompDirectiveNoFileComp))) + genericcli.Must(loginCmd.RegisterFlagCompletionFunc("context", c.ContextManager.ContextListCompletion)) genericcli.Must(loginCmd.RegisterFlagCompletionFunc("admin-role", c.Completion.TokenAdminRoleCompletion)) return loginCmd From d53be809ad5ba77a28ca4311c93c11abc21afd49 Mon Sep 17 00:00:00 2001 From: Philipp Mukosey Date: Mon, 10 Nov 2025 11:14:59 +0100 Subject: [PATCH 09/13] Fix opening browser on mac and windows --- cmd/helpers.go | 20 ++++++++++++++++++++ cmd/login.go | 3 +-- cmd/logout.go | 3 +-- 3 files changed, 22 insertions(+), 4 deletions(-) create mode 100644 cmd/helpers.go 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 f9f0e5b..8b122e6 100644 --- a/cmd/login.go +++ b/cmd/login.go @@ -7,7 +7,6 @@ import ( "fmt" "net" "net/http" - "os/exec" "time" "github.com/fatih/color" @@ -80,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 // TODO probably broken on MAC? + err = openBrowser(url) if err != nil { return fmt.Errorf("error opening browser: %w", err) } diff --git a/cmd/logout.go b/cmd/logout.go index 0329bd5..2c70c15 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" @@ -69,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 // TODO probably broken on MAC? + err = openBrowser(url) if err != nil { return fmt.Errorf("error opening browser: %w", err) } From e5ade27adc0fb9eabe623ca2fc60663a55c6833c Mon Sep 17 00:00:00 2001 From: Philipp Mukosey Date: Thu, 29 Jan 2026 13:04:05 +0100 Subject: [PATCH 10/13] Small corrections --- cmd/login.go | 2 +- cmd/root.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/login.go b/cmd/login.go index 8b122e6..b1379d0 100644 --- a/cmd/login.go +++ b/cmd/login.go @@ -172,7 +172,7 @@ func (l *login) login() error { APIToken: &token, DefaultProject: pointer.Pointer(cmp.Or(ctx.DefaultProject, project)), Provider: &provider, - Activate: true, + IsCurrent: true, }) if err != nil { return err diff --git a/cmd/root.go b/cmd/root.go index 66e7f67..d9cdc2a 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -88,7 +88,7 @@ func newRootCmd(c *config.Config) *cobra.Command { recursiveAutoGenDisable(rootCmd) }, } - contextConfig := &genericcli.ContextConfig{ + contextConfig := &genericcli.ContextCmdConfig{ BinaryName: config.BinaryName, ConfigDirName: config.ConfigDir, Fs: c.Fs, From 7754df5f6c89dc57dc963c65c7e73230b339a20e Mon Sep 17 00:00:00 2001 From: Philipp Mukosey Date: Thu, 29 Jan 2026 16:05:23 +0100 Subject: [PATCH 11/13] Remove test printouts --- cmd/login.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/cmd/login.go b/cmd/login.go index d7d17db..90e15b1 100644 --- a/cmd/login.go +++ b/cmd/login.go @@ -128,9 +128,6 @@ func (l *login) login() error { defaultCtx = err != nil || ctx == nil } - fmt.Println(defaultCtx) - fmt.Println(ctx) - var project string if defaultCtx || ctx.DefaultProject == "" { mc, err := newApiClient(l.c.GetApiURL(), token) From ff34444eaef1fca7308456b6c6c665972921aa5a Mon Sep 17 00:00:00 2001 From: Philipp Mukosey Date: Thu, 29 Jan 2026 16:37:40 +0100 Subject: [PATCH 12/13] Update metal-lib version (commit hash) to fix the pipeline --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 68bf300cd42682f0ca4368ec35475cdf3f25f166 Mon Sep 17 00:00:00 2001 From: Philipp Mukosey Date: Thu, 29 Jan 2026 16:39:34 +0100 Subject: [PATCH 13/13] Add go.sum to fix the pipeline --- go.sum | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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=