diff --git a/README.md b/README.md index 58d38aa..8560cd3 100644 --- a/README.md +++ b/README.md @@ -474,95 +474,99 @@ constantly as well. ### Command tree: ```console -api - api access and documentation - list - list index of API calls - show - show an API doc - repl - interactive API repl -ccr - manage cross cluster replication - status - cross cluster replication status (yaml config with 2 clusters required) - pause - pause shard allocation - resume - resume shard allocation - follower - manage ccr follower indices - show - show ccr follower index details - add - add ccr follower index - delete - delete ccr follower index - unfollow - unfollow ccr follower index - pause - pause ccr index to follow - resume - resume ccr index to follow - renew - renew ccr follower index - info - show ccr remote info -cluster - manage cluster[s] - status - show cluster status - switch - set current elasticsearch cluster - list - list configured clusters - settings - cluster settings management - list - show cluster settings - set - set|update cluster settings -datastream - manage data streams - list - list indicies - show - show details about an data stream - create - create a new data stream - delete - delete a data stream - rollover - roll over a data stream -doc - manage documents - add - add JSON document index - show - show a JSON document - delete - delete JSON document[s] from index[es] -ilm - manage index lifecycle - retry - retry applying an ILM profile to an index - status - get the current index lifecycle management status - list - list index lifecycle policies - show - show details about an index lifecycle policy - create - create a index lifecycle policy - forecast - calculate index phase movements - list - list index rollover config - show - show rollover forecast over all indices -index - manage indicies - list - list indicies - show - show details about an index - create - create a new index - modify - modify anindex - delete - delete an index - close - close an index - allocation - explain index allocation - fields - show info about field capabilities - ilm - show ilm status - alias - manage index aliases - create - create an index alias - list - list index aliases - delete - delete an index alias - rollover - roll over an index alias - template - manage index templates - list - list index templates - show - show details about an index template - create - create a new index template - modify - modify a new index template - delete - delete an index template -node - manage nodes - list - list nodes - show - show details about a node -role - manage roles - list - list roles - show - show details about a role - diff - show differences between roles and CSV baseline -search - search within an index -shard - manage shards - list - list shards - show - show details about a shard -snapshot - manage snapshots - list - list snapshots - show - show details about a snapshot -task - manage tasks - list - list tasks - cancel - cancel running task -version - show esctl version information -debug - developer only -help-jsonpath - show jsonpath help -completion - Output shell completion script for bash, zsh, fish, or Powershell - pwsh - Output pwsh completion script - bash - Output bash completion script - zsh - Output zsh completion script - fish - Output fish completion script +api - api access and documentation + list - list index of API calls + show - show an API doc + repl - interactive API repl +ccr - manage cross cluster replication + status - cross cluster replication status (yaml config with 2 clusters required) + pause - pause shard allocation + resume - resume shard allocation + follower - manage ccr follower indices + show - show ccr follower index details + add - add ccr follower index + delete - delete ccr follower index + unfollow - unfollow ccr follower index + pause - pause ccr index to follow + resume - resume ccr index to follow + renew - renew ccr follower index + info - show ccr remote info +cluster - manage cluster[s] + status - show cluster status + switch - set current elasticsearch cluster + list - list configured clusters + settings - cluster settings management + list - show cluster settings + set - set|update cluster settings + reroute - manually change the allocation of individual shards in the cluster. + move - move shard to another node + allocate-replica - allocate-replica replica to another node + cancel - cancel a reroute operation + allocate-empty-primary - allocate an empty primary shard to a node + allocate-stale-primary - allocate a stale primary shard to a node +datastream - manage data streams + list - list indicies + show - show details about an data stream + create - create a new data stream + delete - delete a data stream + rollover - roll over a data stream +doc - manage documents + add - add JSON document index + show - show a JSON document + delete - delete JSON document[s] from index[es] +ilm - manage index lifecycle + retry - retry applying an ILM profile to an index + status - get the current index lifecycle management status + list - list index lifecycle policies + show - show details about an index lifecycle policy + create - create a new lifecycle policy + update - update an lifecycle policy + forecast - calculate index phase movements + list - list index rollover config + show - show rollover forecast over all indices + explain - explain ilm condition of an index +index - manage indicies + list - list indicies + show - show details about an index + create - create a new index + update - update an index + delete - delete an index + close - close an index + allocation - explain index allocation + fields - show info about field capabilities + ilm - show ilm status + alias - manage index aliases + create - create an index alias + list - list index aliases + delete - delete an index alias + rollover - roll over an index alias + template - manage index templates + list - list index templates + show - show details about an index template + create - create a new index template + update - update a new index template + delete - delete an index template +node - manage nodes + list - list nodes + show - show details about a node +role - manage roles + list - list roles + show - show details about a role + diff - show differences between roles and CSV baseline +search - search within an index +shard - manage shards + list - list shards + show - show details about a shard +snapshot - manage snapshots + list - list snapshots + show - show details about a snapshot +task - manage tasks + list - list tasks + cancel - cancel running task +version - show esctl version information +debug - developer only +help-jsonpath - show jsonpath help +help-command-overview - show overview of all available commands ``` # Development diff --git a/cmd/cluster.go b/cmd/cluster.go index 5670cda..91d646c 100644 --- a/cmd/cluster.go +++ b/cmd/cluster.go @@ -37,6 +37,7 @@ func Cluster(conf *cfg.Config) *cli.Command { ClusterSwitch(conf), ClusterList(conf), ClusterSettings(conf), + ClusterReroute(conf), }, } } diff --git a/cmd/cluster_reroute.go b/cmd/cluster_reroute.go new file mode 100644 index 0000000..935de1a --- /dev/null +++ b/cmd/cluster_reroute.go @@ -0,0 +1,225 @@ +/* +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 ( + "context" + "errors" + + "codeberg.org/scip/esctl/pkg/cfg" + "codeberg.org/scip/esctl/pkg/es" + + "github.com/urfave/cli/v3" +) + +func ClusterReroute(conf *cfg.Config) *cli.Command { + return &cli.Command{ + Name: "reroute", + Usage: "manually change the allocation of individual shards in the cluster.", + UsageText: "reroute ", + Aliases: []string{"ctx"}, + + Commands: []*cli.Command{ + ClusterRerouteMove(conf), + ClusterRerouteAllocateReplica(conf), + ClusterRerouteCancel(conf), + ClusterRerouteAllocatePrimary(conf, false), + ClusterRerouteAllocatePrimary(conf, true), + }, + } +} + +func ClusterRerouteMove(conf *cfg.Config) *cli.Command { + return &cli.Command{ + Name: "move", + Usage: "move shard to another node", + UsageText: "move [options] ", + + ShellComplete: func(ctx context.Context, cmd *cli.Command) { + complete(cmd, Cindex) + }, + + Flags: []cli.Flag{ + &cli.IntFlag{ + Name: "shard", + Usage: "shard to move", + Destination: &conf.Shards, + Aliases: []string{"s"}, + Required: true, + }, + &cli.StringFlag{ + Name: "from-node", + Usage: "current node", + Destination: &conf.FromNode, + Aliases: []string{"f"}, + Required: true, + }, + &cli.StringFlag{ + Name: "to-node", + Usage: "node to move to", + Destination: &conf.ToNode, + Aliases: []string{"t"}, + Required: true, + }, + }, + + Action: func(ctx context.Context, cmd *cli.Command) error { + index := cmd.Args().Get(0) + if index == "" { + return errors.New("no index specified") + } + + return es.ClusterRerouteMove(conf, index) + }, + } +} + +func ClusterRerouteAllocateReplica(conf *cfg.Config) *cli.Command { + return &cli.Command{ + Name: "allocate-replica", + Usage: "allocate-replica replica to another node", + UsageText: "allocate-replica [options] ", + + ShellComplete: func(ctx context.Context, cmd *cli.Command) { + complete(cmd, Cindex) + }, + + Flags: []cli.Flag{ + &cli.IntFlag{ + Name: "shard", + Usage: "shard to move", + Destination: &conf.Shards, + Aliases: []string{"s"}, + Required: true, + }, + &cli.StringFlag{ + Name: "to-node", + Usage: "node to move to", + Destination: &conf.ToNode, + Aliases: []string{"t"}, + Required: true, + }, + }, + + Action: func(ctx context.Context, cmd *cli.Command) error { + index := cmd.Args().Get(0) + if index == "" { + return errors.New("no index specified") + } + + return es.ClusterRerouteAllocateReplica(conf, index) + }, + } +} + +func ClusterRerouteCancel(conf *cfg.Config) *cli.Command { + return &cli.Command{ + Name: "cancel", + Usage: "cancel a reroute operation", + UsageText: "cancel [options] ", + + ShellComplete: func(ctx context.Context, cmd *cli.Command) { + complete(cmd, Cindex) + }, + + Flags: []cli.Flag{ + &cli.IntFlag{ + Name: "shard", + Usage: "shard to move", + Destination: &conf.Shards, + Aliases: []string{"s"}, + Required: true, + }, + &cli.StringFlag{ + Name: "to-node", + Usage: "node to move to", + Destination: &conf.ToNode, + Aliases: []string{"t"}, + Required: true, + }, + &cli.BoolFlag{ + Name: "allow-primary", + Usage: "allow primary shard to cancel", + Destination: &conf.AllowPrimary, + Aliases: []string{"a"}, + }, + }, + + Action: func(ctx context.Context, cmd *cli.Command) error { + index := cmd.Args().Get(0) + if index == "" { + return errors.New("no index specified") + } + + return es.ClusterRerouteCancel(conf, index) + }, + } +} + +func ClusterRerouteAllocatePrimary(conf *cfg.Config, stale bool) *cli.Command { + name := "allocate-empty-primary" + usage := "allocate an empty primary shard to a node" + + if stale { + name = "allocate-stale-primary" + usage = "allocate a stale primary shard to a node" + + } + + return &cli.Command{ + Name: name, + Usage: usage, + UsageText: name + " [options] ", + + ShellComplete: func(ctx context.Context, cmd *cli.Command) { + complete(cmd, Cindex) + }, + + Flags: []cli.Flag{ + &cli.IntFlag{ + Name: "shard", + Usage: "shard to move", + Destination: &conf.Shards, + Aliases: []string{"s"}, + Required: true, + }, + &cli.StringFlag{ + Name: "to-node", + Usage: "node to move to", + Destination: &conf.ToNode, + Aliases: []string{"t"}, + Required: true, + }, + &cli.BoolFlag{ + Name: "accept-data-loss", + Usage: "", + Destination: &conf.AcceptDataLoss, + Aliases: []string{"a"}, + Required: true, + }, + }, + + Action: func(ctx context.Context, cmd *cli.Command) error { + index := cmd.Args().Get(0) + if index == "" { + return errors.New("no index specified") + } + + return es.ClusterRerouteAllocatePrimary(conf, index, stale) + }, + } +} diff --git a/cmd/root.go b/cmd/root.go index 316ea0c..9745f72 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -262,8 +262,25 @@ func HelpUsage(conf *cfg.Config) *cli.Command { }, Action: func(ctx context.Context, cmd *cli.Command) error { - max := 22 + maxCommandWidth := 0 + // first pass, determine max command width + if err := walkVisible(conf, cmd.Root(), func(cmd *cli.Command) error { + path := cmd.Path() + size := len(path[len(path)-1]) + + if size > maxCommandWidth { + maxCommandWidth = size + } + + return nil + }); err != nil { + return err + } + + maxCommandWidth += 4 // account for indent width + + // second pass, build tree return walkVisible(conf, cmd.Root(), func(cmd *cli.Command) error { path := cmd.Path() command := path[len(path)-1] @@ -273,7 +290,7 @@ func HelpUsage(conf *cfg.Config) *cli.Command { } indent := strings.Repeat(" ", len(path[1:])-1) - space := strings.Repeat(" ", max-(len(command)+len(indent))) + space := strings.Repeat(" ", maxCommandWidth-(len(command)+len(indent))) fmt.Printf("%s%s %s - %s\n", indent, command, space, cmd.Usage) diff --git a/pkg/cfg/config.go b/pkg/cfg/config.go index c035632..5d0c8af 100644 --- a/pkg/cfg/config.go +++ b/pkg/cfg/config.go @@ -103,6 +103,9 @@ type Config struct { Tag string // api ls: -t Ilm Ilm // ilm create + + FromNode, ToNode string // cluster reroute move: -f + -t + AllowPrimary, AcceptDataLoss bool // cluster reroute cancel: -p,-a } func NewConfig() *Config { diff --git a/pkg/es/cluster_reroute.go b/pkg/es/cluster_reroute.go new file mode 100644 index 0000000..cda9d3a --- /dev/null +++ b/pkg/es/cluster_reroute.go @@ -0,0 +1,125 @@ +/* +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 es + +import ( + "context" + "fmt" + + "codeberg.org/scip/esctl/pkg/cfg" + "github.com/elastic/go-elasticsearch/v9/typedapi/esdsl" + "github.com/elastic/go-elasticsearch/v9/typedapi/types" +) + +func ClusterRerouteMove(conf *cfg.Config, index string) error { + move := conf.DefaultCluster.ES().Cluster.Reroute() + + commands := esdsl.NewCommand() + moveCommand := &types.CommandMoveAction{ + Shard: conf.Shards, + FromNode: conf.FromNode, + ToNode: conf.ToNode, + Index: index, + } + + commands.CommandCaster().Move = moveCommand + + move.Commands(commands) + + _, err := move.Do(context.Background()) + if err != nil { + return fmt.Errorf("failed to reroute move: %w", err) + } + + return nil +} + +func ClusterRerouteAllocateReplica(conf *cfg.Config, index string) error { + move := conf.DefaultCluster.ES().Cluster.Reroute() + + commands := esdsl.NewCommand() + allocCommand := &types.CommandAllocateReplicaAction{ + Shard: conf.Shards, + Node: conf.ToNode, + Index: index, + } + + commands.CommandCaster().AllocateReplica = allocCommand + + move.Commands(commands) + + _, err := move.Do(context.Background()) + if err != nil { + return fmt.Errorf("failed to allocate a replica shard: %w", err) + } + + return nil +} + +func ClusterRerouteCancel(conf *cfg.Config, index string) error { + move := conf.DefaultCluster.ES().Cluster.Reroute() + + commands := esdsl.NewCommand() + cancelCommand := &types.CommandCancelAction{ + Shard: conf.Shards, + Node: conf.ToNode, + Index: index, + AllowPrimary: &conf.AllowPrimary, + } + + commands.CommandCaster().Cancel = cancelCommand + + move.Commands(commands) + + _, err := move.Do(context.Background()) + if err != nil { + return fmt.Errorf("failed to cancel a reroute process: %w", err) + } + + return nil +} + +func ClusterRerouteAllocatePrimary(conf *cfg.Config, index string, stale bool) error { + move := conf.DefaultCluster.ES().Cluster.Reroute() + + commands := esdsl.NewCommand() + allocCommand := &types.CommandAllocatePrimaryAction{ + Shard: conf.Shards, + Node: conf.ToNode, + Index: index, + AcceptDataLoss: conf.AcceptDataLoss, + } + + if stale { + commands.CommandCaster().AllocateStalePrimary = allocCommand + } else { + commands.CommandCaster().AllocateEmptyPrimary = allocCommand + } + + move.Commands(commands) + + _, err := move.Do(context.Background()) + if err != nil { + which := "empty" + if stale { + which = "stale" + } + return fmt.Errorf("failed to allocate %s primary shard: %w", which, err) + } + + return nil +}