mirror of
https://codeberg.org/scip/esctl.git
synced 2026-08-24 18:14:18 +02:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
15e0f9dc90 | ||
|
|
dd619ab815 |
6
TODO.md
6
TODO.md
@@ -2,6 +2,6 @@
|
||||
- [ES API docs](https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-indices-get)
|
||||
|
||||
- Fix index names custom completion
|
||||
- add cluster default <name> which would add a flag to the config, so that no -C is needed subsequently
|
||||
- add `cluster stats` from `/_cluster/stats` like mem, procs, open files, num indices, shards etc
|
||||
or add these to `cluster status`, maybe add a `--stats` to include stats there?
|
||||
- index show: add more details, see screenshots
|
||||
- add shard explain, aka:
|
||||
get /_cluster/allocation/explain {"index":"yourindex", "primary": true, "shard":0}
|
||||
|
||||
116
cmd/ccr.go
Normal file
116
cmd/ccr.go
Normal file
@@ -0,0 +1,116 @@
|
||||
/*
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"codeberg.org/scip/esctl/pkg/cfg"
|
||||
"codeberg.org/scip/esctl/pkg/es"
|
||||
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
func Ccr(conf *cfg.Config) *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "ccr",
|
||||
Aliases: []string{"replication", "rep"},
|
||||
Usage: "manage cross cluster replication",
|
||||
|
||||
Commands: []*cli.Command{
|
||||
CcrStatus(conf),
|
||||
CcrShardPause(conf),
|
||||
CcrShardResume(conf),
|
||||
CcrFollower(conf),
|
||||
CcrRemoteInfo(conf),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
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)",
|
||||
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "exclude",
|
||||
Usage: "regexp of indicies to exclude",
|
||||
Destination: &conf.Exclude,
|
||||
Aliases: []string{"e"},
|
||||
},
|
||||
},
|
||||
|
||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||
leader := cmd.Args().Get(0)
|
||||
follower := cmd.Args().Get(1)
|
||||
|
||||
if leader == "" || follower == "" {
|
||||
return errors.New("no leader and follower aliases specified")
|
||||
}
|
||||
|
||||
_, hasLeader := conf.Clusters[leader]
|
||||
_, hasFollower := conf.Clusters[follower]
|
||||
|
||||
if !hasLeader || !hasFollower {
|
||||
return errors.New("either leader or follower alias not configured")
|
||||
}
|
||||
|
||||
if err := es.CcrStatus(conf, leader, follower); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func CcrShardPause(conf *cfg.Config) *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "pause",
|
||||
Usage: "pause shard allocation",
|
||||
|
||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||
return es.CcrShardPause(conf)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func CcrShardResume(conf *cfg.Config) *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "resume",
|
||||
Usage: "resume shard allocation",
|
||||
|
||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||
return es.CcrShardResume(conf)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func CcrRemoteInfo(conf *cfg.Config) *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "info",
|
||||
Usage: "show ccr remote info",
|
||||
UsageText: "info [options] [<index>]",
|
||||
|
||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||
return es.CcrRemoteInfo(conf, cmd.Args().Get(0))
|
||||
},
|
||||
}
|
||||
}
|
||||
193
cmd/ccr_follower.go
Normal file
193
cmd/ccr_follower.go
Normal file
@@ -0,0 +1,193 @@
|
||||
/*
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"codeberg.org/scip/esctl/pkg/cfg"
|
||||
"codeberg.org/scip/esctl/pkg/es"
|
||||
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
func CcrFollower(conf *cfg.Config) *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "follower",
|
||||
Aliases: []string{"f"},
|
||||
Usage: "manage ccr follower indices",
|
||||
|
||||
Commands: []*cli.Command{
|
||||
CcrFollowerShow(conf),
|
||||
CcrFollowerAdd(conf),
|
||||
CcrFollowerDelete(conf),
|
||||
CcrFollowerUnfollow(conf),
|
||||
CcrFollowerPause(conf),
|
||||
CcrFollowerResume(conf),
|
||||
CcrFollowerRenew(conf),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func CcrFollowerRenew(conf *cfg.Config) *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "renew",
|
||||
Usage: "renew ccr follower index",
|
||||
UsageText: "renew [options] <index>",
|
||||
|
||||
Flags: []cli.Flag{
|
||||
&cli.BoolFlag{
|
||||
Name: "force",
|
||||
Usage: "force even if unfollow fails (e.g. because following is red anyway)",
|
||||
Destination: &conf.Force,
|
||||
Aliases: []string{"f"},
|
||||
},
|
||||
},
|
||||
|
||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||
args := cmd.Args()
|
||||
|
||||
if args.Len() != 1 {
|
||||
return errors.New("missing arguments: <index>")
|
||||
}
|
||||
|
||||
return es.CcrFollowerRenew(conf, cmd.Args().Get(0))
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func CcrFollowerResume(conf *cfg.Config) *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "resume",
|
||||
Usage: "resume ccr index to follow",
|
||||
UsageText: "resume [options] <index>",
|
||||
|
||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||
args := cmd.Args()
|
||||
|
||||
if args.Len() != 1 {
|
||||
return errors.New("missing arguments: <index>")
|
||||
}
|
||||
|
||||
return es.CcrFollowerResume(conf, cmd.Args().Get(0))
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func CcrFollowerPause(conf *cfg.Config) *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "pause",
|
||||
Usage: "pause ccr index to follow",
|
||||
UsageText: "pause [options] <index>",
|
||||
|
||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||
args := cmd.Args()
|
||||
|
||||
if args.Len() != 1 {
|
||||
return errors.New("missing arguments: <index>")
|
||||
}
|
||||
|
||||
return es.CcrFollowerPause(conf, cmd.Args().Get(0))
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func CcrFollowerUnfollow(conf *cfg.Config) *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "unfollow",
|
||||
Usage: "unfollow ccr follower index",
|
||||
UsageText: "unfollow [options] <index>",
|
||||
|
||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||
args := cmd.Args()
|
||||
|
||||
if args.Len() != 1 {
|
||||
return errors.New("missing arguments: <index>")
|
||||
}
|
||||
|
||||
return es.CcrFollowerUnfollow(conf, cmd.Args().Get(0))
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func CcrFollowerAdd(conf *cfg.Config) *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "add",
|
||||
Aliases: []string{"+"},
|
||||
Usage: "add ccr follower index",
|
||||
UsageText: "add [options] <index>",
|
||||
|
||||
Flags: []cli.Flag{
|
||||
&cli.BoolFlag{
|
||||
Name: "wait",
|
||||
Usage: "wait for active shards",
|
||||
Destination: &conf.Wait,
|
||||
Aliases: []string{"w"},
|
||||
},
|
||||
},
|
||||
|
||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||
args := cmd.Args()
|
||||
|
||||
if args.Len() != 1 {
|
||||
return errors.New("missing arguments: <index>")
|
||||
}
|
||||
|
||||
return es.CcrFollowerAdd(conf, cmd.Args().Get(0))
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func CcrFollowerDelete(conf *cfg.Config) *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "delete",
|
||||
Aliases: []string{"rm"},
|
||||
Usage: "delete ccr follower index",
|
||||
UsageText: "delete <index>",
|
||||
|
||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||
args := cmd.Args()
|
||||
|
||||
if args.Len() != 1 {
|
||||
return errors.New("missing arguments: <index>")
|
||||
}
|
||||
|
||||
// just an alias to index delete
|
||||
return es.IndexDelete(conf, cmd.Args().Get(0))
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func CcrFollowerShow(conf *cfg.Config) *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "show",
|
||||
Aliases: []string{"sh"},
|
||||
Usage: "show ccr follower index details",
|
||||
UsageText: "show <index>",
|
||||
|
||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||
args := cmd.Args()
|
||||
|
||||
if args.Len() != 1 {
|
||||
return errors.New("missing arguments: <index>")
|
||||
}
|
||||
|
||||
return es.CcrFollowerShow(conf, cmd.Args().Get(0))
|
||||
},
|
||||
}
|
||||
}
|
||||
149
cmd/cluster.go
149
cmd/cluster.go
@@ -18,7 +18,6 @@ package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"codeberg.org/scip/esctl/pkg/cfg"
|
||||
"codeberg.org/scip/esctl/pkg/es"
|
||||
@@ -26,10 +25,6 @@ import (
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
const (
|
||||
SETTINGS = `https://www.elastic.co/docs/reference/elasticsearch/configuration-reference`
|
||||
)
|
||||
|
||||
func Cluster(conf *cfg.Config) *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "cluster",
|
||||
@@ -37,7 +32,6 @@ func Cluster(conf *cfg.Config) *cli.Command {
|
||||
Usage: "manage cluster[s]",
|
||||
|
||||
Commands: []*cli.Command{
|
||||
ClusterCompare(conf),
|
||||
ClusterStatus(conf),
|
||||
ClusterList(conf),
|
||||
ClusterSettings(conf),
|
||||
@@ -52,11 +46,7 @@ func ClusterList(conf *cfg.Config) *cli.Command {
|
||||
Aliases: []string{"ls"},
|
||||
|
||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||
if err := es.ClusterList(conf); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
return es.ClusterList(conf)
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -74,141 +64,16 @@ func ClusterStatus(conf *cfg.Config) *cli.Command {
|
||||
Destination: &conf.All,
|
||||
Aliases: []string{"a"},
|
||||
},
|
||||
},
|
||||
|
||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||
if err := es.ClusterStatus(conf); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func ClusterCompare(conf *cfg.Config) *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "compare",
|
||||
Aliases: []string{"c"},
|
||||
Usage: "compare cluster[s] (yaml config with 2 clusters required)",
|
||||
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "exclude",
|
||||
Usage: "regexp of indicies to exclude",
|
||||
Destination: &conf.Exclude,
|
||||
Aliases: []string{"e"},
|
||||
&cli.BoolFlag{
|
||||
Name: "verbose",
|
||||
Usage: "include verbose statistics",
|
||||
Destination: &conf.Verbose,
|
||||
Aliases: []string{"v"},
|
||||
},
|
||||
},
|
||||
|
||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||
leader := cmd.Args().Get(0)
|
||||
follower := cmd.Args().Get(1)
|
||||
|
||||
if leader == "" || follower == "" {
|
||||
return errors.New("no leader and follower aliases specified")
|
||||
}
|
||||
|
||||
_, hasLeader := conf.Clusters[leader]
|
||||
_, hasFollower := conf.Clusters[follower]
|
||||
|
||||
if !hasLeader || !hasFollower {
|
||||
return errors.New("either leader or follower alias not configured")
|
||||
}
|
||||
|
||||
if err := es.ClusterCompare(conf, leader, follower); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func ClusterSettings(conf *cfg.Config) *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "settings",
|
||||
Usage: "cluster settings management",
|
||||
Aliases: []string{"config"},
|
||||
|
||||
Commands: []*cli.Command{
|
||||
ClusterSettingsList(conf),
|
||||
ClusterSettingsSet(conf),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func ClusterSettingsList(conf *cfg.Config) *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "list",
|
||||
Usage: "show cluster settings",
|
||||
Aliases: []string{"ls", "get"},
|
||||
|
||||
Flags: []cli.Flag{
|
||||
&cli.BoolFlag{
|
||||
Name: "persistent",
|
||||
Usage: "only show persistent setting[s] (default)",
|
||||
Destination: &conf.Persistent,
|
||||
Aliases: []string{"p"},
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "transient",
|
||||
Usage: "only show transient setting[s]",
|
||||
Destination: &conf.Transient,
|
||||
Aliases: []string{"t"},
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "default",
|
||||
Usage: "only show default setting[s]",
|
||||
Destination: &conf.Default,
|
||||
Aliases: []string{"D"},
|
||||
},
|
||||
},
|
||||
|
||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||
if err := es.ClusterSettingsList(conf); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func ClusterSettingsSet(conf *cfg.Config) *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "set",
|
||||
Usage: "set|update cluster settings",
|
||||
Aliases: []string{"set", "update"},
|
||||
UsageText: "set [options] setting:value [setting:value ...]\n\nReference: " + SETTINGS,
|
||||
|
||||
Flags: []cli.Flag{
|
||||
&cli.BoolFlag{
|
||||
Name: "persistent",
|
||||
Usage: "add persistent setting[s] (default)",
|
||||
Destination: &conf.Persistent,
|
||||
Aliases: []string{"p"},
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "transient",
|
||||
Usage: "add transient setting[s]",
|
||||
Destination: &conf.Transient,
|
||||
Aliases: []string{"t"},
|
||||
},
|
||||
},
|
||||
|
||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||
args := cmd.Args()
|
||||
|
||||
if args.Len() == 0 {
|
||||
return errors.New("at least one setting must be specified (format: setting:value)")
|
||||
}
|
||||
|
||||
if err := es.ClusterSettingsSet(conf, cmd.Args()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
return es.ClusterStatus(conf)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
119
cmd/cluster_settings.go
Normal file
119
cmd/cluster_settings.go
Normal file
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"codeberg.org/scip/esctl/pkg/cfg"
|
||||
"codeberg.org/scip/esctl/pkg/es"
|
||||
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
const (
|
||||
SETTINGS = `https://www.elastic.co/docs/reference/elasticsearch/configuration-reference`
|
||||
)
|
||||
|
||||
func ClusterSettings(conf *cfg.Config) *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "settings",
|
||||
Usage: "cluster settings management",
|
||||
Aliases: []string{"config"},
|
||||
|
||||
Commands: []*cli.Command{
|
||||
ClusterSettingsList(conf),
|
||||
ClusterSettingsSet(conf),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func ClusterSettingsList(conf *cfg.Config) *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "list",
|
||||
Usage: "show cluster settings",
|
||||
Aliases: []string{"ls", "get"},
|
||||
|
||||
Flags: []cli.Flag{
|
||||
&cli.BoolFlag{
|
||||
Name: "persistent",
|
||||
Usage: "only show persistent setting[s] (default)",
|
||||
Destination: &conf.Persistent,
|
||||
Aliases: []string{"p"},
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "transient",
|
||||
Usage: "only show transient setting[s]",
|
||||
Destination: &conf.Transient,
|
||||
Aliases: []string{"t"},
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "default",
|
||||
Usage: "only show default setting[s]",
|
||||
Destination: &conf.Default,
|
||||
Aliases: []string{"D"},
|
||||
},
|
||||
},
|
||||
|
||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||
if err := es.ClusterSettingsList(conf); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func ClusterSettingsSet(conf *cfg.Config) *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "set",
|
||||
Usage: "set|update cluster settings",
|
||||
Aliases: []string{"set", "update"},
|
||||
UsageText: "set [options] setting:value [setting:value ...]\n\nReference: " + SETTINGS,
|
||||
|
||||
Flags: []cli.Flag{
|
||||
&cli.BoolFlag{
|
||||
Name: "persistent",
|
||||
Usage: "add persistent setting[s] (default)",
|
||||
Destination: &conf.Persistent,
|
||||
Aliases: []string{"p"},
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "transient",
|
||||
Usage: "add transient setting[s]",
|
||||
Destination: &conf.Transient,
|
||||
Aliases: []string{"t"},
|
||||
},
|
||||
},
|
||||
|
||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||
args := cmd.Args()
|
||||
|
||||
if args.Len() == 0 {
|
||||
return errors.New("at least one setting must be specified (format: setting:value)")
|
||||
}
|
||||
|
||||
if err := es.ClusterSettingsSet(conf, cmd.Args()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
58
cmd/doc.go
Normal file
58
cmd/doc.go
Normal file
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"codeberg.org/scip/esctl/pkg/cfg"
|
||||
"codeberg.org/scip/esctl/pkg/es"
|
||||
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
func Doc(conf *cfg.Config) *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "doc",
|
||||
Usage: "manage documents",
|
||||
|
||||
Commands: []*cli.Command{
|
||||
DocAdd(conf),
|
||||
//Delete(conf),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func DocAdd(conf *cfg.Config) *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "add",
|
||||
Aliases: []string{"+"},
|
||||
Usage: "add JSON document index",
|
||||
UsageText: "add [options] <index> '<json-doc>'",
|
||||
|
||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||
args := cmd.Args()
|
||||
|
||||
if args.Len() != 2 {
|
||||
return errors.New("missing arguments: <index> <json-doc>")
|
||||
}
|
||||
|
||||
return es.DocAdd(conf, cmd.Args().Get(0), cmd.Args().Get(1))
|
||||
},
|
||||
}
|
||||
}
|
||||
38
cmd/index.go
38
cmd/index.go
@@ -37,6 +37,7 @@ func Index(conf *cfg.Config) *cli.Command {
|
||||
IndexShow(conf),
|
||||
IndexCreate(conf),
|
||||
IndexDelete(conf),
|
||||
IndexClose(conf),
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -69,11 +70,7 @@ func IndexList(conf *cfg.Config) *cli.Command {
|
||||
},
|
||||
|
||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||
if err := es.IndexList(conf); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
return es.IndexList(conf)
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -85,11 +82,8 @@ func IndexShow(conf *cfg.Config) *cli.Command {
|
||||
Usage: "show details about an index",
|
||||
|
||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||
if err := es.IndexShow(conf, cmd.Args().Get(0)); err != nil {
|
||||
return err
|
||||
}
|
||||
return es.IndexShow(conf, cmd.Args().Get(0))
|
||||
|
||||
return nil
|
||||
},
|
||||
|
||||
// FIXME: doesn't work at all
|
||||
@@ -140,11 +134,14 @@ func IndexCreate(conf *cfg.Config) *cli.Command {
|
||||
},
|
||||
|
||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||
if err := es.IndexCreate(conf, cmd.Args().Get(0)); err != nil {
|
||||
return err
|
||||
args := cmd.Args()
|
||||
if args.Len() == 0 {
|
||||
return fmt.Errorf("no index specified")
|
||||
}
|
||||
|
||||
return nil
|
||||
mappings := args.Slice()[1:]
|
||||
|
||||
return es.IndexCreate(conf, args.Get(0), mappings)
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -156,11 +153,18 @@ func IndexDelete(conf *cfg.Config) *cli.Command {
|
||||
Usage: "delete an index",
|
||||
|
||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||
if err := es.IndexDelete(conf, cmd.Args().Get(0)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
return es.IndexDelete(conf, cmd.Args().Get(0))
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func IndexClose(conf *cfg.Config) *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "close",
|
||||
Usage: "close an index",
|
||||
|
||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||
return es.IndexClose(conf, cmd.Args().Get(0))
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
11
cmd/node.go
11
cmd/node.go
@@ -45,11 +45,7 @@ func NodeList(conf *cfg.Config) *cli.Command {
|
||||
Usage: "list nodes",
|
||||
|
||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||
if err := es.NodeList(conf); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
return es.NodeList(conf)
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -62,10 +58,7 @@ func NodeShow(conf *cfg.Config) *cli.Command {
|
||||
UsageText: "show [options] <node>",
|
||||
|
||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||
// if err := es.NodeShow(conf, cmd.Args().Get(0)); err != nil {
|
||||
// return err
|
||||
// }
|
||||
|
||||
// return es.NodeShow(conf, cmd.Args().Get(0))
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
38
cmd/repl.go
Normal file
38
cmd/repl.go
Normal file
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"codeberg.org/scip/esctl/pkg/cfg"
|
||||
"codeberg.org/scip/esctl/pkg/es"
|
||||
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
func Repl(conf *cfg.Config) *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "repl",
|
||||
Aliases: []string{"shell"},
|
||||
Usage: "interactive API repl",
|
||||
|
||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||
return es.Repl(conf)
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -76,7 +76,10 @@ func Main() int {
|
||||
Index(conf),
|
||||
Snapshot(conf),
|
||||
Cluster(conf),
|
||||
Ccr(conf),
|
||||
Node(conf),
|
||||
Doc(conf),
|
||||
Repl(conf),
|
||||
},
|
||||
|
||||
Before: func(ctx context.Context, cmd *cli.Command) (context.Context, error) {
|
||||
|
||||
@@ -18,6 +18,7 @@ package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"codeberg.org/scip/esctl/pkg/cfg"
|
||||
"codeberg.org/scip/esctl/pkg/es"
|
||||
@@ -30,7 +31,7 @@ func Search(conf *cfg.Config) *cli.Command {
|
||||
Name: "search",
|
||||
Aliases: []string{"/"},
|
||||
Usage: "search within an index",
|
||||
UsageText: "search [options] <query>",
|
||||
UsageText: "search [options] <field=pattern> ...",
|
||||
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
@@ -63,11 +64,12 @@ func Search(conf *cfg.Config) *cli.Command {
|
||||
},
|
||||
|
||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||
if err := es.Search(conf, cmd.Args().Get(0)); err != nil {
|
||||
return err
|
||||
args := cmd.Args()
|
||||
if args.Len() == 0 {
|
||||
return errors.New("at least one query must be specified (format: field=pattern)")
|
||||
}
|
||||
|
||||
return nil
|
||||
return es.Search(conf, args.Slice())
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,11 +54,7 @@ func SnapshotList(conf *cfg.Config) *cli.Command {
|
||||
},
|
||||
|
||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||
if err := es.SnapshotList(conf); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
return es.SnapshotList(conf)
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -71,11 +67,7 @@ func SnapshotShow(conf *cfg.Config) *cli.Command {
|
||||
UsageText: "show [options] <snapshot>",
|
||||
|
||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||
if err := es.SnapshotShow(conf, cmd.Args().Get(0)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
return es.SnapshotShow(conf, cmd.Args().Get(0))
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
2
go.mod
2
go.mod
@@ -19,8 +19,10 @@ go 1.25.0
|
||||
require (
|
||||
github.com/alecthomas/repr v0.5.2 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/chzyer/readline v1.5.1 // indirect
|
||||
github.com/clipperhouse/displaywidth v0.10.0 // indirect
|
||||
github.com/clipperhouse/uax29/v2 v2.6.0 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/elastic/elastic-transport-go/v8 v8.11.0 // indirect
|
||||
github.com/elastic/go-elasticsearch/v9 v9.3.2 // indirect
|
||||
github.com/fatih/color v1.19.0 // indirect
|
||||
|
||||
7
go.sum
7
go.sum
@@ -2,10 +2,16 @@ github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs
|
||||
github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ=
|
||||
github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI=
|
||||
github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk=
|
||||
github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=
|
||||
github.com/clipperhouse/displaywidth v0.10.0 h1:GhBG8WuerxjFQQYeuZAeVTuyxuX+UraiZGD4HJQ3Y8g=
|
||||
github.com/clipperhouse/displaywidth v0.10.0/go.mod h1:XqJajYsaiEwkxOj4bowCTMcT1SgvHo9flfF3jQasdbs=
|
||||
github.com/clipperhouse/uax29/v2 v2.6.0 h1:z0cDbUV+aPASdFb2/ndFnS9ts/WNXgTNNGFoKXuhpos=
|
||||
github.com/clipperhouse/uax29/v2 v2.6.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/elastic/elastic-transport-go/v8 v8.9.0 h1:KeT/2P54F0xS0S8Y3Pf+tFDg4HmBgReQMB+BMz8dDAs=
|
||||
github.com/elastic/elastic-transport-go/v8 v8.9.0/go.mod h1:ssMTvNS2hwf7CaiGsRRsx4gQHFZ/jS/DkLcISxekWzc=
|
||||
github.com/elastic/elastic-transport-go/v8 v8.11.0 h1:taYmqC2M6+fZt/+W+ENYh/W5L9+KrlJGOSbEJs8egWc=
|
||||
@@ -56,6 +62,7 @@ go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt
|
||||
go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
|
||||
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM=
|
||||
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80=
|
||||
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
|
||||
@@ -31,7 +31,7 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
Version string = `v0.0.7`
|
||||
Version string = `v0.0.9`
|
||||
)
|
||||
|
||||
type Cluster struct {
|
||||
@@ -52,8 +52,9 @@ type Config struct {
|
||||
From, To, MaxItems int // search: flags
|
||||
Filter []string // search: -F
|
||||
Exclude string // cluster compare: -e (regexp)
|
||||
All bool // cluster status: -a
|
||||
All, Verbose bool // cluster status: -a -v
|
||||
Persistent, Transient, Default bool // -p -t -D cluster settings set
|
||||
Force bool // ccr follower renew: -f
|
||||
}
|
||||
|
||||
func NewConfig() *Config {
|
||||
|
||||
134
pkg/es/ccr.go
Normal file
134
pkg/es/ccr.go
Normal file
@@ -0,0 +1,134 @@
|
||||
/*
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package es
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
|
||||
"codeberg.org/scip/esctl/pkg/cfg"
|
||||
"github.com/elastic/go-elasticsearch/v9/typedapi/types"
|
||||
)
|
||||
|
||||
const (
|
||||
shard_pause = `cluster.routing.allocation.enable`
|
||||
)
|
||||
|
||||
func CcrShardPause(conf *cfg.Config) error {
|
||||
return ClusterSettingsSetSingle(conf, shard_pause, "none")
|
||||
}
|
||||
|
||||
func CcrShardResume(conf *cfg.Config) error {
|
||||
return ClusterSettingsSetSingle(conf, shard_pause, "all")
|
||||
}
|
||||
|
||||
func CcrStatus(conf *cfg.Config, leader, follower string) error {
|
||||
if !checkClusterIsLeader(conf, leader) {
|
||||
if !checkClusterIsLeader(conf, follower) {
|
||||
return errors.New("leader/follower attribution is invalid, both clusters are followers")
|
||||
}
|
||||
|
||||
// reverse attribution
|
||||
f := follower
|
||||
follower = leader
|
||||
leader = f
|
||||
slog.Debug("leader/follower attribution is invalid, reversing", "leader", leader, "follower", follower)
|
||||
}
|
||||
|
||||
indices := ClusterIndices{}
|
||||
|
||||
for _, alias := range []string{leader, follower} {
|
||||
cat := conf.Clusters[alias].ES.Cat.Indices().
|
||||
// we need to add custom request headers, required for older ES instances
|
||||
Header("content-type", "application/json").
|
||||
Header("accept", "application/json")
|
||||
|
||||
res, err := cat.Do(context.Background())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get indicies on %s: %s", alias, err)
|
||||
}
|
||||
|
||||
indices[alias] = make(map[string]*types.IndicesRecord, len(res))
|
||||
|
||||
for _, index := range res {
|
||||
indices[alias][*index.Index] = &index
|
||||
}
|
||||
}
|
||||
|
||||
if !checkClusterStatus(conf, leader, follower) {
|
||||
return errors.New("one of the two clusters is in a failed state")
|
||||
}
|
||||
|
||||
findIlmErrors(conf, leader, follower)
|
||||
|
||||
if findIndicesOnlyOnLeader(conf, indices, leader, follower) &&
|
||||
findOrphanedIndices(conf, indices, leader, follower) &&
|
||||
findFailedFollowerIndices(conf, indices, follower) {
|
||||
fmt.Println("everything's hunky-dory.")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func CcrRemoteInfo(conf *cfg.Config, index string) error {
|
||||
res, err := conf.DefaultCluster.ES.Cluster.RemoteInfo().
|
||||
Header("content-type", "application/json").
|
||||
Header("accept", "application/json").
|
||||
Do(context.Background())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to retrieve follower info: %s", err)
|
||||
}
|
||||
|
||||
slog.Debug("ccr remote info", "info", res)
|
||||
|
||||
remote := ""
|
||||
var info *types.ClusterRemoteProxyInfo
|
||||
|
||||
for name, data := range res {
|
||||
remote = name
|
||||
info = data.(*types.ClusterRemoteProxyInfo)
|
||||
break
|
||||
}
|
||||
|
||||
if remote == "" {
|
||||
return fmt.Errorf("cluster doesn't follow any other: %s", err)
|
||||
}
|
||||
|
||||
mode := "follower"
|
||||
if checkClusterIsLeader(conf, conf.CurrentCluster) {
|
||||
mode = "leader"
|
||||
}
|
||||
|
||||
table := NewTable(2, 5)
|
||||
table.Addheaders("field", "value")
|
||||
|
||||
table.entries = [][]string{
|
||||
{"Remote Cluster", remote},
|
||||
{"CCR Mode", mode},
|
||||
{"Connected", fmt.Sprintf("%t", info.Connected)},
|
||||
{"Num Proxy Sockets Connected", fmt.Sprintf("%d", info.NumProxySocketsConnected)},
|
||||
{"Proxy Address", info.ProxyAddress},
|
||||
}
|
||||
|
||||
if err := table.PrintMarkdown(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
198
pkg/es/ccr_follower.go
Normal file
198
pkg/es/ccr_follower.go
Normal file
@@ -0,0 +1,198 @@
|
||||
/*
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package es
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
|
||||
"codeberg.org/scip/esctl/pkg/cfg"
|
||||
)
|
||||
|
||||
func getRemoteName(conf *cfg.Config) (string, error) {
|
||||
res, err := conf.DefaultCluster.ES.Cluster.RemoteInfo().
|
||||
Header("content-type", "application/json").
|
||||
Header("accept", "application/json").
|
||||
Do(context.Background())
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to retrieve follower info: %s", err)
|
||||
}
|
||||
|
||||
remote := ""
|
||||
for name := range res {
|
||||
remote = name
|
||||
break
|
||||
}
|
||||
|
||||
if remote == "" {
|
||||
return "", fmt.Errorf("cluster doesn't have a follower: %s", err)
|
||||
}
|
||||
|
||||
return remote, nil
|
||||
}
|
||||
|
||||
func wrapError(call func(*cfg.Config, string) error, conf *cfg.Config, index string) error {
|
||||
err := call(conf, index)
|
||||
|
||||
if conf.Force {
|
||||
fmt.Printf("caught error: %s, continuing anyway", err)
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func CcrFollowerRenew(conf *cfg.Config, index string) error {
|
||||
if err := wrapError(IndexClose, conf, index); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf("closed %s", index)
|
||||
|
||||
if err := wrapError(CcrFollowerPause, conf, index); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf("paused %s", index)
|
||||
|
||||
if err := wrapError(CcrFollowerUnfollow, conf, index); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf("unfollowed %s", index)
|
||||
|
||||
if err := wrapError(IndexDelete, conf, index); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf("deleted %s", index)
|
||||
|
||||
if err := CcrFollowerAdd(conf, index); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf("added follower %s", index)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func CcrFollowerResume(conf *cfg.Config, index string) error {
|
||||
create := conf.DefaultCluster.ES.Ccr.ResumeFollow(index).
|
||||
Header("content-type", "application/json").
|
||||
Header("accept", "application/json")
|
||||
|
||||
_, err := create.Do(context.Background())
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to resume ccr following: %s", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func CcrFollowerPause(conf *cfg.Config, index string) error {
|
||||
create := conf.DefaultCluster.ES.Ccr.PauseFollow(index).
|
||||
Header("content-type", "application/json").
|
||||
Header("accept", "application/json")
|
||||
|
||||
_, err := create.Do(context.Background())
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to pause ccr following: %s", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func CcrFollowerUnfollow(conf *cfg.Config, index string) error {
|
||||
create := conf.DefaultCluster.ES.Ccr.ForgetFollower(index).
|
||||
Header("content-type", "application/json").
|
||||
Header("accept", "application/json")
|
||||
|
||||
_, err := create.Do(context.Background())
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to unfollow index: %s", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func CcrFollowerAdd(conf *cfg.Config, index string) error {
|
||||
remote, err := getRemoteName(conf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
create := conf.DefaultCluster.ES.Ccr.Follow(index).
|
||||
LeaderIndex(index).
|
||||
RemoteCluster(remote).
|
||||
Header("content-type", "application/json").
|
||||
Header("accept", "application/json")
|
||||
|
||||
if conf.Wait {
|
||||
create.WaitForActiveShards("all")
|
||||
}
|
||||
|
||||
_, err = create.Do(context.Background())
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create follower index: %s", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func CcrFollowerShow(conf *cfg.Config, index string) error {
|
||||
res, err := conf.DefaultCluster.ES.Ccr.FollowStats(index).
|
||||
Header("content-type", "application/json").
|
||||
Header("accept", "application/json").
|
||||
Do(context.Background())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to retrieve follower index info: %s", err)
|
||||
}
|
||||
|
||||
slog.Debug("ES result", "follower stats", res.Indices)
|
||||
|
||||
if len(res.Indices) == 0 {
|
||||
return fmt.Errorf("cluster did not return any follower stats for index %s", index)
|
||||
}
|
||||
|
||||
if len(res.Indices[0].Shards) == 0 {
|
||||
return fmt.Errorf("cluster did not return any shard stats on follower index %s", index)
|
||||
}
|
||||
|
||||
follower := res.Indices[0].Shards[0]
|
||||
|
||||
table := NewTable(2, 9)
|
||||
table.Addheaders("field", "value")
|
||||
|
||||
table.entries = [][]string{
|
||||
{"name", index},
|
||||
{"remote_cluster", follower.RemoteCluster},
|
||||
{"leader_checkpoint", fmt.Sprintf("%d", follower.LeaderGlobalCheckpoint)},
|
||||
{"follower_checkpoint", fmt.Sprintf("%d", follower.FollowerGlobalCheckpoint)},
|
||||
{"bytes_read", fmt.Sprintf("%d", follower.BytesRead)},
|
||||
{"failed_read_requests", fmt.Sprintf("%d", follower.FailedReadRequests)},
|
||||
{"failed_write_requests", fmt.Sprintf("%d", follower.FailedWriteRequests)},
|
||||
{"successful_read_requests", fmt.Sprintf("%d", follower.SuccessfulReadRequests)},
|
||||
{"successful_write_requests", fmt.Sprintf("%d", follower.SuccessfulWriteRequests)},
|
||||
}
|
||||
|
||||
if err := table.PrintMarkdown(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -18,15 +18,17 @@ package es
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"codeberg.org/scip/esctl/pkg/cfg"
|
||||
"github.com/dustin/go-humanize"
|
||||
"github.com/elastic/go-elasticsearch/v9/typedapi/ccr/stats"
|
||||
"github.com/elastic/go-elasticsearch/v9/typedapi/cluster/health"
|
||||
clusterstats "github.com/elastic/go-elasticsearch/v9/typedapi/cluster/stats"
|
||||
"github.com/elastic/go-elasticsearch/v9/typedapi/core/info"
|
||||
"github.com/elastic/go-elasticsearch/v9/typedapi/types"
|
||||
)
|
||||
@@ -35,6 +37,7 @@ const (
|
||||
ResponseHealth = iota
|
||||
ResponseInfo
|
||||
ResponseCcr
|
||||
ResponseStats
|
||||
)
|
||||
|
||||
type ClusterIndices map[string]map[string]*types.IndicesRecord
|
||||
@@ -44,49 +47,10 @@ type apiResponse struct {
|
||||
info *info.Response
|
||||
health *health.Response
|
||||
ccr *stats.Response
|
||||
stats *clusterstats.Response
|
||||
which int
|
||||
}
|
||||
|
||||
func ClusterCompare(conf *cfg.Config, leader, follower string) error {
|
||||
if !checkClusterFollower(conf, leader) {
|
||||
return errors.New("leader/follower attribution is invalid, reverse cluster attribution and retry")
|
||||
}
|
||||
|
||||
indices := ClusterIndices{}
|
||||
|
||||
for _, alias := range []string{leader, follower} {
|
||||
cat := conf.Clusters[alias].ES.Cat.Indices().
|
||||
// we need to add custom request headers, required for older ES instances
|
||||
Header("content-type", "application/json").
|
||||
Header("accept", "application/json")
|
||||
|
||||
res, err := cat.Do(context.Background())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get indicies on %s: %s", alias, err)
|
||||
}
|
||||
|
||||
indices[alias] = make(map[string]*types.IndicesRecord, len(res))
|
||||
|
||||
for _, index := range res {
|
||||
indices[alias][*index.Index] = &index
|
||||
}
|
||||
}
|
||||
|
||||
if !checkClusterStatus(conf, leader, follower) {
|
||||
return errors.New("one of the two clusters is in a failed state")
|
||||
}
|
||||
|
||||
findIlmErrors(conf, leader, follower)
|
||||
|
||||
if findIndicesOnlyOnLeader(conf, indices, leader, follower) &&
|
||||
findOrphanedIndices(conf, indices, leader, follower) &&
|
||||
findFailedFollowerIndices(conf, indices, follower) {
|
||||
fmt.Println("everything's hunky-dory.")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func ClusterList(conf *cfg.Config) error {
|
||||
table := NewTable(3, len(conf.Clusters))
|
||||
|
||||
@@ -102,8 +66,6 @@ func ClusterList(conf *cfg.Config) error {
|
||||
|
||||
slices.Sort(names)
|
||||
|
||||
idx = 0
|
||||
|
||||
for idx, name := range names {
|
||||
current := name == "default" || name == conf.CurrentCluster
|
||||
cluster := conf.Clusters[name]
|
||||
@@ -118,7 +80,6 @@ func ClusterList(conf *cfg.Config) error {
|
||||
}
|
||||
|
||||
table.entries[idx] = []string{name, cluster.Uri, fmt.Sprintf("%t", current)}
|
||||
idx++
|
||||
}
|
||||
|
||||
if err := table.PrintMarkdown(); err != nil {
|
||||
@@ -132,6 +93,10 @@ func ClusterList(conf *cfg.Config) error {
|
||||
// have to do 3 of'em for each cluster. This speeds things up.
|
||||
func ClusterStatus(conf *cfg.Config) error {
|
||||
clusters := []string{}
|
||||
gocount := 3
|
||||
if conf.Verbose {
|
||||
gocount++
|
||||
}
|
||||
|
||||
if conf.All {
|
||||
for key := range conf.Clusters {
|
||||
@@ -147,21 +112,26 @@ func ClusterStatus(conf *cfg.Config) error {
|
||||
es = conf.Clusters[cluster].ES
|
||||
}
|
||||
|
||||
responses := make(chan apiResponse, 3)
|
||||
responses := make(chan apiResponse, gocount)
|
||||
wg := &sync.WaitGroup{}
|
||||
|
||||
wg.Add(3)
|
||||
wg.Add(gocount)
|
||||
go getClusterData(es, wg, responses, "health")
|
||||
go getClusterData(es, wg, responses, "info")
|
||||
go getClusterData(es, wg, responses, "ccrstats")
|
||||
|
||||
if conf.Verbose {
|
||||
go getClusterData(es, wg, responses, "stats")
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
var clusterhealth *health.Response
|
||||
var info *info.Response
|
||||
var ccrstats *stats.Response
|
||||
var clusterstats *clusterstats.Response
|
||||
|
||||
for i := 0; i < 3; i++ {
|
||||
for i := 0; i < gocount; i++ {
|
||||
r := <-responses
|
||||
|
||||
if r.error != nil {
|
||||
@@ -175,6 +145,8 @@ func ClusterStatus(conf *cfg.Config) error {
|
||||
ccrstats = r.ccr
|
||||
case ResponseInfo:
|
||||
info = r.info
|
||||
case ResponseStats:
|
||||
clusterstats = r.stats
|
||||
}
|
||||
}
|
||||
|
||||
@@ -198,11 +170,14 @@ func ClusterStatus(conf *cfg.Config) error {
|
||||
{"ES Version", info.Version.Int},
|
||||
{"Active Shards", fmt.Sprintf("%d", clusterhealth.ActiveShards)},
|
||||
{"Active Primary Shards", fmt.Sprintf("%d", clusterhealth.ActivePrimaryShards)},
|
||||
{"Indicies", fmt.Sprintf("%d", len(clusterhealth.Indices))},
|
||||
{"Nodes", fmt.Sprintf("%d", clusterhealth.NumberOfNodes)},
|
||||
{"AutoFollow (success/failed indices)", ccrfollowing},
|
||||
}
|
||||
|
||||
if conf.Verbose {
|
||||
table = gatherClusterStats(conf, clusterstats, table)
|
||||
}
|
||||
|
||||
if err := table.PrintMarkdown(); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -210,3 +185,47 @@ func ClusterStatus(conf *cfg.Config) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func gatherClusterStats(conf *cfg.Config, clusterstats *clusterstats.Response, table *Table) *Table {
|
||||
var querycount int64
|
||||
var vmversion string
|
||||
|
||||
for _, count := range clusterstats.Indices.Search.Queries {
|
||||
querycount += count
|
||||
}
|
||||
|
||||
if len(clusterstats.Nodes.Jvm.Versions) > 0 {
|
||||
vmversion = strings.Join([]string{
|
||||
clusterstats.Nodes.Jvm.Versions[0].VmName,
|
||||
clusterstats.Nodes.Jvm.Versions[0].VmVersion}, " ")
|
||||
}
|
||||
|
||||
isleader := checkClusterIsLeader(conf, conf.CurrentCluster)
|
||||
|
||||
table.entries = append(table.entries, [][]string{
|
||||
{"Indicies", fmt.Sprintf("%d", clusterstats.Indices.Count)},
|
||||
{"Is Leader", fmt.Sprintf("%t", isleader)},
|
||||
{"Docs", fmt.Sprintf("%d", clusterstats.Indices.Docs.Count)},
|
||||
{"Total Size", humanize.Bytes(uint64(clusterstats.Indices.Docs.TotalSizeInBytes))},
|
||||
{"Total Queries", fmt.Sprintf("%d", querycount)},
|
||||
{"Shards Primaries", fmt.Sprintf("%d", clusterstats.Indices.Shards.Primaries)},
|
||||
{"Shards Total", fmt.Sprintf("%d", clusterstats.Indices.Shards.Total)},
|
||||
{"Storage", fmt.Sprintf(
|
||||
"%s/%s",
|
||||
humanize.Bytes(uint64(clusterstats.Indices.Store.SizeInBytes)),
|
||||
humanize.Bytes(uint64(*clusterstats.Indices.Store.TotalDataSetSizeInBytes)),
|
||||
)},
|
||||
{"JVM Heap", fmt.Sprintf(
|
||||
"%s/%s",
|
||||
humanize.Bytes(uint64(clusterstats.Nodes.Jvm.Mem.HeapUsedInBytes)),
|
||||
humanize.Bytes(uint64(clusterstats.Nodes.Jvm.Mem.HeapMaxInBytes)),
|
||||
)},
|
||||
{"JVM Threads", fmt.Sprintf("%d", clusterstats.Nodes.Jvm.Threads)},
|
||||
{"JVM Version", vmversion},
|
||||
{"CPUs", fmt.Sprintf("%d", clusterstats.Nodes.Os.AllocatedProcessors)},
|
||||
{"CPU Usage", fmt.Sprintf("%d%%", clusterstats.Nodes.Process.Cpu.Percent)},
|
||||
{"Open FDs", fmt.Sprintf("%d", clusterstats.Nodes.Process.OpenFileDescriptors.Avg)},
|
||||
}...)
|
||||
|
||||
return table
|
||||
}
|
||||
|
||||
@@ -139,3 +139,22 @@ func ClusterSettingsSet(conf *cfg.Config, args cli.Args) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func ClusterSettingsSetSingle(conf *cfg.Config, setting, value string) error {
|
||||
message, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshall persistent value <%v> to valid JSON: %s", value, err)
|
||||
}
|
||||
|
||||
_, err = conf.Clusters[conf.CurrentCluster].ES.Cluster.PutSettings().
|
||||
AddPersistent(setting, message).
|
||||
Header("content-type", "application/json").
|
||||
Header("accept", "application/json").
|
||||
Do(context.Background())
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to set %s: %s", setting, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ const (
|
||||
DefaultExclude = `(part|monitoring|.internal|metrics-endpoint)`
|
||||
)
|
||||
|
||||
func checkClusterFollower(conf *cfg.Config, leader string) bool {
|
||||
func checkClusterIsLeader(conf *cfg.Config, leader string) bool {
|
||||
stats, err := conf.Clusters[leader].ES.Ccr.Stats().
|
||||
Header("content-type", "application/json").
|
||||
Header("accept", "application/json").
|
||||
@@ -375,6 +375,16 @@ func getClusterData(es *elasticsearch.TypedClient, wg *sync.WaitGroup, reschan c
|
||||
ar.ccr = res
|
||||
ar.which = ResponseCcr
|
||||
arerr = err
|
||||
|
||||
case "stats":
|
||||
res, err := es.Cluster.Stats().
|
||||
Header("content-type", "application/json").
|
||||
Header("accept", "application/json").
|
||||
Do(context.Background())
|
||||
|
||||
ar.stats = res
|
||||
ar.which = ResponseStats
|
||||
arerr = err
|
||||
}
|
||||
|
||||
if arerr != nil {
|
||||
|
||||
55
pkg/es/doc.go
Normal file
55
pkg/es/doc.go
Normal file
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package es
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"codeberg.org/scip/esctl/pkg/cfg"
|
||||
)
|
||||
|
||||
// create index with:
|
||||
//
|
||||
// esctl index create foo [id:keyword user:text age:integer]
|
||||
//
|
||||
// then add a doc:
|
||||
//
|
||||
// esctl doc add foo2 '{"id":"d8d8d","user":"scip"}'
|
||||
func DocAdd(conf *cfg.Config, index, jsondoc string) error {
|
||||
data := map[string]any{}
|
||||
|
||||
err := json.Unmarshal([]byte(jsondoc), &data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("supplied document was not valid JSON: %s", err)
|
||||
}
|
||||
|
||||
now := fmt.Sprintf("%d", time.Now().Unix())
|
||||
|
||||
_, err = conf.DefaultCluster.ES.Create(index, now).
|
||||
Document(data).
|
||||
Header("content-type", "application/json").
|
||||
Header("accept", "application/json").
|
||||
Do(context.Background())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create new doc in index %s: %s", index, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"codeberg.org/scip/esctl/pkg/cfg"
|
||||
@@ -134,11 +135,7 @@ func IndexShow(conf *cfg.Config, index string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func IndexCreate(conf *cfg.Config, index string) error {
|
||||
if index == "" {
|
||||
return fmt.Errorf("no index specified")
|
||||
}
|
||||
|
||||
func IndexCreate(conf *cfg.Config, index string, mappings []string) error {
|
||||
settings := esdsl.NewIndexSettings()
|
||||
|
||||
create := conf.DefaultCluster.ES.Indices.Create(index).
|
||||
@@ -156,6 +153,30 @@ func IndexCreate(conf *cfg.Config, index string) error {
|
||||
settings = settings.NumberOfReplicas(strconv.Itoa(conf.Replicas))
|
||||
}
|
||||
|
||||
if len(mappings) > 0 {
|
||||
maps := esdsl.NewTypeMapping()
|
||||
|
||||
for _, mapping := range mappings {
|
||||
parts := strings.Split(mapping, ":")
|
||||
if len(parts) != 2 {
|
||||
return fmt.Errorf("invalid mapping %s, expect <name:type> (type: integer, text, date, keyword)", mapping)
|
||||
}
|
||||
|
||||
switch parts[1] {
|
||||
case "text":
|
||||
maps.AddProperty(parts[0], esdsl.NewTextProperty())
|
||||
case "integer":
|
||||
maps.AddProperty(parts[0], esdsl.NewIntegerNumberProperty())
|
||||
case "date":
|
||||
maps.AddProperty(parts[0], esdsl.NewDateProperty())
|
||||
case "keyword":
|
||||
maps.AddProperty(parts[0], esdsl.NewKeywordProperty())
|
||||
}
|
||||
}
|
||||
|
||||
create.Mappings(maps)
|
||||
}
|
||||
|
||||
_, err := create.Settings(settings).
|
||||
Do(context.Background())
|
||||
|
||||
@@ -181,3 +202,17 @@ func IndexDelete(conf *cfg.Config, index string) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func IndexClose(conf *cfg.Config, index string) error {
|
||||
create := conf.DefaultCluster.ES.Indices.Close(index).
|
||||
Header("content-type", "application/json").
|
||||
Header("accept", "application/json")
|
||||
|
||||
_, err := create.Do(context.Background())
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to close index: %s", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
153
pkg/es/repl.go
Normal file
153
pkg/es/repl.go
Normal file
@@ -0,0 +1,153 @@
|
||||
/*
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package es
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"codeberg.org/scip/esctl/pkg/cfg"
|
||||
"github.com/chzyer/readline"
|
||||
)
|
||||
|
||||
func encodeAuth(username, password string) string {
|
||||
return base64.StdEncoding.EncodeToString([]byte(username + ":" + password))
|
||||
}
|
||||
|
||||
func CallAPI(conf *cfg.Config, input []string) error {
|
||||
var data string
|
||||
|
||||
verb := strings.ToUpper(input[0])
|
||||
path := input[1]
|
||||
|
||||
if len(input) == 3 {
|
||||
data = input[2]
|
||||
}
|
||||
|
||||
// we're using port-forwards anyway
|
||||
tr := &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
}
|
||||
|
||||
client := &http.Client{Transport: tr}
|
||||
|
||||
req, err := http.NewRequest(verb, conf.DefaultCluster.Uri+path, bytes.NewBuffer([]byte(data)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req.Header.Add("Content-Type", "application/json")
|
||||
req.Header.Add("accept", "application/json")
|
||||
req.Header.Add("Authorization", "Basic "+encodeAuth(conf.DefaultCluster.User, conf.DefaultCluster.Pass))
|
||||
|
||||
// actually execute the request
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Read and print response
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read response body: %s", err)
|
||||
}
|
||||
|
||||
var pretty bytes.Buffer
|
||||
error := json.Indent(&pretty, body, "", "\t")
|
||||
if error != nil {
|
||||
return fmt.Errorf("json parse error: %s", err)
|
||||
}
|
||||
|
||||
fmt.Println(pretty.String())
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func Repl(conf *cfg.Config) error {
|
||||
verbs := []string{"post", "get", "put", "delete"}
|
||||
|
||||
fmt.Println("Input format: verb path [data]")
|
||||
fmt.Println("example: post /yourindex/_ccr/pause_follow")
|
||||
|
||||
reader, err := readline.NewEx(&readline.Config{
|
||||
Prompt: "> ",
|
||||
HistoryFile: os.Getenv("HOME") + "/.config/esctl/history",
|
||||
HistoryLimit: 500,
|
||||
InterruptPrompt: "^C",
|
||||
EOFPrompt: "exit",
|
||||
HistorySearchFold: true,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to initialize readline lib: %s", err)
|
||||
}
|
||||
|
||||
for {
|
||||
text, err := reader.Readline()
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
text = strings.TrimSpace(text)
|
||||
|
||||
if text == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
parts := strings.SplitN(strings.TrimSpace(text), " ", 3)
|
||||
if len(parts) < 2 {
|
||||
fmt.Println("error: you need to input a verb, uri [and post data]")
|
||||
continue
|
||||
}
|
||||
|
||||
if !slices.Contains(verbs, strings.ToLower(parts[0])) {
|
||||
fmt.Println("error: verb must be one of " + strings.Join(verbs, ","))
|
||||
continue
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(parts[1], "/") {
|
||||
fmt.Println("error: url path must start with /")
|
||||
continue
|
||||
}
|
||||
|
||||
if len(parts) == 3 {
|
||||
data := map[string]any{}
|
||||
err := json.Unmarshal([]byte(parts[2]), &data)
|
||||
if err != nil {
|
||||
fmt.Printf("error: input data is not proper JSON: %s", err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
err = CallAPI(conf, parts)
|
||||
if err != nil {
|
||||
fmt.Printf("failed to call API: %s\n", err)
|
||||
}
|
||||
|
||||
reader.SetPrompt("> ")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -34,9 +34,17 @@ Execute an ES search.
|
||||
q is the actual search query given as arg to the 'search' cmd
|
||||
additional filters can be given as -F key=value
|
||||
*/
|
||||
func Search(conf *cfg.Config, q string) error {
|
||||
query := esdsl.NewBoolQuery().
|
||||
Must(esdsl.NewMatchQuery("message", q))
|
||||
func Search(conf *cfg.Config, queries []string) error {
|
||||
query := esdsl.NewBoolQuery()
|
||||
|
||||
for _, q := range queries {
|
||||
parts := strings.Split(q, "=")
|
||||
if len(parts) != 2 {
|
||||
return fmt.Errorf("search queries must be in the form field=pattern")
|
||||
}
|
||||
|
||||
query.Must(esdsl.NewMatchQuery(parts[0], parts[1]))
|
||||
}
|
||||
|
||||
filters := make([]types.QueryVariant, len(conf.Filter))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user