mirror of
https://codeberg.org/scip/esctl.git
synced 2026-08-24 17:44:18 +02:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
40e0612bef | ||
|
|
db50072a7b | ||
|
|
b71819f9e8 | ||
|
|
15e0f9dc90 |
10
TODO.md
10
TODO.md
@@ -2,6 +2,10 @@
|
|||||||
- [ES API docs](https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-indices-get)
|
- [ES API docs](https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-indices-get)
|
||||||
|
|
||||||
- Fix index names custom completion
|
- Fix index names custom completion
|
||||||
- add cluster default <name> which would add a flag to the config, so that no -C is needed subsequently
|
https://github.com/urfave/cli/issues/2332
|
||||||
- add `cluster stats` from `/_cluster/stats` like mem, procs, open files, num indices, shards etc
|
https://github.com/urfave/cli/issues/2333
|
||||||
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}
|
||||||
|
|||||||
13
cmd/ccr.go
13
cmd/ccr.go
@@ -37,6 +37,7 @@ func Ccr(conf *cfg.Config) *cli.Command {
|
|||||||
CcrShardPause(conf),
|
CcrShardPause(conf),
|
||||||
CcrShardResume(conf),
|
CcrShardResume(conf),
|
||||||
CcrFollower(conf),
|
CcrFollower(conf),
|
||||||
|
CcrRemoteInfo(conf),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -101,3 +102,15 @@ func CcrShardResume(conf *cfg.Config) *cli.Command {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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))
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -36,6 +36,91 @@ func CcrFollower(conf *cfg.Config) *cli.Command {
|
|||||||
CcrFollowerShow(conf),
|
CcrFollowerShow(conf),
|
||||||
CcrFollowerAdd(conf),
|
CcrFollowerAdd(conf),
|
||||||
CcrFollowerDelete(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))
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -71,7 +156,7 @@ func CcrFollowerAdd(conf *cfg.Config) *cli.Command {
|
|||||||
func CcrFollowerDelete(conf *cfg.Config) *cli.Command {
|
func CcrFollowerDelete(conf *cfg.Config) *cli.Command {
|
||||||
return &cli.Command{
|
return &cli.Command{
|
||||||
Name: "delete",
|
Name: "delete",
|
||||||
Aliases: []string{"-"},
|
Aliases: []string{"rm"},
|
||||||
Usage: "delete ccr follower index",
|
Usage: "delete ccr follower index",
|
||||||
UsageText: "delete <index>",
|
UsageText: "delete <index>",
|
||||||
|
|
||||||
|
|||||||
@@ -64,6 +64,12 @@ func ClusterStatus(conf *cfg.Config) *cli.Command {
|
|||||||
Destination: &conf.All,
|
Destination: &conf.All,
|
||||||
Aliases: []string{"a"},
|
Aliases: []string{"a"},
|
||||||
},
|
},
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: "verbose",
|
||||||
|
Usage: "include verbose statistics",
|
||||||
|
Destination: &conf.Verbose,
|
||||||
|
Aliases: []string{"v"},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||||
|
|||||||
41
cmd/index.go
41
cmd/index.go
@@ -37,6 +37,8 @@ func Index(conf *cfg.Config) *cli.Command {
|
|||||||
IndexShow(conf),
|
IndexShow(conf),
|
||||||
IndexCreate(conf),
|
IndexCreate(conf),
|
||||||
IndexDelete(conf),
|
IndexDelete(conf),
|
||||||
|
IndexClose(conf),
|
||||||
|
IndexAllocation(conf),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -74,6 +76,33 @@ func IndexList(conf *cfg.Config) *cli.Command {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func IndexAllocation(conf *cfg.Config) *cli.Command {
|
||||||
|
return &cli.Command{
|
||||||
|
Name: "allocation",
|
||||||
|
Aliases: []string{"a"},
|
||||||
|
Usage: "explain index allocation",
|
||||||
|
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
&cli.IntFlag{
|
||||||
|
Name: "shard",
|
||||||
|
Usage: "shard number to explain for",
|
||||||
|
Destination: &conf.Shards,
|
||||||
|
Aliases: []string{"s"},
|
||||||
|
},
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: "primary",
|
||||||
|
Usage: "explain primary allocation (default true)",
|
||||||
|
Destination: &conf.Primary,
|
||||||
|
Aliases: []string{"p"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||||
|
return es.IndexAllocation(conf, cmd.Args().Get(0))
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func IndexShow(conf *cfg.Config) *cli.Command {
|
func IndexShow(conf *cfg.Config) *cli.Command {
|
||||||
return &cli.Command{
|
return &cli.Command{
|
||||||
Name: "show",
|
Name: "show",
|
||||||
@@ -82,7 +111,6 @@ func IndexShow(conf *cfg.Config) *cli.Command {
|
|||||||
|
|
||||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||||
return es.IndexShow(conf, cmd.Args().Get(0))
|
return es.IndexShow(conf, cmd.Args().Get(0))
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// FIXME: doesn't work at all
|
// FIXME: doesn't work at all
|
||||||
@@ -156,3 +184,14 @@ func IndexDelete(conf *cfg.Config) *cli.Command {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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))
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
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)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -79,6 +79,7 @@ func Main() int {
|
|||||||
Ccr(conf),
|
Ccr(conf),
|
||||||
Node(conf),
|
Node(conf),
|
||||||
Doc(conf),
|
Doc(conf),
|
||||||
|
Repl(conf),
|
||||||
},
|
},
|
||||||
|
|
||||||
Before: func(ctx context.Context, cmd *cli.Command) (context.Context, error) {
|
Before: func(ctx context.Context, cmd *cli.Command) (context.Context, error) {
|
||||||
|
|||||||
28
go.mod
28
go.mod
@@ -17,30 +17,36 @@ module codeberg.org/scip/esctl
|
|||||||
go 1.25.0
|
go 1.25.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/alecthomas/repr v0.5.2 // indirect
|
github.com/alecthomas/repr v0.5.2
|
||||||
|
github.com/chzyer/readline v1.5.1
|
||||||
|
github.com/dustin/go-humanize v1.0.1
|
||||||
|
github.com/elastic/elastic-transport-go/v8 v8.11.0
|
||||||
|
github.com/elastic/go-elasticsearch/v9 v9.3.4
|
||||||
|
github.com/fatih/color v1.19.0
|
||||||
|
github.com/mattn/go-isatty v0.0.22
|
||||||
|
github.com/olekukonko/tablewriter v1.1.4
|
||||||
|
github.com/tlinden/yadu v0.1.3
|
||||||
|
github.com/urfave/cli/v3 v3.9.0
|
||||||
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
github.com/clipperhouse/displaywidth v0.10.0 // indirect
|
github.com/clipperhouse/displaywidth v0.10.0 // indirect
|
||||||
github.com/clipperhouse/uax29/v2 v2.6.0 // indirect
|
github.com/clipperhouse/uax29/v2 v2.6.0 // 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
|
|
||||||
github.com/go-logr/logr v1.4.3 // indirect
|
github.com/go-logr/logr v1.4.3 // indirect
|
||||||
github.com/go-logr/stdr v1.2.2 // indirect
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
|
github.com/goccy/go-json v0.10.6 // indirect
|
||||||
github.com/lmittmann/tint v1.1.3 // indirect
|
github.com/lmittmann/tint v1.1.3 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.21 // indirect
|
github.com/mattn/go-runewidth v0.0.23 // indirect
|
||||||
github.com/mattn/go-runewidth v0.0.19 // indirect
|
|
||||||
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 // indirect
|
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 // indirect
|
||||||
github.com/olekukonko/errors v1.2.0 // indirect
|
github.com/olekukonko/errors v1.2.0 // indirect
|
||||||
github.com/olekukonko/ll v0.1.6 // indirect
|
github.com/olekukonko/ll v0.1.8 // indirect
|
||||||
github.com/olekukonko/tablewriter v1.1.4 // indirect
|
|
||||||
github.com/tlinden/yadu v0.1.3 // indirect
|
|
||||||
github.com/urfave/cli/v3 v3.8.0 // indirect
|
|
||||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||||
go.opentelemetry.io/otel v1.35.0 // indirect
|
go.opentelemetry.io/otel v1.35.0 // indirect
|
||||||
go.opentelemetry.io/otel/metric v1.35.0 // indirect
|
go.opentelemetry.io/otel/metric v1.35.0 // indirect
|
||||||
go.opentelemetry.io/otel/trace v1.35.0 // indirect
|
go.opentelemetry.io/otel/trace v1.35.0 // indirect
|
||||||
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f // indirect
|
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f // indirect
|
||||||
golang.org/x/sys v0.42.0 // indirect
|
golang.org/x/sys v0.42.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
|
||||||
)
|
)
|
||||||
|
|||||||
19
go.sum
19
go.sum
@@ -2,16 +2,24 @@ 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/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 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
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 h1:GhBG8WuerxjFQQYeuZAeVTuyxuX+UraiZGD4HJQ3Y8g=
|
||||||
github.com/clipperhouse/displaywidth v0.10.0/go.mod h1:XqJajYsaiEwkxOj4bowCTMcT1SgvHo9flfF3jQasdbs=
|
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 h1:z0cDbUV+aPASdFb2/ndFnS9ts/WNXgTNNGFoKXuhpos=
|
||||||
github.com/clipperhouse/uax29/v2 v2.6.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
|
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 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.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=
|
github.com/elastic/elastic-transport-go/v8 v8.11.0 h1:taYmqC2M6+fZt/+W+ENYh/W5L9+KrlJGOSbEJs8egWc=
|
||||||
github.com/elastic/elastic-transport-go/v8 v8.11.0/go.mod h1:DZQ0szCNywc9F+C9l/Kkd4n69SvJVj0I3yK1Of7s3l8=
|
github.com/elastic/elastic-transport-go/v8 v8.11.0/go.mod h1:DZQ0szCNywc9F+C9l/Kkd4n69SvJVj0I3yK1Of7s3l8=
|
||||||
github.com/elastic/go-elasticsearch/v9 v9.3.2 h1:nvtvfN/Gsp/rzUPz/9yILwDAsYJ3s5L0VmhR16zPKCA=
|
github.com/elastic/go-elasticsearch/v9 v9.3.2 h1:nvtvfN/Gsp/rzUPz/9yILwDAsYJ3s5L0VmhR16zPKCA=
|
||||||
github.com/elastic/go-elasticsearch/v9 v9.3.2/go.mod h1:ubKUMJCJbX5V/gW5MIn2NQZyaEZ61ubXwJmD5UMNrM8=
|
github.com/elastic/go-elasticsearch/v9 v9.3.2/go.mod h1:ubKUMJCJbX5V/gW5MIn2NQZyaEZ61ubXwJmD5UMNrM8=
|
||||||
|
github.com/elastic/go-elasticsearch/v9 v9.3.4 h1:vnqXl6jnlA+ZwlfRaKF9BR9woZC1KSB17gtyvgf8zVU=
|
||||||
|
github.com/elastic/go-elasticsearch/v9 v9.3.4/go.mod h1:ubKUMJCJbX5V/gW5MIn2NQZyaEZ61ubXwJmD5UMNrM8=
|
||||||
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
|
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
|
||||||
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
|
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
|
||||||
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||||
@@ -23,6 +31,8 @@ github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
|||||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||||
|
github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU=
|
||||||
|
github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||||
github.com/lmittmann/tint v1.1.3 h1:Hv4EaHWXQr+GTFnOU4VKf8UvAtZgn0VuKT+G0wFlO3I=
|
github.com/lmittmann/tint v1.1.3 h1:Hv4EaHWXQr+GTFnOU4VKf8UvAtZgn0VuKT+G0wFlO3I=
|
||||||
github.com/lmittmann/tint v1.1.3/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE=
|
github.com/lmittmann/tint v1.1.3/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE=
|
||||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||||
@@ -32,20 +42,28 @@ github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stg
|
|||||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
github.com/mattn/go-isatty v0.0.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs=
|
github.com/mattn/go-isatty v0.0.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs=
|
||||||
github.com/mattn/go-isatty v0.0.21/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
|
github.com/mattn/go-isatty v0.0.21/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
|
||||||
|
github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
|
||||||
|
github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
|
||||||
github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
|
github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
|
||||||
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
||||||
|
github.com/mattn/go-runewidth v0.0.23 h1:7ykA0T0jkPpzSvMS5i9uoNn2Xy3R383f9HDx3RybWcw=
|
||||||
|
github.com/mattn/go-runewidth v0.0.23/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
||||||
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 h1:zrbMGy9YXpIeTnGj4EljqMiZsIcE09mmF8XsD5AYOJc=
|
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 h1:zrbMGy9YXpIeTnGj4EljqMiZsIcE09mmF8XsD5AYOJc=
|
||||||
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6/go.mod h1:rEKTHC9roVVicUIfZK7DYrdIoM0EOr8mK1Hj5s3JjH0=
|
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6/go.mod h1:rEKTHC9roVVicUIfZK7DYrdIoM0EOr8mK1Hj5s3JjH0=
|
||||||
github.com/olekukonko/errors v1.2.0 h1:10Zcn4GeV59t/EGqJc8fUjtFT/FuUh5bTMzZ1XwmCRo=
|
github.com/olekukonko/errors v1.2.0 h1:10Zcn4GeV59t/EGqJc8fUjtFT/FuUh5bTMzZ1XwmCRo=
|
||||||
github.com/olekukonko/errors v1.2.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y=
|
github.com/olekukonko/errors v1.2.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y=
|
||||||
github.com/olekukonko/ll v0.1.6 h1:lGVTHO+Qc4Qm+fce/2h2m5y9LvqaW+DCN7xW9hsU3uA=
|
github.com/olekukonko/ll v0.1.6 h1:lGVTHO+Qc4Qm+fce/2h2m5y9LvqaW+DCN7xW9hsU3uA=
|
||||||
github.com/olekukonko/ll v0.1.6/go.mod h1:NVUmjBb/aCtUpjKk75BhWrOlARz3dqsM+OtszpY4o88=
|
github.com/olekukonko/ll v0.1.6/go.mod h1:NVUmjBb/aCtUpjKk75BhWrOlARz3dqsM+OtszpY4o88=
|
||||||
|
github.com/olekukonko/ll v0.1.8 h1:ysHCJRGHYKzmBSdz9w5AySztx7lG8SQY+naTGYUbsz8=
|
||||||
|
github.com/olekukonko/ll v0.1.8/go.mod h1:RPRC6UcscfFZgjo1nulkfMH5IM0QAYim0LfnMvUuozw=
|
||||||
github.com/olekukonko/tablewriter v1.1.4 h1:ORUMI3dXbMnRlRggJX3+q7OzQFDdvgbN9nVWj1drm6I=
|
github.com/olekukonko/tablewriter v1.1.4 h1:ORUMI3dXbMnRlRggJX3+q7OzQFDdvgbN9nVWj1drm6I=
|
||||||
github.com/olekukonko/tablewriter v1.1.4/go.mod h1:+kedxuyTtgoZLwif3P1Em4hARJs+mVnzKxmsCL/C5RY=
|
github.com/olekukonko/tablewriter v1.1.4/go.mod h1:+kedxuyTtgoZLwif3P1Em4hARJs+mVnzKxmsCL/C5RY=
|
||||||
github.com/tlinden/yadu v0.1.3 h1:5cRCUmj+l5yvlM2irtpFBIJwVV2DPEgYSaWvF19FtcY=
|
github.com/tlinden/yadu v0.1.3 h1:5cRCUmj+l5yvlM2irtpFBIJwVV2DPEgYSaWvF19FtcY=
|
||||||
github.com/tlinden/yadu v0.1.3/go.mod h1:l3bRmHKL9zGAR6pnBHY2HRPxBecf7L74BoBgOOpTcUA=
|
github.com/tlinden/yadu v0.1.3/go.mod h1:l3bRmHKL9zGAR6pnBHY2HRPxBecf7L74BoBgOOpTcUA=
|
||||||
github.com/urfave/cli/v3 v3.8.0 h1:XqKPrm0q4P0q5JpoclYoCAv0/MIvH/jZ2umzuf8pNTI=
|
github.com/urfave/cli/v3 v3.8.0 h1:XqKPrm0q4P0q5JpoclYoCAv0/MIvH/jZ2umzuf8pNTI=
|
||||||
github.com/urfave/cli/v3 v3.8.0/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso=
|
github.com/urfave/cli/v3 v3.8.0/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso=
|
||||||
|
github.com/urfave/cli/v3 v3.9.0 h1:AV9lIiPv3ukYnxunaCUsHnEozptYmDN2F0+yWqLMn/c=
|
||||||
|
github.com/urfave/cli/v3 v3.9.0/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso=
|
||||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||||
go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
|
go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
|
||||||
@@ -56,6 +74,7 @@ go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt
|
|||||||
go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
|
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 h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM=
|
||||||
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80=
|
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.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 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
||||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
Version string = `v0.0.8`
|
Version string = `v0.0.10`
|
||||||
)
|
)
|
||||||
|
|
||||||
type Cluster struct {
|
type Cluster struct {
|
||||||
@@ -47,13 +47,16 @@ type Config struct {
|
|||||||
DefaultCluster *Cluster
|
DefaultCluster *Cluster
|
||||||
Index string // index: -i
|
Index string // index: -i
|
||||||
Failed, Partials bool // index: flags
|
Failed, Partials bool // index: flags
|
||||||
Shards, Replicas int // index create: -s -r
|
Shards, Replicas int // index create+allocation: -s -r
|
||||||
Wait bool // index create: -w
|
Wait bool // index create: -w
|
||||||
|
Primary bool // index allocation: -p
|
||||||
From, To, MaxItems int // search: flags
|
From, To, MaxItems int // search: flags
|
||||||
Filter []string // search: -F
|
Filter []string // search: -F
|
||||||
Exclude string // cluster compare: -e (regexp)
|
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
|
Persistent, Transient, Default bool // -p -t -D cluster settings set
|
||||||
|
Force bool // ccr follower renew: -f
|
||||||
|
HaveJQ bool // determined at runtime by ourselfes
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewConfig() *Config {
|
func NewConfig() *Config {
|
||||||
@@ -109,6 +112,8 @@ func (conf *Config) Init() error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
conf.HaveJQ = isJQinstalled()
|
||||||
|
|
||||||
conf.PrintDebug()
|
conf.PrintDebug()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
38
pkg/cfg/jq.go
Normal file
38
pkg/cfg/jq.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 cfg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func isJQinstalled() bool {
|
||||||
|
cmd := exec.CommandContext(context.Background(), "jq", "-h")
|
||||||
|
out, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.Contains(string(out), "Usage") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
@@ -85,3 +85,50 @@ func CcrStatus(conf *cfg.Config, leader, follower string) error {
|
|||||||
|
|
||||||
return nil
|
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("ccr remote property", "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
|
||||||
|
}
|
||||||
|
|||||||
@@ -24,13 +24,13 @@ import (
|
|||||||
"codeberg.org/scip/esctl/pkg/cfg"
|
"codeberg.org/scip/esctl/pkg/cfg"
|
||||||
)
|
)
|
||||||
|
|
||||||
func CcrFollowerAdd(conf *cfg.Config, index string) error {
|
func getRemoteName(conf *cfg.Config) (string, error) {
|
||||||
res, err := conf.DefaultCluster.ES.Cluster.RemoteInfo().
|
res, err := conf.DefaultCluster.ES.Cluster.RemoteInfo().
|
||||||
Header("content-type", "application/json").
|
Header("content-type", "application/json").
|
||||||
Header("accept", "application/json").
|
Header("accept", "application/json").
|
||||||
Do(context.Background())
|
Do(context.Background())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to retrieve follower info: %s", err)
|
return "", fmt.Errorf("failed to retrieve follower info: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
remote := ""
|
remote := ""
|
||||||
@@ -40,7 +40,99 @@ func CcrFollowerAdd(conf *cfg.Config, index string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if remote == "" {
|
if remote == "" {
|
||||||
return fmt.Errorf("cluster doesn't have a follower: %s", err)
|
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).
|
create := conf.DefaultCluster.ES.Ccr.Follow(index).
|
||||||
@@ -84,7 +176,7 @@ func CcrFollowerShow(conf *cfg.Config, index string) error {
|
|||||||
follower := res.Indices[0].Shards[0]
|
follower := res.Indices[0].Shards[0]
|
||||||
|
|
||||||
table := NewTable(2, 9)
|
table := NewTable(2, 9)
|
||||||
table.Addheaders("field", "value")
|
table.Addheaders("ccr follower property", "value")
|
||||||
|
|
||||||
table.entries = [][]string{
|
table.entries = [][]string{
|
||||||
{"name", index},
|
{"name", index},
|
||||||
|
|||||||
@@ -21,11 +21,14 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"slices"
|
"slices"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"codeberg.org/scip/esctl/pkg/cfg"
|
"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/ccr/stats"
|
||||||
"github.com/elastic/go-elasticsearch/v9/typedapi/cluster/health"
|
"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/core/info"
|
||||||
"github.com/elastic/go-elasticsearch/v9/typedapi/types"
|
"github.com/elastic/go-elasticsearch/v9/typedapi/types"
|
||||||
)
|
)
|
||||||
@@ -34,6 +37,7 @@ const (
|
|||||||
ResponseHealth = iota
|
ResponseHealth = iota
|
||||||
ResponseInfo
|
ResponseInfo
|
||||||
ResponseCcr
|
ResponseCcr
|
||||||
|
ResponseStats
|
||||||
)
|
)
|
||||||
|
|
||||||
type ClusterIndices map[string]map[string]*types.IndicesRecord
|
type ClusterIndices map[string]map[string]*types.IndicesRecord
|
||||||
@@ -43,6 +47,7 @@ type apiResponse struct {
|
|||||||
info *info.Response
|
info *info.Response
|
||||||
health *health.Response
|
health *health.Response
|
||||||
ccr *stats.Response
|
ccr *stats.Response
|
||||||
|
stats *clusterstats.Response
|
||||||
which int
|
which int
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,6 +93,10 @@ func ClusterList(conf *cfg.Config) error {
|
|||||||
// have to do 3 of'em for each cluster. This speeds things up.
|
// have to do 3 of'em for each cluster. This speeds things up.
|
||||||
func ClusterStatus(conf *cfg.Config) error {
|
func ClusterStatus(conf *cfg.Config) error {
|
||||||
clusters := []string{}
|
clusters := []string{}
|
||||||
|
gocount := 3
|
||||||
|
if conf.Verbose {
|
||||||
|
gocount++
|
||||||
|
}
|
||||||
|
|
||||||
if conf.All {
|
if conf.All {
|
||||||
for key := range conf.Clusters {
|
for key := range conf.Clusters {
|
||||||
@@ -103,21 +112,26 @@ func ClusterStatus(conf *cfg.Config) error {
|
|||||||
es = conf.Clusters[cluster].ES
|
es = conf.Clusters[cluster].ES
|
||||||
}
|
}
|
||||||
|
|
||||||
responses := make(chan apiResponse, 3)
|
responses := make(chan apiResponse, gocount)
|
||||||
wg := &sync.WaitGroup{}
|
wg := &sync.WaitGroup{}
|
||||||
|
|
||||||
wg.Add(3)
|
wg.Add(gocount)
|
||||||
go getClusterData(es, wg, responses, "health")
|
go getClusterData(es, wg, responses, "health")
|
||||||
go getClusterData(es, wg, responses, "info")
|
go getClusterData(es, wg, responses, "info")
|
||||||
go getClusterData(es, wg, responses, "ccrstats")
|
go getClusterData(es, wg, responses, "ccrstats")
|
||||||
|
|
||||||
|
if conf.Verbose {
|
||||||
|
go getClusterData(es, wg, responses, "stats")
|
||||||
|
}
|
||||||
|
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
|
|
||||||
var clusterhealth *health.Response
|
var clusterhealth *health.Response
|
||||||
var info *info.Response
|
var info *info.Response
|
||||||
var ccrstats *stats.Response
|
var ccrstats *stats.Response
|
||||||
|
var clusterstats *clusterstats.Response
|
||||||
|
|
||||||
for i := 0; i < 3; i++ {
|
for i := 0; i < gocount; i++ {
|
||||||
r := <-responses
|
r := <-responses
|
||||||
|
|
||||||
if r.error != nil {
|
if r.error != nil {
|
||||||
@@ -131,6 +145,8 @@ func ClusterStatus(conf *cfg.Config) error {
|
|||||||
ccrstats = r.ccr
|
ccrstats = r.ccr
|
||||||
case ResponseInfo:
|
case ResponseInfo:
|
||||||
info = r.info
|
info = r.info
|
||||||
|
case ResponseStats:
|
||||||
|
clusterstats = r.stats
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -154,11 +170,14 @@ func ClusterStatus(conf *cfg.Config) error {
|
|||||||
{"ES Version", info.Version.Int},
|
{"ES Version", info.Version.Int},
|
||||||
{"Active Shards", fmt.Sprintf("%d", clusterhealth.ActiveShards)},
|
{"Active Shards", fmt.Sprintf("%d", clusterhealth.ActiveShards)},
|
||||||
{"Active Primary Shards", fmt.Sprintf("%d", clusterhealth.ActivePrimaryShards)},
|
{"Active Primary Shards", fmt.Sprintf("%d", clusterhealth.ActivePrimaryShards)},
|
||||||
{"Indicies", fmt.Sprintf("%d", len(clusterhealth.Indices))},
|
|
||||||
{"Nodes", fmt.Sprintf("%d", clusterhealth.NumberOfNodes)},
|
{"Nodes", fmt.Sprintf("%d", clusterhealth.NumberOfNodes)},
|
||||||
{"AutoFollow (success/failed indices)", ccrfollowing},
|
{"AutoFollow (success/failed indices)", ccrfollowing},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if conf.Verbose {
|
||||||
|
table = gatherClusterStats(conf, clusterstats, table)
|
||||||
|
}
|
||||||
|
|
||||||
if err := table.PrintMarkdown(); err != nil {
|
if err := table.PrintMarkdown(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -166,3 +185,47 @@ func ClusterStatus(conf *cfg.Config) error {
|
|||||||
|
|
||||||
return nil
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -375,6 +375,16 @@ func getClusterData(es *elasticsearch.TypedClient, wg *sync.WaitGroup, reschan c
|
|||||||
ar.ccr = res
|
ar.ccr = res
|
||||||
ar.which = ResponseCcr
|
ar.which = ResponseCcr
|
||||||
arerr = err
|
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 {
|
if arerr != nil {
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ func IndexShow(conf *cfg.Config, index string) error {
|
|||||||
slog.Debug("ES result", "index", res)
|
slog.Debug("ES result", "index", res)
|
||||||
|
|
||||||
table := NewTable(2, 5)
|
table := NewTable(2, 5)
|
||||||
table.Addheaders("field", "value")
|
table.Addheaders("index property", "value")
|
||||||
|
|
||||||
ts, err := strconv.ParseInt(res[index].Settings.Index.CreationDate.(string), 10, 64)
|
ts, err := strconv.ParseInt(res[index].Settings.Index.CreationDate.(string), 10, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -202,3 +202,66 @@ func IndexDelete(conf *cfg.Config, index string) error {
|
|||||||
|
|
||||||
return nil
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
func IndexAllocation(conf *cfg.Config, index string) error {
|
||||||
|
if index == "" {
|
||||||
|
return fmt.Errorf("no index specified")
|
||||||
|
}
|
||||||
|
|
||||||
|
alloc := conf.DefaultCluster.ES.Cluster.AllocationExplain().
|
||||||
|
Index(index).
|
||||||
|
Primary(conf.Primary).
|
||||||
|
Shard(conf.Shards)
|
||||||
|
|
||||||
|
res, err := alloc.Header("content-type", "application/json").
|
||||||
|
Header("accept", "application/json").
|
||||||
|
Do(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get index allocation explain: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.Debug("ES result", "index", res)
|
||||||
|
|
||||||
|
currentNode := res.CurrentNode
|
||||||
|
|
||||||
|
table := NewTable(2, 10)
|
||||||
|
table.Addheaders("index allocation setting", "value")
|
||||||
|
|
||||||
|
roles := make([]string, len(currentNode.Roles))
|
||||||
|
for idx, role := range currentNode.Roles {
|
||||||
|
roles[idx] = role.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
table.entries = [][]string{
|
||||||
|
{"Index", index},
|
||||||
|
{"Current node", currentNode.Name},
|
||||||
|
{"Current k8s node", currentNode.Attributes["k8s_node_name"]},
|
||||||
|
{"Current node address", currentNode.TransportAddress},
|
||||||
|
{"Current node id", currentNode.Id},
|
||||||
|
{"Current node weight", fmt.Sprintf("%d", currentNode.WeightRanking)},
|
||||||
|
{"Current node roles", strings.Join(roles, ",")},
|
||||||
|
{"Can rebalance cluster", res.CanRebalanceCluster.Name},
|
||||||
|
{"Can rebalance to another node", res.CanRebalanceToOtherNode.Name},
|
||||||
|
{"Can remain on current node", res.CanRemainOnCurrentNode.Name},
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := table.PrintMarkdown(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
176
pkg/es/repl.go
Normal file
176
pkg/es/repl.go
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
/*
|
||||||
|
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"
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"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)
|
||||||
|
}
|
||||||
|
|
||||||
|
return prettyfiJson(conf, body)
|
||||||
|
}
|
||||||
|
|
||||||
|
func prettyfiJson(conf *cfg.Config, raw []byte) error {
|
||||||
|
if conf.HaveJQ {
|
||||||
|
cmd := exec.CommandContext(context.Background(), "jq", "-C")
|
||||||
|
cmd.Stdin = bytes.NewReader(raw)
|
||||||
|
|
||||||
|
var out bytes.Buffer
|
||||||
|
cmd.Stdout = &out
|
||||||
|
|
||||||
|
err := cmd.Run()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println(out.String())
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var pretty bytes.Buffer
|
||||||
|
err := json.Indent(&pretty, raw, "", "\t")
|
||||||
|
if err != 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
|
||||||
|
}
|
||||||
@@ -117,7 +117,7 @@ func SnapshotShow(conf *cfg.Config, snapshot string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
table := NewTable(2, 17)
|
table := NewTable(2, 17)
|
||||||
table.Addheaders("field", "value")
|
table.Addheaders("snapshot property", "value")
|
||||||
|
|
||||||
snap := res.Snapshots[0]
|
snap := res.Snapshots[0]
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user