mirror of
https://codeberg.org/scip/esctl.git
synced 2026-08-24 06:34:18 +02:00
Add CCR stuff, fix search, add docs support, support index maps, refactor (#13)
This commit is contained in:
103
cmd/ccr.go
Normal file
103
cmd/ccr.go
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
/*
|
||||||
|
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),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
108
cmd/ccr_follower.go
Normal file
108
cmd/ccr_follower.go
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
/*
|
||||||
|
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),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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{"-"},
|
||||||
|
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))
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
145
cmd/cluster.go
145
cmd/cluster.go
@@ -18,7 +18,6 @@ package cmd
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
|
|
||||||
"codeberg.org/scip/esctl/pkg/cfg"
|
"codeberg.org/scip/esctl/pkg/cfg"
|
||||||
"codeberg.org/scip/esctl/pkg/es"
|
"codeberg.org/scip/esctl/pkg/es"
|
||||||
@@ -26,10 +25,6 @@ import (
|
|||||||
"github.com/urfave/cli/v3"
|
"github.com/urfave/cli/v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
SETTINGS = `https://www.elastic.co/docs/reference/elasticsearch/configuration-reference`
|
|
||||||
)
|
|
||||||
|
|
||||||
func Cluster(conf *cfg.Config) *cli.Command {
|
func Cluster(conf *cfg.Config) *cli.Command {
|
||||||
return &cli.Command{
|
return &cli.Command{
|
||||||
Name: "cluster",
|
Name: "cluster",
|
||||||
@@ -37,7 +32,6 @@ func Cluster(conf *cfg.Config) *cli.Command {
|
|||||||
Usage: "manage cluster[s]",
|
Usage: "manage cluster[s]",
|
||||||
|
|
||||||
Commands: []*cli.Command{
|
Commands: []*cli.Command{
|
||||||
ClusterCompare(conf),
|
|
||||||
ClusterStatus(conf),
|
ClusterStatus(conf),
|
||||||
ClusterList(conf),
|
ClusterList(conf),
|
||||||
ClusterSettings(conf),
|
ClusterSettings(conf),
|
||||||
@@ -52,11 +46,7 @@ func ClusterList(conf *cfg.Config) *cli.Command {
|
|||||||
Aliases: []string{"ls"},
|
Aliases: []string{"ls"},
|
||||||
|
|
||||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||||
if err := es.ClusterList(conf); err != nil {
|
return es.ClusterList(conf)
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -77,138 +67,7 @@ func ClusterStatus(conf *cfg.Config) *cli.Command {
|
|||||||
},
|
},
|
||||||
|
|
||||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||||
if err := es.ClusterStatus(conf); err != nil {
|
return es.ClusterStatus(conf)
|
||||||
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"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
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
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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))
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
26
cmd/index.go
26
cmd/index.go
@@ -69,11 +69,7 @@ func IndexList(conf *cfg.Config) *cli.Command {
|
|||||||
},
|
},
|
||||||
|
|
||||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||||
if err := es.IndexList(conf); err != nil {
|
return es.IndexList(conf)
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -85,11 +81,8 @@ func IndexShow(conf *cfg.Config) *cli.Command {
|
|||||||
Usage: "show details about an index",
|
Usage: "show details about an index",
|
||||||
|
|
||||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||||
if err := es.IndexShow(conf, cmd.Args().Get(0)); err != nil {
|
return es.IndexShow(conf, cmd.Args().Get(0))
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// FIXME: doesn't work at all
|
// FIXME: doesn't work at all
|
||||||
@@ -140,11 +133,14 @@ func IndexCreate(conf *cfg.Config) *cli.Command {
|
|||||||
},
|
},
|
||||||
|
|
||||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||||
if err := es.IndexCreate(conf, cmd.Args().Get(0)); err != nil {
|
args := cmd.Args()
|
||||||
return err
|
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 +152,7 @@ func IndexDelete(conf *cfg.Config) *cli.Command {
|
|||||||
Usage: "delete an index",
|
Usage: "delete an index",
|
||||||
|
|
||||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||||
if err := es.IndexDelete(conf, cmd.Args().Get(0)); err != nil {
|
return es.IndexDelete(conf, cmd.Args().Get(0))
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
11
cmd/node.go
11
cmd/node.go
@@ -45,11 +45,7 @@ func NodeList(conf *cfg.Config) *cli.Command {
|
|||||||
Usage: "list nodes",
|
Usage: "list nodes",
|
||||||
|
|
||||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||||
if err := es.NodeList(conf); err != nil {
|
return es.NodeList(conf)
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -62,10 +58,7 @@ func NodeShow(conf *cfg.Config) *cli.Command {
|
|||||||
UsageText: "show [options] <node>",
|
UsageText: "show [options] <node>",
|
||||||
|
|
||||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||||
// if err := es.NodeShow(conf, cmd.Args().Get(0)); err != nil {
|
// return es.NodeShow(conf, cmd.Args().Get(0))
|
||||||
// return err
|
|
||||||
// }
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -76,7 +76,9 @@ func Main() int {
|
|||||||
Index(conf),
|
Index(conf),
|
||||||
Snapshot(conf),
|
Snapshot(conf),
|
||||||
Cluster(conf),
|
Cluster(conf),
|
||||||
|
Ccr(conf),
|
||||||
Node(conf),
|
Node(conf),
|
||||||
|
Doc(conf),
|
||||||
},
|
},
|
||||||
|
|
||||||
Before: func(ctx context.Context, cmd *cli.Command) (context.Context, error) {
|
Before: func(ctx context.Context, cmd *cli.Command) (context.Context, error) {
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ package cmd
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
|
|
||||||
"codeberg.org/scip/esctl/pkg/cfg"
|
"codeberg.org/scip/esctl/pkg/cfg"
|
||||||
"codeberg.org/scip/esctl/pkg/es"
|
"codeberg.org/scip/esctl/pkg/es"
|
||||||
@@ -30,7 +31,7 @@ func Search(conf *cfg.Config) *cli.Command {
|
|||||||
Name: "search",
|
Name: "search",
|
||||||
Aliases: []string{"/"},
|
Aliases: []string{"/"},
|
||||||
Usage: "search within an index",
|
Usage: "search within an index",
|
||||||
UsageText: "search [options] <query>",
|
UsageText: "search [options] <field=pattern> ...",
|
||||||
|
|
||||||
Flags: []cli.Flag{
|
Flags: []cli.Flag{
|
||||||
&cli.StringFlag{
|
&cli.StringFlag{
|
||||||
@@ -63,11 +64,12 @@ func Search(conf *cfg.Config) *cli.Command {
|
|||||||
},
|
},
|
||||||
|
|
||||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||||
if err := es.Search(conf, cmd.Args().Get(0)); err != nil {
|
args := cmd.Args()
|
||||||
return err
|
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 {
|
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||||
if err := es.SnapshotList(conf); err != nil {
|
return es.SnapshotList(conf)
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -71,11 +67,7 @@ func SnapshotShow(conf *cfg.Config) *cli.Command {
|
|||||||
UsageText: "show [options] <snapshot>",
|
UsageText: "show [options] <snapshot>",
|
||||||
|
|
||||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||||
if err := es.SnapshotShow(conf, cmd.Args().Get(0)); err != nil {
|
return es.SnapshotShow(conf, cmd.Args().Get(0))
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
Version string = `v0.0.7`
|
Version string = `v0.0.8`
|
||||||
)
|
)
|
||||||
|
|
||||||
type Cluster struct {
|
type Cluster struct {
|
||||||
|
|||||||
87
pkg/es/ccr.go
Normal file
87
pkg/es/ccr.go
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
/*
|
||||||
|
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
|
||||||
|
}
|
||||||
106
pkg/es/ccr_follower.go
Normal file
106
pkg/es/ccr_follower.go
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
/*
|
||||||
|
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 CcrFollowerAdd(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)
|
||||||
|
}
|
||||||
|
|
||||||
|
remote := ""
|
||||||
|
for name := range res {
|
||||||
|
remote = name
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if remote == "" {
|
||||||
|
return fmt.Errorf("cluster doesn't have a follower: %s", 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,7 +18,6 @@ package es
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"slices"
|
"slices"
|
||||||
@@ -47,46 +46,6 @@ type apiResponse struct {
|
|||||||
which int
|
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 {
|
func ClusterList(conf *cfg.Config) error {
|
||||||
table := NewTable(3, len(conf.Clusters))
|
table := NewTable(3, len(conf.Clusters))
|
||||||
|
|
||||||
@@ -102,8 +61,6 @@ func ClusterList(conf *cfg.Config) error {
|
|||||||
|
|
||||||
slices.Sort(names)
|
slices.Sort(names)
|
||||||
|
|
||||||
idx = 0
|
|
||||||
|
|
||||||
for idx, name := range names {
|
for idx, name := range names {
|
||||||
current := name == "default" || name == conf.CurrentCluster
|
current := name == "default" || name == conf.CurrentCluster
|
||||||
cluster := conf.Clusters[name]
|
cluster := conf.Clusters[name]
|
||||||
@@ -118,7 +75,6 @@ func ClusterList(conf *cfg.Config) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
table.entries[idx] = []string{name, cluster.Uri, fmt.Sprintf("%t", current)}
|
table.entries[idx] = []string{name, cluster.Uri, fmt.Sprintf("%t", current)}
|
||||||
idx++
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := table.PrintMarkdown(); err != nil {
|
if err := table.PrintMarkdown(); err != nil {
|
||||||
|
|||||||
@@ -139,3 +139,22 @@ func ClusterSettingsSet(conf *cfg.Config, args cli.Args) error {
|
|||||||
|
|
||||||
return nil
|
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)`
|
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().
|
stats, err := conf.Clusters[leader].ES.Ccr.Stats().
|
||||||
Header("content-type", "application/json").
|
Header("content-type", "application/json").
|
||||||
Header("accept", "application/json").
|
Header("accept", "application/json").
|
||||||
|
|||||||
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"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"codeberg.org/scip/esctl/pkg/cfg"
|
"codeberg.org/scip/esctl/pkg/cfg"
|
||||||
@@ -134,11 +135,7 @@ func IndexShow(conf *cfg.Config, index string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func IndexCreate(conf *cfg.Config, index string) error {
|
func IndexCreate(conf *cfg.Config, index string, mappings []string) error {
|
||||||
if index == "" {
|
|
||||||
return fmt.Errorf("no index specified")
|
|
||||||
}
|
|
||||||
|
|
||||||
settings := esdsl.NewIndexSettings()
|
settings := esdsl.NewIndexSettings()
|
||||||
|
|
||||||
create := conf.DefaultCluster.ES.Indices.Create(index).
|
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))
|
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).
|
_, err := create.Settings(settings).
|
||||||
Do(context.Background())
|
Do(context.Background())
|
||||||
|
|
||||||
|
|||||||
@@ -34,9 +34,17 @@ Execute an ES search.
|
|||||||
q is the actual search query given as arg to the 'search' cmd
|
q is the actual search query given as arg to the 'search' cmd
|
||||||
additional filters can be given as -F key=value
|
additional filters can be given as -F key=value
|
||||||
*/
|
*/
|
||||||
func Search(conf *cfg.Config, q string) error {
|
func Search(conf *cfg.Config, queries []string) error {
|
||||||
query := esdsl.NewBoolQuery().
|
query := esdsl.NewBoolQuery()
|
||||||
Must(esdsl.NewMatchQuery("message", q))
|
|
||||||
|
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))
|
filters := make([]types.QueryVariant, len(conf.Filter))
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user