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
+}