From 01e0fd024bea5dd06888593a763bd8a9e6a8d4e8 Mon Sep 17 00:00:00 2001 From: "T. von Dein" Date: Wed, 3 Jun 2026 13:07:11 +0200 Subject: [PATCH] fix-index-show-completion, add more completions (#29) --- README.md | 24 ++++++++++++ cmd/ccr.go | 15 ++++++-- cmd/ccr_follower.go | 28 ++++++++++++++ cmd/completion.go | 89 +++++++++++++++++++++++++++++++++++++++++++++ cmd/index.go | 44 ++++++++++++---------- cmd/index_alias.go | 9 +++++ cmd/node.go | 1 + cmd/roles.go | 4 ++ pkg/es/index.go | 54 ++++++++++++++------------- pkg/es/role.go | 17 +++++++++ 10 files changed, 236 insertions(+), 49 deletions(-) create mode 100644 cmd/completion.go diff --git a/README.md b/README.md index 4740013..ead3bbf 100644 --- a/README.md +++ b/README.md @@ -145,6 +145,30 @@ make sudo make install ``` +# Development + +## To test completion + +Add the flag `--generate-shell-completion` to any command, e.g.: + +```console +./esctl role show --generate-shell-completion +machine_learning_admin +rollup_admin +editor +reporting_user +snapshot_user +fcn_admin +machine_learning_user +kibana_system +beats_admin +kibana_user +fcns_space +transport_client +transform_user +[..] +``` + # Report bugs [Please open an issue](https://codeberg.org/scip/esctl/issues). Thanks! diff --git a/cmd/ccr.go b/cmd/ccr.go index 4930587..8e19893 100644 --- a/cmd/ccr.go +++ b/cmd/ccr.go @@ -44,9 +44,10 @@ func Ccr(conf *cfg.Config) *cli.Command { func CcrStatus(conf *cfg.Config) *cli.Command { return &cli.Command{ - Name: "status", - Aliases: []string{"st"}, - Usage: "cross cluster replication status (yaml config with 2 clusters required)", + Name: "status", + Aliases: []string{"st"}, + Usage: "cross cluster replication status (yaml config with 2 clusters required)", + UsageText: "status ", Flags: []cli.Flag{ &cli.StringFlag{ @@ -57,6 +58,10 @@ func CcrStatus(conf *cfg.Config) *cli.Command { }, }, + ShellComplete: func(ctx context.Context, cmd *cli.Command) { + completeCluster(cmd) + }, + Action: func(ctx context.Context, cmd *cli.Command) error { leader := cmd.Args().Get(0) follower := cmd.Args().Get(1) @@ -109,6 +114,10 @@ func CcrRemoteInfo(conf *cfg.Config) *cli.Command { Usage: "show ccr remote info", UsageText: "info [options] []", + ShellComplete: func(ctx context.Context, cmd *cli.Command) { + completeIndex(cmd) + }, + Action: func(ctx context.Context, cmd *cli.Command) error { return es.CcrRemoteInfo(conf, cmd.Args().Get(0)) }, diff --git a/cmd/ccr_follower.go b/cmd/ccr_follower.go index 1dec4d5..20bb02b 100644 --- a/cmd/ccr_follower.go +++ b/cmd/ccr_follower.go @@ -59,6 +59,10 @@ func CcrFollowerRenew(conf *cfg.Config) *cli.Command { }, }, + ShellComplete: func(ctx context.Context, cmd *cli.Command) { + completeIndex(cmd) + }, + Action: func(ctx context.Context, cmd *cli.Command) error { args := cmd.Args() @@ -77,6 +81,10 @@ func CcrFollowerResume(conf *cfg.Config) *cli.Command { Usage: "resume ccr index to follow", UsageText: "resume [options] ", + ShellComplete: func(ctx context.Context, cmd *cli.Command) { + completeIndex(cmd) + }, + Action: func(ctx context.Context, cmd *cli.Command) error { args := cmd.Args() @@ -95,6 +103,10 @@ func CcrFollowerPause(conf *cfg.Config) *cli.Command { Usage: "pause ccr index to follow", UsageText: "pause [options] ", + ShellComplete: func(ctx context.Context, cmd *cli.Command) { + completeIndex(cmd) + }, + Action: func(ctx context.Context, cmd *cli.Command) error { args := cmd.Args() @@ -113,6 +125,10 @@ func CcrFollowerUnfollow(conf *cfg.Config) *cli.Command { Usage: "unfollow ccr follower index", UsageText: "unfollow [options] ", + ShellComplete: func(ctx context.Context, cmd *cli.Command) { + completeIndex(cmd) + }, + Action: func(ctx context.Context, cmd *cli.Command) error { args := cmd.Args() @@ -141,6 +157,10 @@ func CcrFollowerAdd(conf *cfg.Config) *cli.Command { }, }, + ShellComplete: func(ctx context.Context, cmd *cli.Command) { + completeIndex(cmd) + }, + Action: func(ctx context.Context, cmd *cli.Command) error { args := cmd.Args() @@ -160,6 +180,10 @@ func CcrFollowerDelete(conf *cfg.Config) *cli.Command { Usage: "delete ccr follower index", UsageText: "delete ", + ShellComplete: func(ctx context.Context, cmd *cli.Command) { + completeIndex(cmd) + }, + Action: func(ctx context.Context, cmd *cli.Command) error { args := cmd.Args() @@ -180,6 +204,10 @@ func CcrFollowerShow(conf *cfg.Config) *cli.Command { Usage: "show ccr follower index details", UsageText: "show ", + ShellComplete: func(ctx context.Context, cmd *cli.Command) { + completeIndex(cmd) + }, + Action: func(ctx context.Context, cmd *cli.Command) error { args := cmd.Args() diff --git a/cmd/completion.go b/cmd/completion.go new file mode 100644 index 0000000..8fb4459 --- /dev/null +++ b/cmd/completion.go @@ -0,0 +1,89 @@ +/* +Copyright © 2026 Thomas von Dein + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ +package cmd + +import ( + "fmt" + + "codeberg.org/scip/esctl/pkg/cfg" + "codeberg.org/scip/esctl/pkg/es" + + "github.com/urfave/cli/v3" +) + +func completeIndex(cmd *cli.Command) { + if cmd.NArg() > 0 { + return + } + + // FIXME: config should load from root.Before(), see https://github.com/urfave/cli/issues/2348 + // workaround: load it directly here + conf := cfg.NewConfig() + if err := conf.Init(); err != nil { + return + } + + indices, err := es.IndexNames(conf) + if err != nil { + return + } + + for _, index := range indices { + fmt.Println(index) + } + +} + +func completeRole(cmd *cli.Command) { + if cmd.NArg() > 0 { + return + } + + // FIXME: config should load from root.Before(), see https://github.com/urfave/cli/issues/2348 + // workaround: load it directly here + conf := cfg.NewConfig() + if err := conf.Init(); err != nil { + return + } + + roles, err := es.RoleNames(conf) + if err != nil { + return + } + + for _, role := range roles { + fmt.Println(role) + } + +} + +func completeCluster(cmd *cli.Command) { + if cmd.NArg() > 0 { + return + } + + // FIXME: config should load from root.Before(), see https://github.com/urfave/cli/issues/2348 + // workaround: load it directly here + conf := cfg.NewConfig() + if err := conf.Init(); err != nil { + return + } + + for cluster := range conf.Clusters { + fmt.Println(cluster) + } +} diff --git a/cmd/index.go b/cmd/index.go index 5579f7d..0064dfd 100644 --- a/cmd/index.go +++ b/cmd/index.go @@ -133,22 +133,8 @@ func IndexShow(conf *cfg.Config) *cli.Command { return es.IndexShow(conf, cmd.Args().Get(0)) }, - // FIXME: doesn't work at all - // FIXME: also it would ONLY work if the user uses env vars, -C would not be - // there when the completion output is being generated ShellComplete: func(ctx context.Context, cmd *cli.Command) { - if cmd.NArg() > 0 { - return - } - - indices, err := es.IndexNames(conf) - if err != nil { - return - } - - for _, index := range indices { - fmt.Println(index) - } + completeIndex(cmd) }, } } @@ -197,9 +183,14 @@ Valid field mapping types: integer, text, date, keyword`, func IndexDelete(conf *cfg.Config) *cli.Command { return &cli.Command{ - Name: "delete", - Aliases: []string{"rm"}, - Usage: "delete an index", + Name: "delete", + Aliases: []string{"rm"}, + Usage: "delete an index", + UsageText: "delete ", + + ShellComplete: func(ctx context.Context, cmd *cli.Command) { + completeIndex(cmd) + }, Action: func(ctx context.Context, cmd *cli.Command) error { index := cmd.Args().Get(0) @@ -214,8 +205,13 @@ func IndexDelete(conf *cfg.Config) *cli.Command { func IndexClose(conf *cfg.Config) *cli.Command { return &cli.Command{ - Name: "close", - Usage: "close an index", + Name: "close", + Usage: "close an index", + UsageText: "close ", + + ShellComplete: func(ctx context.Context, cmd *cli.Command) { + completeIndex(cmd) + }, Action: func(ctx context.Context, cmd *cli.Command) error { index := cmd.Args().Get(0) @@ -234,6 +230,10 @@ func IndexModify(conf *cfg.Config) *cli.Command { Usage: "modify an index", UsageText: "modify ", + ShellComplete: func(ctx context.Context, cmd *cli.Command) { + completeIndex(cmd) + }, + Flags: []cli.Flag{ &cli.IntFlag{ Name: "replicas", @@ -281,6 +281,10 @@ func IndexFields(conf *cfg.Config) *cli.Command { }, }, + ShellComplete: func(ctx context.Context, cmd *cli.Command) { + completeIndex(cmd) + }, + Action: func(ctx context.Context, cmd *cli.Command) error { index := cmd.Args().Get(0) if index == "" { diff --git a/cmd/index_alias.go b/cmd/index_alias.go index eb855f5..2153cf5 100644 --- a/cmd/index_alias.go +++ b/cmd/index_alias.go @@ -36,6 +36,7 @@ func IndexAlias(conf *cfg.Config) *cli.Command { IndexAliasCreate(conf), IndexAliasList(conf), IndexAliasDelete(conf), + // FIXME: implement IndexAliasShow + IndexAliasAdd //IndexAliasShow(conf), //IndexAliasAdd(conf), // see https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-indices-update-aliases }, @@ -49,6 +50,10 @@ func IndexAliasCreate(conf *cfg.Config) *cli.Command { Usage: "create an index alias", UsageText: "create ", + ShellComplete: func(ctx context.Context, cmd *cli.Command) { + completeIndex(cmd) + }, + Action: func(ctx context.Context, cmd *cli.Command) error { index := cmd.Args().Get(0) alias := cmd.Args().Get(1) @@ -69,6 +74,10 @@ func IndexAliasDelete(conf *cfg.Config) *cli.Command { Usage: "delete an index alias", UsageText: "delete ", + ShellComplete: func(ctx context.Context, cmd *cli.Command) { + completeIndex(cmd) + }, + Action: func(ctx context.Context, cmd *cli.Command) error { index := cmd.Args().Get(0) alias := cmd.Args().Get(1) diff --git a/cmd/node.go b/cmd/node.go index cf04748..c403e2f 100644 --- a/cmd/node.go +++ b/cmd/node.go @@ -58,6 +58,7 @@ func NodeShow(conf *cfg.Config) *cli.Command { UsageText: "show [options] ", Action: func(ctx context.Context, cmd *cli.Command) error { + // FIXME: implement es.NodeShow() // return es.NodeShow(conf, cmd.Args().Get(0)) return nil }, diff --git a/cmd/roles.go b/cmd/roles.go index d932c28..68a0959 100644 --- a/cmd/roles.go +++ b/cmd/roles.go @@ -67,6 +67,10 @@ func RoleShow(conf *cfg.Config) *cli.Command { Usage: "show details about a role", UsageText: "show [options] ", + ShellComplete: func(ctx context.Context, cmd *cli.Command) { + completeRole(cmd) + }, + Action: func(ctx context.Context, cmd *cli.Command) error { index := cmd.Args().Get(0) if index == "" { diff --git a/pkg/es/index.go b/pkg/es/index.go index e2877b3..dc9d48c 100644 --- a/pkg/es/index.go +++ b/pkg/es/index.go @@ -118,8 +118,8 @@ func IndexList(conf *cfg.Config) error { return nil } -func IndexShow(conf *cfg.Config, index string) error { - res, err := conf.DefaultCluster.ES.Indices.Get(index). +func IndexShow(conf *cfg.Config, indexpattern string) error { + res, err := conf.DefaultCluster.ES.Indices.Get(indexpattern). // we need to add custom request headers, required for older ES instances Header("content-type", "application/json"). Header("accept", "application/json"). @@ -128,36 +128,38 @@ func IndexShow(conf *cfg.Config, index string) error { return fmt.Errorf("failed to get index: %s", esErrorString(err)) } - slog.Debug("ES result", "index", res) + for name, index := range res { + fields := make([]string, len(index.Mappings.Properties)) + idx := 0 + for field := range index.Mappings.Properties { + fields[idx] = field + idx++ + } - fields := make([]string, len(res[index].Mappings.Properties)) - idx := 0 - for field := range res[index].Mappings.Properties { - fields[idx] = field - idx++ - } + table := printer.NewTable(conf, 2, 5) + table.Addheaders("index property", "value") - table := printer.NewTable(conf, 2, 5) - table.Addheaders("index property", "value") + ts, err := strconv.ParseInt(index.Settings.Index.CreationDate.(string), 10, 64) + if err != nil { + ts = 0 + } - ts, err := strconv.ParseInt(res[index].Settings.Index.CreationDate.(string), 10, 64) - if err != nil { - ts = 0 - } + created := time.Unix(ts/1000, 0) - created := time.Unix(ts/1000, 0) + table.Entries = [][]string{ + {"name", name}, + {"replicas", *index.Settings.Index.NumberOfReplicas}, + {"shards", *index.Settings.Index.NumberOfShards}, + {"created", created.Format("2006-01-02 15:04:05")}, + {"uuid", *index.Settings.Index.Uuid}, + {"fields", strings.Join(fields, ",")}, + } - table.Entries = [][]string{ - {"name", index}, - {"replicas", *res[index].Settings.Index.NumberOfReplicas}, - {"shards", *res[index].Settings.Index.NumberOfShards}, - {"created", created.Format("2006-01-02 15:04:05")}, - {"uuid", *res[index].Settings.Index.Uuid}, - {"fields", strings.Join(fields, ",")}, - } + if err := table.Print(); err != nil { + return err + } - if err := table.Print(); err != nil { - return err + fmt.Println() } return nil diff --git a/pkg/es/role.go b/pkg/es/role.go index 8a922f8..de22035 100644 --- a/pkg/es/role.go +++ b/pkg/es/role.go @@ -27,6 +27,23 @@ import ( "github.com/elastic/go-elasticsearch/v9/typedapi/types" ) +func RoleNames(conf *cfg.Config) ([]string, error) { + res, err := conf.DefaultCluster.ES.Security.GetRole(). + Do(context.Background()) + if err != nil { + return nil, fmt.Errorf("failed to get roles: %s", esErrorString(err)) + } + + roles := make([]string, len(res)) + idx := 0 + for name := range res { + roles[idx] = name + idx++ + } + + return roles, nil +} + func RoleList(conf *cfg.Config) error { res, err := conf.DefaultCluster.ES.Security.GetRole(). Do(context.Background())