mirror of
https://codeberg.org/scip/esctl.git
synced 2026-08-24 12:44:18 +02:00
Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 355e3051b4 | |||
|
|
a81d6b94e9 | ||
|
|
9b5ba0775b | ||
|
|
802217158c | ||
| 5d345aaa9a | |||
| 2f029304f3 | |||
| 49bc2c7acb | |||
|
|
f1ff361c12 | ||
| 072e949147 | |||
|
|
ce4a60f8e2 | ||
|
|
97004e0957 | ||
|
|
d1f3082e5a | ||
|
|
d0d1d69791 | ||
|
|
ec1ce56b2d |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -30,3 +30,5 @@ esctl
|
||||
*.sh
|
||||
*.json
|
||||
*.log
|
||||
|
||||
cpu.profile
|
||||
|
||||
@@ -2,7 +2,7 @@ matrix:
|
||||
platform:
|
||||
- linux/amd64
|
||||
goversion:
|
||||
- 1.25
|
||||
- 1.25.8
|
||||
|
||||
labels:
|
||||
platform: ${platform}
|
||||
|
||||
33
Makefile
33
Makefile
@@ -23,17 +23,27 @@ UID = root
|
||||
GID = 0
|
||||
HAVE_POD := $(shell pod2text -h 2>/dev/null)
|
||||
|
||||
BRANCH = $(shell git branch --show-current)
|
||||
COMMIT = $(shell git rev-parse --short=8 HEAD)
|
||||
BUILD = $(shell date +%Y.%m.%d.%H%M%S)
|
||||
GOVERSION = $(shell grep -E "^go" go.mod | cut -d' ' -f2)
|
||||
APIVERSION = $(shell grep github.com/elastic/go-elasticsearch go.mod | cut -d' ' -f2)
|
||||
|
||||
BRANCH = $(shell git branch --show-current)
|
||||
COMMIT = $(shell git rev-parse --short=8 HEAD)
|
||||
BUILD = $(shell date +%Y.%m.%d.%H%M%S)
|
||||
GOVERSION = $(shell grep -E "^go" go.mod | cut -d' ' -f2)
|
||||
APIVERSION = $(shell grep github.com/elastic/go-elasticsearch go.mod | cut -d' ' -f2)
|
||||
APISPEC = "https://www.elastic.co/docs/api/doc/elasticsearch.json"
|
||||
|
||||
all: buildlocal
|
||||
|
||||
|
||||
buildlocal:
|
||||
# pkg/es/openspec.go:
|
||||
# echo "package es" > pkg/es/openspec.go
|
||||
# echo >> pkg/es/openspec.go
|
||||
# echo "const Openspec = \`" >> pkg/es/openspec.go
|
||||
# curl -o - $(APISPEC) | sed "s/\`/'/g" >> pkg/es/openspec.go
|
||||
# echo "\`" >> pkg/es/openspec.go
|
||||
|
||||
assets/openapi.json:
|
||||
curl -o assets/openapi.json $(APISPEC)
|
||||
|
||||
buildlocal: assets/openapi.json
|
||||
CGO_LDFLAGS='-static' go build -tags osusergo,netgo -ldflags "-extldflags=-static" \
|
||||
-ldflags "-X codeberg.org/scip/esctl/pkg/cfg.BRANCH=$(BRANCH) \
|
||||
-X codeberg.org/scip/esctl/pkg/cfg.COMMIT=$(COMMIT) \
|
||||
@@ -50,7 +60,7 @@ install: buildlocal
|
||||
install -o $(UID) -g $(GID) -m 444 $(tool).1 $(PREFIX)/man/man1/
|
||||
|
||||
clean:
|
||||
rm -rf $(tool) coverage.out testdata t/out
|
||||
rm -rf $(tool) coverage.out testdata t/out pkg/es/openspec.go
|
||||
|
||||
test: clean
|
||||
mkdir -p t/out
|
||||
@@ -96,5 +106,8 @@ show-versions: buildlocal
|
||||
@echo "### go version used for building:"
|
||||
@grep -m 1 go go.mod
|
||||
|
||||
# lint:
|
||||
# golangci-lint run -p bugs -p unused
|
||||
.PHONY: profile
|
||||
profile: buildlocal
|
||||
./esctl api ls --profile-file cpu.profile
|
||||
go tool pprof -text esctl cpu.profile
|
||||
go tool pprof --http localhost:8888 ./esctl cpu.profile
|
||||
|
||||
28
README.md
28
README.md
@@ -51,6 +51,10 @@ Features:
|
||||
Command tree:
|
||||
|
||||
```console
|
||||
api
|
||||
list
|
||||
repl
|
||||
show
|
||||
ccr
|
||||
follower
|
||||
add
|
||||
@@ -70,6 +74,12 @@ Command tree:
|
||||
list
|
||||
set
|
||||
status
|
||||
datastream
|
||||
create
|
||||
delete
|
||||
list
|
||||
rollover
|
||||
show
|
||||
debug
|
||||
doc
|
||||
add
|
||||
@@ -77,23 +87,36 @@ Command tree:
|
||||
show
|
||||
help
|
||||
help-jsonpath
|
||||
ilm
|
||||
create
|
||||
list
|
||||
retry
|
||||
show
|
||||
status
|
||||
index
|
||||
alias
|
||||
create
|
||||
delete
|
||||
list
|
||||
rollover
|
||||
allocation
|
||||
close
|
||||
create
|
||||
delete
|
||||
fields
|
||||
ilm
|
||||
list
|
||||
modify
|
||||
show
|
||||
template
|
||||
create
|
||||
delete
|
||||
list
|
||||
modify
|
||||
show
|
||||
node
|
||||
list
|
||||
show
|
||||
repl
|
||||
role
|
||||
diff
|
||||
list
|
||||
@@ -105,6 +128,9 @@ Command tree:
|
||||
snapshot
|
||||
list
|
||||
show
|
||||
task
|
||||
cancel
|
||||
list
|
||||
version
|
||||
```
|
||||
|
||||
|
||||
43
assets/assets.go
Normal file
43
assets/assets.go
Normal file
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
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 assets
|
||||
|
||||
import (
|
||||
"embed"
|
||||
|
||||
"github.com/go-openapi/loads"
|
||||
"github.com/go-openapi/swag/loading"
|
||||
)
|
||||
|
||||
//go:embed *.json
|
||||
var AssetFS embed.FS
|
||||
|
||||
var OpenAPI *loads.Document
|
||||
|
||||
func LoadAssetOpenApi() {
|
||||
doc, err := loads.Spec(
|
||||
"openapi.json",
|
||||
loads.WithLoadingOptions(
|
||||
loading.WithFS(AssetFS),
|
||||
))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
OpenAPI = doc
|
||||
}
|
||||
160985
assets/openapi.json
Normal file
160985
assets/openapi.json
Normal file
File diff suppressed because one or more lines are too long
90
cmd/api.go
Normal file
90
cmd/api.go
Normal file
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
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 Api(conf *cfg.Config) *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "api",
|
||||
Usage: "manage apiuments",
|
||||
|
||||
Commands: []*cli.Command{
|
||||
ApiList(conf),
|
||||
ApiShow(conf),
|
||||
ApiRepl(conf),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func ApiList(conf *cfg.Config) *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "list",
|
||||
Aliases: []string{"ls"},
|
||||
Usage: "list index of API calls",
|
||||
UsageText: "list [<pattern>]",
|
||||
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "tag",
|
||||
Usage: "list only operations matching <tag>",
|
||||
Destination: &conf.Tag,
|
||||
Aliases: []string{"t"},
|
||||
},
|
||||
},
|
||||
|
||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||
return es.ApiList(conf, cmd.Args().Get(0))
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func ApiShow(conf *cfg.Config) *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "show",
|
||||
Aliases: []string{"sh"},
|
||||
Usage: "show an API doc",
|
||||
UsageText: "show <pattern> [<verb>]",
|
||||
|
||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||
return es.ApiShow(conf, cmd.Args().Get(0), cmd.Args().Get(1))
|
||||
},
|
||||
|
||||
ShellComplete: func(ctx context.Context, cmd *cli.Command) {
|
||||
complete(cmd, Capi)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func ApiRepl(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.ApiRepl(conf)
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -59,7 +59,7 @@ func CcrStatus(conf *cfg.Config) *cli.Command {
|
||||
},
|
||||
|
||||
ShellComplete: func(ctx context.Context, cmd *cli.Command) {
|
||||
completeCluster(cmd)
|
||||
complete(cmd, Ccluster)
|
||||
},
|
||||
|
||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||
@@ -115,7 +115,7 @@ func CcrRemoteInfo(conf *cfg.Config) *cli.Command {
|
||||
UsageText: "info [options] [<index>]",
|
||||
|
||||
ShellComplete: func(ctx context.Context, cmd *cli.Command) {
|
||||
completeIndex(cmd)
|
||||
complete(cmd, Cindex)
|
||||
},
|
||||
|
||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||
|
||||
@@ -60,7 +60,7 @@ func CcrFollowerRenew(conf *cfg.Config) *cli.Command {
|
||||
},
|
||||
|
||||
ShellComplete: func(ctx context.Context, cmd *cli.Command) {
|
||||
completeIndex(cmd)
|
||||
complete(cmd, Cindex)
|
||||
},
|
||||
|
||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||
@@ -82,7 +82,7 @@ func CcrFollowerResume(conf *cfg.Config) *cli.Command {
|
||||
UsageText: "resume [options] <index>",
|
||||
|
||||
ShellComplete: func(ctx context.Context, cmd *cli.Command) {
|
||||
completeIndex(cmd)
|
||||
complete(cmd, Cindex)
|
||||
},
|
||||
|
||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||
@@ -104,7 +104,7 @@ func CcrFollowerPause(conf *cfg.Config) *cli.Command {
|
||||
UsageText: "pause [options] <index>",
|
||||
|
||||
ShellComplete: func(ctx context.Context, cmd *cli.Command) {
|
||||
completeIndex(cmd)
|
||||
complete(cmd, Cindex)
|
||||
},
|
||||
|
||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||
@@ -126,7 +126,7 @@ func CcrFollowerUnfollow(conf *cfg.Config) *cli.Command {
|
||||
UsageText: "unfollow [options] <index>",
|
||||
|
||||
ShellComplete: func(ctx context.Context, cmd *cli.Command) {
|
||||
completeIndex(cmd)
|
||||
complete(cmd, Cindex)
|
||||
},
|
||||
|
||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||
@@ -158,7 +158,7 @@ func CcrFollowerAdd(conf *cfg.Config) *cli.Command {
|
||||
},
|
||||
|
||||
ShellComplete: func(ctx context.Context, cmd *cli.Command) {
|
||||
completeIndex(cmd)
|
||||
complete(cmd, Cindex)
|
||||
},
|
||||
|
||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||
@@ -181,7 +181,7 @@ func CcrFollowerDelete(conf *cfg.Config) *cli.Command {
|
||||
UsageText: "delete <index>",
|
||||
|
||||
ShellComplete: func(ctx context.Context, cmd *cli.Command) {
|
||||
completeIndex(cmd)
|
||||
complete(cmd, Cindex)
|
||||
},
|
||||
|
||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||
@@ -205,7 +205,7 @@ func CcrFollowerShow(conf *cfg.Config) *cli.Command {
|
||||
UsageText: "show <index>",
|
||||
|
||||
ShellComplete: func(ctx context.Context, cmd *cli.Command) {
|
||||
completeIndex(cmd)
|
||||
complete(cmd, Cindex)
|
||||
},
|
||||
|
||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||
|
||||
@@ -27,7 +27,7 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
SETTINGS = `https://www.elastic.co/docs/reference/elasticsearch/configuration-reference`
|
||||
SETTINGS = ``
|
||||
)
|
||||
|
||||
func ClusterSettings(conf *cfg.Config) *cli.Command {
|
||||
@@ -85,7 +85,9 @@ func ClusterSettingsSet(conf *cfg.Config) *cli.Command {
|
||||
Name: "set",
|
||||
Usage: "set|update cluster settings",
|
||||
Aliases: []string{"set", "update"},
|
||||
UsageText: "set [options] setting:value [setting:value ...]\n\nReference: " + SETTINGS,
|
||||
UsageText: "set [options] setting:value [setting:value ...]",
|
||||
CustomHelpTemplate: addReference(
|
||||
"https://www.elastic.co/docs/reference/elasticsearch/configuration-reference"),
|
||||
|
||||
Flags: []cli.Flag{
|
||||
&cli.BoolFlag{
|
||||
|
||||
@@ -25,7 +25,16 @@ import (
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
func completeIndex(cmd *cli.Command) {
|
||||
const (
|
||||
Cindex = iota
|
||||
Cdatastream
|
||||
Crole
|
||||
Ccluster
|
||||
Capi
|
||||
Cilm
|
||||
)
|
||||
|
||||
func complete(cmd *cli.Command, what int) {
|
||||
if cmd.NArg() > 0 {
|
||||
return
|
||||
}
|
||||
@@ -37,53 +46,31 @@ func completeIndex(cmd *cli.Command) {
|
||||
return
|
||||
}
|
||||
|
||||
indices, err := es.IndexNames(conf)
|
||||
var list []string
|
||||
var err error
|
||||
|
||||
switch what {
|
||||
case Cindex:
|
||||
list, err = es.IndexNames(conf)
|
||||
case Crole:
|
||||
list, err = es.RoleNames(conf)
|
||||
case Ccluster:
|
||||
for cluster := range conf.Clusters {
|
||||
list = append(list, cluster)
|
||||
}
|
||||
case Cdatastream:
|
||||
list, err = es.DatastreamNames(conf)
|
||||
case Capi:
|
||||
list = es.ApiPathNames()
|
||||
case Cilm:
|
||||
list, err = es.IlmNames(conf)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, index := range indices {
|
||||
fmt.Println(index)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func completeRole(cmd *cli.Command) {
|
||||
if cmd.NArg() > 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// FIXME: config should load from root.Before(), see https://github.com/urfave/cli/issues/2348
|
||||
// workaround: load it directly here
|
||||
conf := cfg.NewConfig()
|
||||
if err := conf.Init(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
roles, err := es.RoleNames(conf)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, role := range roles {
|
||||
fmt.Println(role)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func completeCluster(cmd *cli.Command) {
|
||||
if cmd.NArg() > 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// FIXME: config should load from root.Before(), see https://github.com/urfave/cli/issues/2348
|
||||
// workaround: load it directly here
|
||||
conf := cfg.NewConfig()
|
||||
if err := conf.Init(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for cluster := range conf.Clusters {
|
||||
fmt.Println(cluster)
|
||||
for _, item := range list {
|
||||
fmt.Println(item)
|
||||
}
|
||||
}
|
||||
|
||||
220
cmd/datastream.go
Normal file
220
cmd/datastream.go
Normal file
@@ -0,0 +1,220 @@
|
||||
/*
|
||||
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 Datastream(conf *cfg.Config) *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "datastream",
|
||||
Aliases: []string{"ds"},
|
||||
Usage: "manage data streams",
|
||||
|
||||
Commands: []*cli.Command{
|
||||
DatastreamList(conf),
|
||||
DatastreamShow(conf),
|
||||
DatastreamCreate(conf),
|
||||
DatastreamDelete(conf),
|
||||
DatastreamRollover(conf),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func DatastreamList(conf *cfg.Config) *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "list",
|
||||
Aliases: []string{"ls"},
|
||||
Usage: "list indicies",
|
||||
|
||||
Flags: []cli.Flag{
|
||||
&cli.IntFlag{
|
||||
Name: "max",
|
||||
Usage: "max number of items to process",
|
||||
Destination: &conf.MaxItems,
|
||||
Aliases: []string{"m"},
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "hidden",
|
||||
Usage: "include hidden data streams",
|
||||
Destination: &conf.Hidden,
|
||||
Aliases: []string{"H"},
|
||||
},
|
||||
&cli.StringSliceFlag{
|
||||
Name: "filter",
|
||||
Usage: "show only data streams matching the filter",
|
||||
Destination: &conf.Filter,
|
||||
Aliases: []string{"F"},
|
||||
},
|
||||
},
|
||||
|
||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||
return es.DatastreamList(conf)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func DatastreamShow(conf *cfg.Config) *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "show",
|
||||
Aliases: []string{"sh"},
|
||||
Usage: "show details about an data stream",
|
||||
|
||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||
index := cmd.Args().Get(0)
|
||||
if index == "" {
|
||||
return errors.New("no data stream specified")
|
||||
}
|
||||
|
||||
return es.DatastreamShow(conf, cmd.Args().Get(0))
|
||||
},
|
||||
|
||||
ShellComplete: func(ctx context.Context, cmd *cli.Command) {
|
||||
complete(cmd, Cdatastream)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func DatastreamCreate(conf *cfg.Config) *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "create",
|
||||
Aliases: []string{"+"},
|
||||
Usage: "create a new data stream",
|
||||
UsageText: "create [options] <name>",
|
||||
|
||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||
index := cmd.Args().Get(0)
|
||||
if index == "" {
|
||||
return errors.New("no data stream specified")
|
||||
}
|
||||
|
||||
return es.DatastreamCreate(conf, cmd.Args().Get(0))
|
||||
},
|
||||
|
||||
ShellComplete: func(ctx context.Context, cmd *cli.Command) {
|
||||
complete(cmd, Cdatastream)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func DatastreamDelete(conf *cfg.Config) *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "delete",
|
||||
Aliases: []string{"rm"},
|
||||
Usage: "delete a data stream",
|
||||
UsageText: "delete [options] <name>",
|
||||
|
||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||
index := cmd.Args().Get(0)
|
||||
if index == "" {
|
||||
return errors.New("no data stream specified")
|
||||
}
|
||||
|
||||
return es.DatastreamDelete(conf, cmd.Args().Get(0))
|
||||
},
|
||||
|
||||
ShellComplete: func(ctx context.Context, cmd *cli.Command) {
|
||||
complete(cmd, Cdatastream)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func DatastreamRollover(conf *cfg.Config) *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "rollover",
|
||||
Aliases: []string{"roll"},
|
||||
Usage: "roll over a data stream",
|
||||
UsageText: "rollover <data stream>",
|
||||
|
||||
ShellComplete: func(ctx context.Context, cmd *cli.Command) {
|
||||
complete(cmd, Cindex)
|
||||
},
|
||||
|
||||
Flags: []cli.Flag{
|
||||
&cli.BoolFlag{
|
||||
Name: "wait",
|
||||
Usage: "wait for active shards",
|
||||
Destination: &conf.Wait,
|
||||
Aliases: []string{"w"},
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "dry-run",
|
||||
Usage: "checks conditions but does not perform a rollover",
|
||||
Destination: &conf.Wait,
|
||||
Aliases: []string{"n"},
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "max-age",
|
||||
Usage: "roll over after max age (eg: 7d, 2m, 8h)",
|
||||
Destination: &conf.MaxAge,
|
||||
},
|
||||
&cli.IntFlag{
|
||||
Name: "max-docs",
|
||||
Usage: "roll over after max docs",
|
||||
Destination: &conf.MaxDocs,
|
||||
},
|
||||
&cli.IntFlag{
|
||||
Name: "max-primary-shard-size",
|
||||
Usage: "roll over when primary shard size reaches size",
|
||||
Destination: &conf.MaxShardSize,
|
||||
},
|
||||
&cli.IntFlag{
|
||||
Name: "max-primary-shard-docs",
|
||||
Usage: "roll over after max docs in primary shard reached",
|
||||
Destination: &conf.MaxShardDocs,
|
||||
},
|
||||
},
|
||||
|
||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||
alias := cmd.Args().Get(0)
|
||||
|
||||
if alias == "" {
|
||||
return errors.New("no data stream specified")
|
||||
}
|
||||
|
||||
return es.DatastreamRollover(conf, alias)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func DatastreamIlm(conf *cfg.Config) *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "ilm",
|
||||
Usage: "show ilm status",
|
||||
UsageText: "ds ilm <name>",
|
||||
|
||||
ShellComplete: func(ctx context.Context, cmd *cli.Command) {
|
||||
complete(cmd, Cdatastream)
|
||||
},
|
||||
|
||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||
ds := cmd.Args().Get(0)
|
||||
if ds == "" {
|
||||
return errors.New("no ds name specified")
|
||||
}
|
||||
|
||||
return es.IlmExplain(conf, ds)
|
||||
},
|
||||
}
|
||||
}
|
||||
277
cmd/ilm.go
Normal file
277
cmd/ilm.go
Normal file
@@ -0,0 +1,277 @@
|
||||
/*
|
||||
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 Ilm(conf *cfg.Config) *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "ilm",
|
||||
Usage: "manage index lifecycle",
|
||||
|
||||
Commands: []*cli.Command{
|
||||
IlmRetry(conf),
|
||||
IlmStatus(conf),
|
||||
IlmList(conf),
|
||||
IlmShow(conf),
|
||||
IlmCreate(conf),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func IlmRetry(conf *cfg.Config) *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "retry",
|
||||
Usage: "retry applying an ILM profile to an index",
|
||||
UsageText: "retry <index>",
|
||||
|
||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||
return es.IlmRetry(conf, cmd.Args().Get(0))
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func IlmStatus(conf *cfg.Config) *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "status",
|
||||
Usage: "get the current index lifecycle management status",
|
||||
|
||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||
return es.IlmStatus(conf)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func IlmList(conf *cfg.Config) *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "list",
|
||||
Aliases: []string{"ls"},
|
||||
Usage: "list index lifecycle policies",
|
||||
UsageText: "list [<policy-pattern>]",
|
||||
|
||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||
return es.IlmList(conf, cmd.Args().Get(0))
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func IlmShow(conf *cfg.Config) *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "show",
|
||||
Aliases: []string{"sh"},
|
||||
Usage: "show details about an index lifecycle policy",
|
||||
UsageText: "show <policy>",
|
||||
|
||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||
policy := cmd.Args().Get(0)
|
||||
if policy == "" {
|
||||
return errors.New("no policy specified")
|
||||
}
|
||||
|
||||
return es.IlmShow(conf, cmd.Args().Get(0))
|
||||
},
|
||||
|
||||
ShellComplete: func(ctx context.Context, cmd *cli.Command) {
|
||||
complete(cmd, Cilm)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
*
|
||||
{
|
||||
"policy": {
|
||||
"phases": {
|
||||
"hot": {
|
||||
"min_age": "0ms",
|
||||
"actions": {
|
||||
"rollover": {
|
||||
"max_primary_shard_size": "50gb",
|
||||
"max_age": "1d",
|
||||
"max_docs": 100000000
|
||||
},
|
||||
"set_priority": {
|
||||
"priority": 100
|
||||
}
|
||||
}
|
||||
},
|
||||
"warm": {
|
||||
"min_age": "7d",
|
||||
"actions": {
|
||||
"migrate": {},
|
||||
"shrink": {
|
||||
"number_of_shards": 1
|
||||
},
|
||||
"forcemerge": {
|
||||
"max_num_segments": 1
|
||||
},
|
||||
"set_priority": {
|
||||
"priority": 50
|
||||
}
|
||||
}
|
||||
},
|
||||
"cold": {
|
||||
"min_age": "30d",
|
||||
"actions": {
|
||||
"migrate": {},
|
||||
"searchable_snapshot": {
|
||||
"snapshot_repository": "s3-logs-archive",
|
||||
"force_merge_index": true
|
||||
},
|
||||
"set_priority": {
|
||||
"priority": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"min_age": "90d",
|
||||
"actions": {
|
||||
"delete": {
|
||||
"delete_searchable_snapshot": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
*/
|
||||
|
||||
func IlmCreate(conf *cfg.Config) *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "create",
|
||||
Aliases: []string{"+"},
|
||||
Usage: "create a index lifecycle policy",
|
||||
UsageText: "create [options] <policy>",
|
||||
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "description",
|
||||
Usage: "policy description",
|
||||
Destination: &conf.Ilm.Description,
|
||||
Aliases: []string{"D"},
|
||||
},
|
||||
|
||||
// hot
|
||||
&cli.StringFlag{
|
||||
Name: "hot-min-age",
|
||||
Usage: "hot phase: min age",
|
||||
Destination: &conf.Ilm.HotMinAge,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "hot-rollover-max-age",
|
||||
Usage: "hot phase: rollover after",
|
||||
Destination: &conf.Ilm.HotRolloverMaxAge,
|
||||
},
|
||||
&cli.Int64Flag{
|
||||
Name: "hot-rollover-max-primary-shard-size",
|
||||
Usage: "hot phase: max primary shard size",
|
||||
Destination: &conf.Ilm.HotRolloverMaxPrimaryShardSize,
|
||||
},
|
||||
&cli.Int64Flag{
|
||||
Name: "hot-rollover-max-docs",
|
||||
Usage: "hot phase: max docs",
|
||||
Destination: &conf.Ilm.HotRolloverMaxDocs,
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "hot-readonly",
|
||||
Usage: "hot phase: make index ro before rollover",
|
||||
Destination: &conf.Ilm.HotReadonly,
|
||||
},
|
||||
|
||||
// warm
|
||||
&cli.StringFlag{
|
||||
Name: "warm-min-age",
|
||||
Usage: "warm phase: min age",
|
||||
Destination: &conf.Ilm.WarmMinAge,
|
||||
},
|
||||
&cli.IntFlag{
|
||||
Name: "warm-force-merge-segments",
|
||||
Usage: "warm phase: force merge segments per shard",
|
||||
Destination: &conf.Ilm.WarmForceMerge,
|
||||
},
|
||||
&cli.IntFlag{
|
||||
Name: "warm-shrink-shards",
|
||||
Usage: "warm phase: shrink on number of shards",
|
||||
Destination: &conf.Ilm.WarmShrinkShards,
|
||||
},
|
||||
&cli.IntFlag{
|
||||
Name: "warm-priority",
|
||||
Usage: "warm phase: priority",
|
||||
Destination: &conf.Ilm.WarmPriority,
|
||||
},
|
||||
|
||||
// cold
|
||||
&cli.StringFlag{
|
||||
Name: "cold-min-age",
|
||||
Usage: "cold phase: min age",
|
||||
Destination: &conf.Ilm.ColdMinAge,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "cold-searchable-snapshot-repo",
|
||||
Usage: "cold phase: searchable snapshot repo",
|
||||
Destination: &conf.Ilm.ColdSearchableSnapshotRepo,
|
||||
},
|
||||
&cli.IntFlag{
|
||||
Name: "cold-force-merge-segments",
|
||||
Usage: "cold phase: force merge segments per shard",
|
||||
Destination: &conf.Ilm.ColdForceMerge,
|
||||
},
|
||||
&cli.IntFlag{
|
||||
Name: "cold-priority",
|
||||
Usage: "cold phase: priority",
|
||||
Destination: &conf.Ilm.ColdPriority,
|
||||
},
|
||||
|
||||
// frozen
|
||||
&cli.StringFlag{
|
||||
Name: "frozen-min-age",
|
||||
Usage: "frozen phase: min age",
|
||||
Destination: &conf.Ilm.FrozenMinAge,
|
||||
},
|
||||
|
||||
// delete
|
||||
&cli.StringFlag{
|
||||
Name: "delete-min-age",
|
||||
Usage: "delete phase: min age",
|
||||
Destination: &conf.Ilm.DeleteMinAge,
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "delete-searchable-snapshots",
|
||||
Usage: "delete phase: delete searchable snapshots",
|
||||
Destination: &conf.Ilm.DeleteSearchableSnapshots,
|
||||
},
|
||||
},
|
||||
|
||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||
policy := cmd.Args().Get(0)
|
||||
if policy == "" {
|
||||
return errors.New("no policy name specified")
|
||||
}
|
||||
|
||||
return es.IlmCreate(conf, cmd.Args().Get(0))
|
||||
},
|
||||
}
|
||||
}
|
||||
39
cmd/index.go
39
cmd/index.go
@@ -41,8 +41,12 @@ func Index(conf *cfg.Config) *cli.Command {
|
||||
IndexClose(conf),
|
||||
IndexAllocation(conf),
|
||||
IndexModify(conf),
|
||||
IndexAlias(conf),
|
||||
IndexFields(conf),
|
||||
IndexIlm(conf),
|
||||
|
||||
// sub commands
|
||||
IndexAlias(conf),
|
||||
IndexTemplate(conf),
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -134,7 +138,7 @@ func IndexShow(conf *cfg.Config) *cli.Command {
|
||||
},
|
||||
|
||||
ShellComplete: func(ctx context.Context, cmd *cli.Command) {
|
||||
completeIndex(cmd)
|
||||
complete(cmd, Cindex)
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -165,6 +169,7 @@ Valid field mapping types: integer, text, date, keyword`,
|
||||
Usage: "number of replicas to create",
|
||||
Destination: &conf.Replicas,
|
||||
Aliases: []string{"r"},
|
||||
Value: 1,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -189,7 +194,7 @@ func IndexDelete(conf *cfg.Config) *cli.Command {
|
||||
UsageText: "delete <index>",
|
||||
|
||||
ShellComplete: func(ctx context.Context, cmd *cli.Command) {
|
||||
completeIndex(cmd)
|
||||
complete(cmd, Cindex)
|
||||
},
|
||||
|
||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||
@@ -210,7 +215,7 @@ func IndexClose(conf *cfg.Config) *cli.Command {
|
||||
UsageText: "close <index>",
|
||||
|
||||
ShellComplete: func(ctx context.Context, cmd *cli.Command) {
|
||||
completeIndex(cmd)
|
||||
complete(cmd, Cindex)
|
||||
},
|
||||
|
||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||
@@ -231,7 +236,7 @@ func IndexModify(conf *cfg.Config) *cli.Command {
|
||||
UsageText: "modify <index[,index,...]|_all>",
|
||||
|
||||
ShellComplete: func(ctx context.Context, cmd *cli.Command) {
|
||||
completeIndex(cmd)
|
||||
complete(cmd, Cindex)
|
||||
},
|
||||
|
||||
Flags: []cli.Flag{
|
||||
@@ -240,6 +245,7 @@ func IndexModify(conf *cfg.Config) *cli.Command {
|
||||
Usage: "number of replicas",
|
||||
Destination: &conf.Replicas,
|
||||
Aliases: []string{"r"},
|
||||
Value: 1,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -282,7 +288,7 @@ func IndexFields(conf *cfg.Config) *cli.Command {
|
||||
},
|
||||
|
||||
ShellComplete: func(ctx context.Context, cmd *cli.Command) {
|
||||
completeIndex(cmd)
|
||||
complete(cmd, Cindex)
|
||||
},
|
||||
|
||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||
@@ -295,3 +301,24 @@ func IndexFields(conf *cfg.Config) *cli.Command {
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func IndexIlm(conf *cfg.Config) *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "ilm",
|
||||
Usage: "show ilm status",
|
||||
UsageText: "index ilm <index>",
|
||||
|
||||
ShellComplete: func(ctx context.Context, cmd *cli.Command) {
|
||||
complete(cmd, Cindex)
|
||||
},
|
||||
|
||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||
index := cmd.Args().Get(0)
|
||||
if index == "" {
|
||||
return errors.New("no index specified")
|
||||
}
|
||||
|
||||
return es.IlmExplain(conf, index)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,8 +36,8 @@ func IndexAlias(conf *cfg.Config) *cli.Command {
|
||||
IndexAliasCreate(conf),
|
||||
IndexAliasList(conf),
|
||||
IndexAliasDelete(conf),
|
||||
// FIXME: implement IndexAliasShow + IndexAliasAdd
|
||||
//IndexAliasShow(conf),
|
||||
IndexAliasRollover(conf),
|
||||
// FIXME: implement IndexAliasAdd
|
||||
//IndexAliasAdd(conf), // see https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-indices-update-aliases
|
||||
},
|
||||
}
|
||||
@@ -51,7 +51,7 @@ func IndexAliasCreate(conf *cfg.Config) *cli.Command {
|
||||
UsageText: "create <index> <alias>",
|
||||
|
||||
ShellComplete: func(ctx context.Context, cmd *cli.Command) {
|
||||
completeIndex(cmd)
|
||||
complete(cmd, Cindex)
|
||||
},
|
||||
|
||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||
@@ -75,7 +75,7 @@ func IndexAliasDelete(conf *cfg.Config) *cli.Command {
|
||||
UsageText: "delete <index> <alias>",
|
||||
|
||||
ShellComplete: func(ctx context.Context, cmd *cli.Command) {
|
||||
completeIndex(cmd)
|
||||
complete(cmd, Cindex)
|
||||
},
|
||||
|
||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||
@@ -91,6 +91,64 @@ func IndexAliasDelete(conf *cfg.Config) *cli.Command {
|
||||
}
|
||||
}
|
||||
|
||||
func IndexAliasRollover(conf *cfg.Config) *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "rollover",
|
||||
Aliases: []string{"roll"},
|
||||
Usage: "roll over an index alias",
|
||||
UsageText: "rollover <alias>",
|
||||
|
||||
ShellComplete: func(ctx context.Context, cmd *cli.Command) {
|
||||
complete(cmd, Cindex)
|
||||
},
|
||||
|
||||
Flags: []cli.Flag{
|
||||
&cli.BoolFlag{
|
||||
Name: "wait",
|
||||
Usage: "wait for active shards",
|
||||
Destination: &conf.Wait,
|
||||
Aliases: []string{"w"},
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "dry-run",
|
||||
Usage: "checks conditions but does not perform a rollover",
|
||||
Destination: &conf.Wait,
|
||||
Aliases: []string{"n"},
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "max-age",
|
||||
Usage: "roll over after max age (eg: 7d, 2m, 8h)",
|
||||
Destination: &conf.MaxAge,
|
||||
},
|
||||
&cli.IntFlag{
|
||||
Name: "max-docs",
|
||||
Usage: "roll over after max docs",
|
||||
Destination: &conf.MaxDocs,
|
||||
},
|
||||
&cli.IntFlag{
|
||||
Name: "max-primary-shard-size",
|
||||
Usage: "roll over when primary shard size reaches size",
|
||||
Destination: &conf.MaxShardSize,
|
||||
},
|
||||
&cli.IntFlag{
|
||||
Name: "max-primary-shard-docs",
|
||||
Usage: "roll over after max docs in primary shard reached",
|
||||
Destination: &conf.MaxShardDocs,
|
||||
},
|
||||
},
|
||||
|
||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||
alias := cmd.Args().Get(0)
|
||||
|
||||
if alias == "" {
|
||||
return errors.New("no alias specified")
|
||||
}
|
||||
|
||||
return es.IndexAliasRollover(conf, alias)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func IndexAliasList(conf *cfg.Config) *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "list",
|
||||
|
||||
216
cmd/index_template.go
Normal file
216
cmd/index_template.go
Normal file
@@ -0,0 +1,216 @@
|
||||
/*
|
||||
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"
|
||||
"fmt"
|
||||
"slices"
|
||||
|
||||
"codeberg.org/scip/esctl/pkg/cfg"
|
||||
"codeberg.org/scip/esctl/pkg/es"
|
||||
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
func IndexTemplate(conf *cfg.Config) *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "template",
|
||||
Aliases: []string{"tpl"},
|
||||
Usage: "manage index templates",
|
||||
|
||||
Commands: []*cli.Command{
|
||||
IndexTemplateList(conf),
|
||||
IndexTemplateShow(conf),
|
||||
IndexTemplateCreate(conf, false),
|
||||
IndexTemplateCreate(conf, true),
|
||||
IndexTemplateDelete(conf),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func IndexTemplateList(conf *cfg.Config) *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "list",
|
||||
Aliases: []string{"ls"},
|
||||
Usage: "list index templates",
|
||||
|
||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||
return es.IndexTemplateList(conf)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func IndexTemplateShow(conf *cfg.Config) *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "show",
|
||||
Aliases: []string{"sh"},
|
||||
Usage: "show details about an index template",
|
||||
|
||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||
index := cmd.Args().Get(0)
|
||||
if index == "" {
|
||||
return errors.New("no index template specified")
|
||||
}
|
||||
|
||||
return es.IndexTemplateShow(conf, cmd.Args().Get(0))
|
||||
},
|
||||
|
||||
ShellComplete: func(ctx context.Context, cmd *cli.Command) {
|
||||
complete(cmd, Cindex)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func IndexTemplateCreate(conf *cfg.Config, modify bool) *cli.Command {
|
||||
name := "create"
|
||||
alias := "+"
|
||||
required := true
|
||||
|
||||
if modify {
|
||||
name = "modify"
|
||||
alias = "mod"
|
||||
required = false
|
||||
}
|
||||
|
||||
return &cli.Command{
|
||||
Name: name,
|
||||
Aliases: []string{alias},
|
||||
Usage: name + " a new index template",
|
||||
UsageText: name + ` <name> <fieldmapping:type>...`,
|
||||
CustomHelpTemplate: addReference(
|
||||
`Valid field mapping types: integer, text, date, keyword
|
||||
|
||||
Doc for index settings:
|
||||
https://www.elastic.co/docs/reference/elasticsearch/index-settings
|
||||
`),
|
||||
|
||||
Flags: []cli.Flag{
|
||||
&cli.BoolFlag{
|
||||
Name: "data-stream",
|
||||
Usage: "template creates a data stream",
|
||||
Destination: &conf.Stream,
|
||||
Aliases: []string{"S"},
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "allow-auto-create",
|
||||
Usage: "automatically create indices",
|
||||
Destination: &conf.AutoCreate,
|
||||
Aliases: []string{"a"},
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "rollover",
|
||||
Usage: "automatically rollover associated aliases",
|
||||
Destination: &conf.Rollover,
|
||||
Aliases: []string{"R"},
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "mode",
|
||||
Usage: "index mode, one of: standard, timeseries, logsdb or lookup",
|
||||
Destination: &conf.Mode,
|
||||
Aliases: []string{"m"},
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "retention",
|
||||
Usage: "data retention, e.g. -1 (infinitely), 30d, 3m or 1y (data stream only)",
|
||||
Destination: &conf.Retention,
|
||||
Aliases: []string{"r"},
|
||||
Required: required,
|
||||
},
|
||||
&cli.IntFlag{
|
||||
Name: "priority",
|
||||
Usage: "index template priority",
|
||||
Destination: &conf.Priority,
|
||||
Aliases: []string{"p"},
|
||||
},
|
||||
&cli.StringSliceFlag{
|
||||
Name: "index-pattern",
|
||||
Usage: "index matching pattern",
|
||||
Destination: &conf.Patterns,
|
||||
Aliases: []string{"i"},
|
||||
Required: required,
|
||||
},
|
||||
&cli.StringSliceFlag{
|
||||
Name: "settings",
|
||||
Usage: "individual template settings, format: name:value, multiple possible",
|
||||
Destination: &conf.Settings,
|
||||
Aliases: []string{"s"},
|
||||
},
|
||||
&cli.StringSliceFlag{
|
||||
Name: "component",
|
||||
Usage: "include component template, multiple possible",
|
||||
Destination: &conf.Components,
|
||||
Aliases: []string{"c"},
|
||||
},
|
||||
&cli.StringSliceFlag{
|
||||
Name: "meta",
|
||||
Usage: "custom meta field, format: name:value, multiple possible",
|
||||
Destination: &conf.Meta,
|
||||
Aliases: []string{"M"},
|
||||
},
|
||||
&cli.StringSliceFlag{
|
||||
Name: "aliases",
|
||||
Usage: "set up aliases to associate with indices",
|
||||
Destination: &conf.Aliases,
|
||||
Aliases: []string{"A"},
|
||||
},
|
||||
},
|
||||
|
||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||
args := cmd.Args()
|
||||
if args.Len() == 0 {
|
||||
return fmt.Errorf("no name specified")
|
||||
}
|
||||
|
||||
if conf.Mode != "" {
|
||||
if !slices.Contains([]string{"standard", "timeseries", "logsdb", "lookup"}, conf.Mode) {
|
||||
return errors.New("mode must be one of: standard, timeseries, logsdb or lookup")
|
||||
}
|
||||
}
|
||||
|
||||
mappings := args.Slice()[1:]
|
||||
|
||||
if modify {
|
||||
return es.IndexTemplateModify(conf, args.Get(0), mappings)
|
||||
}
|
||||
|
||||
return es.IndexTemplateCreate(conf, args.Get(0), mappings)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func IndexTemplateDelete(conf *cfg.Config) *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "delete",
|
||||
Aliases: []string{"rm"},
|
||||
Usage: "delete an index template",
|
||||
|
||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||
index := cmd.Args().Get(0)
|
||||
if index == "" {
|
||||
return errors.New("no index template specified")
|
||||
}
|
||||
|
||||
return es.IndexTemplateDelete(conf, cmd.Args().Get(0))
|
||||
},
|
||||
|
||||
ShellComplete: func(ctx context.Context, cmd *cli.Command) {
|
||||
complete(cmd, Cindex)
|
||||
},
|
||||
}
|
||||
}
|
||||
21
cmd/repl.go
21
cmd/repl.go
@@ -15,24 +15,3 @@ 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)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ func RoleShow(conf *cfg.Config) *cli.Command {
|
||||
UsageText: "show [options] <role>",
|
||||
|
||||
ShellComplete: func(ctx context.Context, cmd *cli.Command) {
|
||||
completeRole(cmd)
|
||||
complete(cmd, Crole)
|
||||
},
|
||||
|
||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||
|
||||
47
cmd/root.go
47
cmd/root.go
@@ -19,7 +19,9 @@ package cmd
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
golog "log"
|
||||
"os"
|
||||
"runtime/pprof"
|
||||
|
||||
"codeberg.org/scip/esctl/pkg/cfg"
|
||||
"codeberg.org/scip/esctl/pkg/es"
|
||||
@@ -83,6 +85,12 @@ func Main() int {
|
||||
Usage: "output mode (tsv, markdown, json, yaml) default: tsv",
|
||||
Destination: &conf.Output,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "profile-file",
|
||||
Usage: "golang profiler output file",
|
||||
Destination: &conf.ProfileFile,
|
||||
Hidden: true,
|
||||
},
|
||||
},
|
||||
|
||||
Commands: []*cli.Command{
|
||||
@@ -90,15 +98,18 @@ func Main() int {
|
||||
Cluster(conf),
|
||||
Ccr(conf),
|
||||
Index(conf),
|
||||
Ilm(conf),
|
||||
Datastream(conf),
|
||||
Shard(conf),
|
||||
Snapshot(conf),
|
||||
Node(conf),
|
||||
Doc(conf),
|
||||
Repl(conf),
|
||||
Version(conf),
|
||||
Debug(conf),
|
||||
Roles(conf),
|
||||
Task(conf),
|
||||
HelpJsonPath(conf),
|
||||
Api(conf),
|
||||
},
|
||||
|
||||
Before: func(ctx context.Context, cmd *cli.Command) (context.Context, error) {
|
||||
@@ -113,6 +124,26 @@ func Main() int {
|
||||
|
||||
log.Init(conf)
|
||||
|
||||
if conf.ProfileFile != "" {
|
||||
// enable cpu profiling. Do NOT use q to stop the game but
|
||||
// close the window to get a profile
|
||||
fd, err := os.Create(conf.ProfileFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() {
|
||||
if err := fd.Close(); err != nil {
|
||||
golog.Fatal(err)
|
||||
}
|
||||
}()
|
||||
|
||||
if err := pprof.StartCPUProfile(fd); err != nil {
|
||||
golog.Fatal(err)
|
||||
}
|
||||
|
||||
defer pprof.StopCPUProfile()
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
},
|
||||
}
|
||||
@@ -146,6 +177,20 @@ friends.#(first%"D*").last >> "Murphy"
|
||||
friends.#(first!%"D*").last >> "Craig"
|
||||
friends.#(nets.#(=="fb"))#.first >> ["Dale","Roger"]
|
||||
|
||||
To extract more than one field, use:
|
||||
|
||||
{"ns":source.namespace_name,"time":source.@timestamp} >>
|
||||
|
||||
{
|
||||
"ns": "3f80316965c64405-275a75782e984bcc8d82",
|
||||
"time": "2026-06-10T04:23:38.914080361+00:00"
|
||||
}
|
||||
{
|
||||
"ns": "3f80316965c64405-f0e1dcef668e45bb9afd",
|
||||
"time": "2026-06-10T09:39:00.554892542+00:00"
|
||||
}
|
||||
|
||||
|
||||
Documentation: https://github.com/tidwall/gjson/blob/master/SYNTAX.md`
|
||||
|
||||
return &cli.Command{
|
||||
|
||||
@@ -25,7 +25,7 @@ import (
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
const SearchUsage = `<sep> might be one of:
|
||||
const SearchUsage = `field <sep> might be one of:
|
||||
=: Must match
|
||||
!=: Must not match
|
||||
|
||||
@@ -43,15 +43,15 @@ For datetime range format refer to:
|
||||
https://www.elastic.co/docs/reference/elasticsearch/rest-apis/common-options#date-math
|
||||
|
||||
For timestamp formats refer to:
|
||||
https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/mapping-date-format
|
||||
`
|
||||
https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/mapping-date-format`
|
||||
|
||||
func Search(conf *cfg.Config) *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "search",
|
||||
Aliases: []string{"/"},
|
||||
Usage: "search within an index",
|
||||
UsageText: "search [options] [<[field<sep>]pattern> ...]\n" + SearchUsage,
|
||||
Name: "search",
|
||||
Aliases: []string{"/"},
|
||||
Usage: "search within an index",
|
||||
UsageText: "search [options] [<[field<sep>]pattern> ...]\n",
|
||||
CustomHelpTemplate: addReference(SearchUsage),
|
||||
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
|
||||
69
cmd/tasks.go
Normal file
69
cmd/tasks.go
Normal file
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
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 Task(conf *cfg.Config) *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "task",
|
||||
Usage: "manage tasks",
|
||||
|
||||
Commands: []*cli.Command{
|
||||
TaskList(conf),
|
||||
TaskCancel(conf),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TaskList(conf *cfg.Config) *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "list",
|
||||
Aliases: []string{"ls"},
|
||||
Usage: "list tasks",
|
||||
|
||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||
return es.TaskList(conf)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TaskCancel(conf *cfg.Config) *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "cancel",
|
||||
Aliases: []string{"rm"},
|
||||
Usage: "cancel running task",
|
||||
UsageText: "cancel <taskId>",
|
||||
|
||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||
taskId := cmd.Args().Get(0)
|
||||
if taskId == "" {
|
||||
return errors.New("no taskId specified")
|
||||
}
|
||||
|
||||
return es.TaskCancel(conf, cmd.Args().Get(0))
|
||||
},
|
||||
}
|
||||
}
|
||||
35
cmd/utilities.go
Normal file
35
cmd/utilities.go
Normal file
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
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 (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
func addReference(ref string) string {
|
||||
indentedRef := []string{}
|
||||
|
||||
for _, line := range strings.Split(ref, "\n") {
|
||||
indentedRef = append(indentedRef, " "+line)
|
||||
}
|
||||
return fmt.Sprintf("%s\nREFERENCE:\n%s\n",
|
||||
cli.SubcommandHelpTemplate,
|
||||
strings.Join(indentedRef, "\n"))
|
||||
}
|
||||
55
go.mod
55
go.mod
@@ -14,42 +14,83 @@
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
module codeberg.org/scip/esctl
|
||||
|
||||
go 1.25.0
|
||||
go 1.25.8
|
||||
|
||||
require (
|
||||
github.com/MichaelMure/go-term-markdown v0.1.4
|
||||
github.com/alecthomas/repr v0.5.2
|
||||
github.com/charmbracelet/lipgloss v1.1.0
|
||||
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/go-openapi/loads v0.24.0
|
||||
github.com/go-openapi/spec v0.22.5
|
||||
github.com/go-openapi/swag/loading v0.26.1
|
||||
github.com/mattn/go-isatty v0.0.22
|
||||
github.com/olekukonko/tablewriter v1.1.4
|
||||
github.com/tidwall/gjson v1.19.0
|
||||
github.com/tlinden/yadu v0.1.3
|
||||
github.com/urfave/cli/v3 v3.9.1-0.20260524212652-be8b79d0c8de
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/MichaelMure/go-term-text v0.3.1 // indirect
|
||||
github.com/alecthomas/chroma v0.7.1 // indirect
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/clipperhouse/displaywidth v0.10.0 // indirect
|
||||
github.com/clipperhouse/uax29/v2 v2.6.0 // indirect
|
||||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
|
||||
github.com/charmbracelet/x/ansi v0.8.0 // indirect
|
||||
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect
|
||||
github.com/charmbracelet/x/term v0.2.1 // indirect
|
||||
github.com/clipperhouse/displaywidth v0.11.0 // indirect
|
||||
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
|
||||
github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964 // indirect
|
||||
github.com/disintegration/imaging v1.6.2 // indirect
|
||||
github.com/dlclark/regexp2 v1.11.0 // indirect
|
||||
github.com/eliukblau/pixterm/pkg/ansimage v0.0.0-20191210081756-9fb6cf8c2f75 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-openapi/analysis v0.25.2 // indirect
|
||||
github.com/go-openapi/errors v0.22.8 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.23.1 // indirect
|
||||
github.com/go-openapi/jsonreference v0.21.6 // indirect
|
||||
github.com/go-openapi/strfmt v0.26.3 // indirect
|
||||
github.com/go-openapi/swag/conv v0.26.1 // indirect
|
||||
github.com/go-openapi/swag/jsonname v0.26.1 // indirect
|
||||
github.com/go-openapi/swag/jsonutils v0.26.1 // indirect
|
||||
github.com/go-openapi/swag/mangling v0.26.1 // indirect
|
||||
github.com/go-openapi/swag/stringutils v0.26.1 // indirect
|
||||
github.com/go-openapi/swag/typeutils v0.26.1 // indirect
|
||||
github.com/go-openapi/swag/yamlutils v0.26.1 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
|
||||
github.com/goccy/go-json v0.10.6 // indirect
|
||||
github.com/lmittmann/tint v1.1.3 // indirect
|
||||
github.com/gomarkdown/markdown v0.0.0-20191123064959-2c17d62f5098 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/kyokomi/emoji/v2 v2.2.8 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.4.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.23 // indirect
|
||||
github.com/muesli/termenv v0.16.0 // indirect
|
||||
github.com/oklog/ulid/v2 v2.1.1 // indirect
|
||||
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 // indirect
|
||||
github.com/olekukonko/errors v1.2.0 // indirect
|
||||
github.com/olekukonko/ll v0.1.8 // indirect
|
||||
github.com/tidwall/gjson v1.19.0 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.0 // indirect
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||
go.opentelemetry.io/otel v1.35.0 // indirect
|
||||
go.opentelemetry.io/otel/metric 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/sys v0.42.0 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect
|
||||
golang.org/x/image v0.0.0-20191206065243-da761ea9ff43 // indirect
|
||||
golang.org/x/net v0.55.0 // indirect
|
||||
golang.org/x/sys v0.46.0 // indirect
|
||||
golang.org/x/term v0.44.0 // indirect
|
||||
golang.org/x/text v0.37.0 // indirect
|
||||
)
|
||||
|
||||
192
go.sum
192
go.sum
@@ -1,29 +1,60 @@
|
||||
github.com/MichaelMure/go-term-markdown v0.1.4 h1:Ir3kBXDUtOX7dEv0EaQV8CNPpH+T7AfTh0eniMOtNcs=
|
||||
github.com/MichaelMure/go-term-markdown v0.1.4/go.mod h1:EhcA3+pKYnlUsxYKBJ5Sn1cTQmmBMjeNlpV8nRb+JxA=
|
||||
github.com/MichaelMure/go-term-text v0.3.1 h1:Kw9kZanyZWiCHOYu9v/8pWEgDQ6UVN9/ix2Vd2zzWf0=
|
||||
github.com/MichaelMure/go-term-text v0.3.1/go.mod h1:QgVjAEDUnRMlzpS6ky5CGblux7ebeiLnuy9dAaFZu8o=
|
||||
github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38 h1:smF2tmSOzy2Mm+0dGI2AIUHY+w0BUc+4tn40djz7+6U=
|
||||
github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38/go.mod h1:r7bzyVFMNntcxPZXK3/+KdruV1H5KSlyVY0gc+NgInI=
|
||||
github.com/alecthomas/chroma v0.7.1 h1:G1i02OhUbRi2nJxcNkwJaY/J1gHXj9tt72qN6ZouLFQ=
|
||||
github.com/alecthomas/chroma v0.7.1/go.mod h1:gHw09mkX1Qp80JlYbmN9L3+4R5o6DJJ3GRShh+AICNc=
|
||||
github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721 h1:JHZL0hZKJ1VENNfmXvHbgYlbUOvpzYzvy2aZU5gXVeo=
|
||||
github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721/go.mod h1:QO9JBoKquHd+jz9nshCh40fOfO+JzsoXy8qTHF68zU0=
|
||||
github.com/alecthomas/kong v0.2.1-0.20190708041108-0548c6b1afae/go.mod h1:+inYUSluD+p4L8KdviBSgzcqEjUQOfC5fQDRFuc36lI=
|
||||
github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897/go.mod h1:xTS7Pm1pD1mvyM075QCDSRqH6qRLXylzS24ZTpRiSzQ=
|
||||
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/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
||||
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/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
|
||||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
|
||||
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
|
||||
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
|
||||
github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE=
|
||||
github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q=
|
||||
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8=
|
||||
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
|
||||
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
|
||||
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
|
||||
github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM=
|
||||
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 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04=
|
||||
github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=
|
||||
github.com/clipperhouse/displaywidth v0.10.0 h1:GhBG8WuerxjFQQYeuZAeVTuyxuX+UraiZGD4HJQ3Y8g=
|
||||
github.com/clipperhouse/displaywidth v0.10.0/go.mod h1:XqJajYsaiEwkxOj4bowCTMcT1SgvHo9flfF3jQasdbs=
|
||||
github.com/clipperhouse/uax29/v2 v2.6.0 h1:z0cDbUV+aPASdFb2/ndFnS9ts/WNXgTNNGFoKXuhpos=
|
||||
github.com/clipperhouse/uax29/v2 v2.6.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
|
||||
github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8=
|
||||
github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0=
|
||||
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
|
||||
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
|
||||
github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964 h1:y5HC9v93H5EPKqaS1UYVg1uYah5Xf51mBfIoWehClUQ=
|
||||
github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964/go.mod h1:Xd9hchkHSWYkEqJwUGisez3G1QY8Ryz0sdWrLPMGjLk=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
|
||||
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
|
||||
github.com/dlclark/regexp2 v1.1.6/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
|
||||
github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
|
||||
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/elastic/elastic-transport-go/v8 v8.9.0 h1:KeT/2P54F0xS0S8Y3Pf+tFDg4HmBgReQMB+BMz8dDAs=
|
||||
github.com/elastic/elastic-transport-go/v8 v8.9.0/go.mod h1:ssMTvNS2hwf7CaiGsRRsx4gQHFZ/jS/DkLcISxekWzc=
|
||||
github.com/elastic/elastic-transport-go/v8 v8.11.0 h1:taYmqC2M6+fZt/+W+ENYh/W5L9+KrlJGOSbEJs8egWc=
|
||||
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/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/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
|
||||
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
||||
github.com/eliukblau/pixterm/pkg/ansimage v0.0.0-20191210081756-9fb6cf8c2f75 h1:vbix8DDQ/rfatfFr/8cf/sJfIL69i4BcZfjrVOxsMqk=
|
||||
github.com/eliukblau/pixterm/pkg/ansimage v0.0.0-20191210081756-9fb6cf8c2f75/go.mod h1:0gZuvTO1ikSA5LtTI6E13LEOdWQNjIo5MTQOvrV0eFg=
|
||||
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
|
||||
github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w=
|
||||
github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
@@ -31,33 +62,103 @@ 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/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-openapi/analysis v0.25.2 h1:I0vy4n3alz+DHTiN1PRhCb7QZxkK6g5YmswZKv2TKuw=
|
||||
github.com/go-openapi/analysis v0.25.2/go.mod h1:Uhs1t/2XR10EnwONYILGEzw8gcfGIG5Xk5K2AxnhqDo=
|
||||
github.com/go-openapi/errors v0.22.8 h1:oP7sW7TWc3wFFjrzzj0nI83H2qMBkNjNfSd+XRejk/I=
|
||||
github.com/go-openapi/errors v0.22.8/go.mod h1:BuUoHcYrU6E7V9gfj1I5wLQqgtIHnup/alXZ8KdgQ0w=
|
||||
github.com/go-openapi/jsonpointer v0.23.1 h1:1HBACs7XIwR2RcmItfdSFlALhGbe6S92p0ry4d1GWg4=
|
||||
github.com/go-openapi/jsonpointer v0.23.1/go.mod h1:iWRmZTrGn7XwYhtPt/fvdSFj1OfNBngqRT2UG3BxSqY=
|
||||
github.com/go-openapi/jsonreference v0.21.6 h1:NZ5nGfnaM1n4I43Xjm1e5/M2GjOwQwndQz22uhxwD+Y=
|
||||
github.com/go-openapi/jsonreference v0.21.6/go.mod h1:xzbgtQ3ZbWxvET3AxdzCJlJt6vkovbf+IfSPJjD0tUY=
|
||||
github.com/go-openapi/loads v0.24.0 h1:4LLorXRPTzIN9V6ngMUZbAscsBOUBk3Oa8cClu/bFrQ=
|
||||
github.com/go-openapi/loads v0.24.0/go.mod h1:xQMgX+hw5xRAhGrcDXxeMw78IFqUpIzhleu3HqPhyF4=
|
||||
github.com/go-openapi/spec v0.22.5 h1:KhO7RBlKQfonUWX2WzQCoLIXVA6AcNqDGZ3a1Dutdlo=
|
||||
github.com/go-openapi/spec v0.22.5/go.mod h1:vxpOtMya5TXtENXKE5bKqv5NjocVhyhxHrlZfvKnZ74=
|
||||
github.com/go-openapi/strfmt v0.26.3 h1:rzmslHarJgBbf2qfGge+X3htclQfmXqBZMm0Too0HhU=
|
||||
github.com/go-openapi/strfmt v0.26.3/go.mod h1:a5nsUw0oRpQzZeOwx8bi6cKbzFZslpbCKt1LEot+KnQ=
|
||||
github.com/go-openapi/swag/conv v0.26.1 h1:slr5FVkg9Wc3Y5zcwenD8Sd/PQ94b2I/QJI7N7KTBpg=
|
||||
github.com/go-openapi/swag/conv v0.26.1/go.mod h1:mvQXgPptZk9GTrFgGwWvT4q+dN+zQej9JfmGwnipz1A=
|
||||
github.com/go-openapi/swag/jsonname v0.26.1 h1:VReupaV6WxlAsCn0e4DUfgV6bPmINnPpyJDLqSfNPcE=
|
||||
github.com/go-openapi/swag/jsonname v0.26.1/go.mod h1:OvdW6BoWoj33pTfi7x9vFrgmT+fk7aw0BRwvCE0YOuc=
|
||||
github.com/go-openapi/swag/jsonutils v0.26.1 h1:2hdBfFkHg+7Wrz2VsCbeyR6hzkRDs7AztnMR2u84yOY=
|
||||
github.com/go-openapi/swag/jsonutils v0.26.1/go.mod h1:U+RMJH3wa+6BRiphuRtIyI8fW9HPFqFQ4sHk2oRx0UQ=
|
||||
github.com/go-openapi/swag/jsonutils/fixtures_test v0.26.1 h1:1CD7NiLLb/TXl3tOnFYU4b+mNfb5rtgHkaA+q7RMYYQ=
|
||||
github.com/go-openapi/swag/jsonutils/fixtures_test v0.26.1/go.mod h1:ZWafc8nMdYzTE3uYY6W86f0n46+IF0g4uUyRhJw/kXc=
|
||||
github.com/go-openapi/swag/loading v0.26.1 h1:E9K4wqXeROlhjFQ13K9zMz6ojFGXIggGe+ad1odrK9w=
|
||||
github.com/go-openapi/swag/loading v0.26.1/go.mod h1:3qvRIlWzWdq1HvmldwmuJ2ohpcAryN6xVt2OTKd0/7E=
|
||||
github.com/go-openapi/swag/mangling v0.26.1 h1:gpYI4WuPKFJJVjV5cDLGlDVJhFIxYjQc7yN5eEb4CqM=
|
||||
github.com/go-openapi/swag/mangling v0.26.1/go.mod h1:POETDH01hqAdASXfw7ISEd9bCOE6xBHOt8NHmGZRmYM=
|
||||
github.com/go-openapi/swag/stringutils v0.26.1 h1:f88uYyTso7TnHrKM/bUBsQ5e2wKf37cpgo6pvbzd9yU=
|
||||
github.com/go-openapi/swag/stringutils v0.26.1/go.mod h1:Sc6d3bU8fgk5AyZR8/8jEQ+Is/Ald+TD/IIggPN8UJk=
|
||||
github.com/go-openapi/swag/typeutils v0.26.1 h1:yg42FgMzRR6PVQ3M3qHz1s+Y6/P4HoJ3cBarXa3OVnU=
|
||||
github.com/go-openapi/swag/typeutils v0.26.1/go.mod h1:VfnV+oUtSP2vCSCn2aJgnr8OevUYemyIzzS1VOzS10o=
|
||||
github.com/go-openapi/swag/yamlutils v0.26.1 h1:0TSLK+lXs9vfIhAWzBeI/lOzEnIoot6WTCO1aAeWFTk=
|
||||
github.com/go-openapi/swag/yamlutils v0.26.1/go.mod h1:7W5b7PRX9MxwL7TjeG7H8HkyBGRsIDRObhyMWFgBI2M=
|
||||
github.com/go-openapi/testify/enable/yaml/v2 v2.5.1 h1:q9NtHwK4qHF7yZziBPvZyv7zWAIk8ok88Gh2mR6Jpc8=
|
||||
github.com/go-openapi/testify/enable/yaml/v2 v2.5.1/go.mod h1:JW0MXIotCYps/XsgJnG3a8Q7rE5xAiBwoOD5OfaIQBk=
|
||||
github.com/go-openapi/testify/v2 v2.5.1 h1:TMdhCaw8fUNraVSf3Omoob1dO/AzBfhtFAPW0an6sBo=
|
||||
github.com/go-openapi/testify/v2 v2.5.1/go.mod h1:SgsVHtfooshd0tublTtJ50FPKhujf47YRqauXXOUxfw=
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
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/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/gomarkdown/markdown v0.0.0-20191123064959-2c17d62f5098 h1:Qxs3bNRWe8GTcKMxYOSXm0jx6j0de8XUtb/fsP3GZ0I=
|
||||
github.com/gomarkdown/markdown v0.0.0-20191123064959-2c17d62f5098/go.mod h1:aii0r/K0ZnHv7G0KF7xy1v0A7s2Ljrb5byB7MO5p6TU=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/kyokomi/emoji/v2 v2.2.8 h1:jcofPxjHWEkJtkIbcLHvZhxKgCPl6C7MyjTrD4KDqUE=
|
||||
github.com/kyokomi/emoji/v2 v2.2.8/go.mod h1:JUcn42DTdsXJo1SWanHh4HKDEyPaR5CqkmoirZZP9qE=
|
||||
github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/lucasb-eyer/go-colorful v1.4.0 h1:UtrWVfLdarDgc44HcS7pYloGHJUjHV/4FwW4TvVgFr4=
|
||||
github.com/lucasb-eyer/go-colorful v1.4.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
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/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
|
||||
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
|
||||
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/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
||||
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
||||
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/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
|
||||
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
|
||||
github.com/oklog/ulid/v2 v2.1.1 h1:suPZ4ARWLOJLegGFiZZ1dFAkqzhMjL3J1TzI+5wHz8s=
|
||||
github.com/oklog/ulid/v2 v2.1.1/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ=
|
||||
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/errors v1.2.0 h1:10Zcn4GeV59t/EGqJc8fUjtFT/FuUh5bTMzZ1XwmCRo=
|
||||
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/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/go.mod h1:+kedxuyTtgoZLwif3P1Em4hARJs+mVnzKxmsCL/C5RY=
|
||||
github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
||||
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
|
||||
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/tidwall/gjson v1.19.0 h1:xwxm7n691Uf3u5OFjzngavjGTh55KX5q/9w9xHW88JU=
|
||||
github.com/tidwall/gjson v1.19.0/go.mod h1:V37/opeE/JbLUOfH0QTXiNez2l0RUjYUhpT4szFQAfc=
|
||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||
@@ -66,30 +167,49 @@ github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
|
||||
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tlinden/yadu v0.1.3 h1:5cRCUmj+l5yvlM2irtpFBIJwVV2DPEgYSaWvF19FtcY=
|
||||
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/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=
|
||||
github.com/urfave/cli/v3 v3.9.1-0.20260524212652-be8b79d0c8de h1:ESKPiS7inVoBnv4FmgGNZdjWBI/wmvaragoyD3D9nM4=
|
||||
github.com/urfave/cli/v3 v3.9.1-0.20260524212652-be8b79d0c8de/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||
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/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
|
||||
go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
|
||||
go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
|
||||
go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
|
||||
go.opentelemetry.io/otel/sdk v1.29.0 h1:vkqKjk7gwhS8VaWb0POZKmIEDimRCMsopNYnriHyryo=
|
||||
go.opentelemetry.io/otel/sdk v1.29.0/go.mod h1:pM8Dx5WKnvxLCb+8lG1PRNIDxu9g9b9g59Qr7hfAAok=
|
||||
go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
|
||||
go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
|
||||
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM=
|
||||
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80=
|
||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
golang.org/dl v0.0.0-20190829154251-82a15e2f2ead/go.mod h1:IUMfjQLJQd4UTqG1Z90tenwKoCX93Gn3MAQJMOSBsDQ=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
|
||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
|
||||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.0.0-20191206065243-da761ea9ff43 h1:gQ6GUSD102fPgli+Yb4cR/cGaHF7tNBt+GYoRCpGC7s=
|
||||
golang.org/x/image v0.0.0-20191206065243-da761ea9ff43/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
|
||||
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
|
||||
golang.org/x/sys v0.0.0-20181128092732-4ed8d59d0b35/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
|
||||
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
|
||||
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc=
|
||||
golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
|
||||
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
@@ -34,7 +34,7 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
Version string = `v0.0.18`
|
||||
Version string = `v0.0.19`
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -48,41 +48,70 @@ type Cluster struct {
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
ConfigFile string // -c
|
||||
CurrentCluster string // -C
|
||||
Debug bool // -d
|
||||
Output string // -o <mode>
|
||||
Clusters map[string]*Cluster
|
||||
DefaultCluster *Cluster
|
||||
Index string // index: -i
|
||||
Failed, Partials bool // index: flags
|
||||
Shards, Replicas int // index create+allocation: -s -r
|
||||
Wait bool // index create: -w
|
||||
Primary bool // index allocation: -p
|
||||
Searchable bool // index fields: -s
|
||||
Aggretable bool // index fields: -a
|
||||
From, To, MaxItems int // search: flags
|
||||
Filter []string // search: -F
|
||||
Path string // search+doc sh: -p
|
||||
Subhelp bool // search+doc sh: -H
|
||||
Tail bool // search: -f [tail]
|
||||
Or bool // search: -O
|
||||
Range string // search: -r
|
||||
TimestampFormat string // search: --timestamp-format
|
||||
Explain bool // search: -e
|
||||
Validate bool // search: --validate
|
||||
SortBy string // sort: -k
|
||||
Ascending bool // sort: -a
|
||||
Exclude string // cluster compare: -e (regexp)
|
||||
All, Verbose bool // cluster status: -a -v
|
||||
Persistent, Transient, Default bool // -p -t -D cluster settings set
|
||||
Force bool // ccr follower renew: -f
|
||||
HaveJQ bool // determined at runtime by ourselfes
|
||||
DebugHTTP bool // root: --debug-http
|
||||
Separator string // role diff: -s
|
||||
NotDeployed bool // role diff: -n
|
||||
Undefined bool // role diff: -u
|
||||
Diff bool // role diff: -D
|
||||
ConfigFile string // -c
|
||||
CurrentCluster string // -C
|
||||
Debug bool // -d
|
||||
Output string // -o <mode>
|
||||
Clusters map[string]*Cluster
|
||||
DefaultCluster *Cluster
|
||||
HaveJQ bool // determined at runtime by ourselfes
|
||||
ProfileFile string // for internal use (golang profiling)
|
||||
|
||||
Index string // index: -i
|
||||
Failed, Partials bool // index: flags
|
||||
Shards, Replicas int // index create+allocation: -s -r
|
||||
Wait bool // index create: -w
|
||||
Primary bool // index allocation: -p
|
||||
Searchable bool // index fields: -s
|
||||
Aggretable bool // index fields: -a
|
||||
|
||||
Priority int // index template create: -p
|
||||
Settings []string // index template create: -s
|
||||
Patterns []string // index template create: -i
|
||||
Components []string // index template create: -c
|
||||
Meta []string // index template create: -M
|
||||
Aliases []string // index template create: -A
|
||||
Stream bool // index template create: -S
|
||||
AutoCreate bool // index template create: -a
|
||||
Mode string // index template create: -a
|
||||
Retention string // index template create: -r
|
||||
Rollover bool // index template create: -R
|
||||
|
||||
From, To, MaxItems int // search: flags
|
||||
Filter []string // search: -F
|
||||
Path string // search+doc sh: -p
|
||||
Subhelp bool // search+doc sh: -H
|
||||
Tail bool // search: -f [tail]
|
||||
Or bool // search: -O
|
||||
Range string // search: -r
|
||||
TimestampFormat string // search: --timestamp-format
|
||||
Explain bool // search: -e
|
||||
Validate bool // search: --validate
|
||||
|
||||
SortBy string // sort: -k
|
||||
Ascending bool // sort: -a
|
||||
|
||||
Exclude string // cluster compare: -e (regexp)
|
||||
All, Verbose bool // cluster status: -a -v
|
||||
Persistent, Transient, Default bool // cluster settings set: -p -t -D
|
||||
|
||||
Force bool // ccr follower renew: -f
|
||||
|
||||
DebugHTTP bool // root: --debug-http
|
||||
Separator string // role diff: -s
|
||||
NotDeployed bool // role diff: -n
|
||||
Undefined bool // role diff: -u
|
||||
Diff bool // role diff: -D
|
||||
Hidden bool // ds ls: -H
|
||||
|
||||
// rollover
|
||||
MaxAge string
|
||||
MaxDocs, MaxShardSize, MaxShardDocs int // roll over
|
||||
DryRun bool // rollover: -n
|
||||
|
||||
Tag string // api ls: -t
|
||||
|
||||
Ilm Ilm // ilm create
|
||||
}
|
||||
|
||||
func NewConfig() *Config {
|
||||
|
||||
72
pkg/cfg/ilm.go
Normal file
72
pkg/cfg/ilm.go
Normal file
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
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
|
||||
|
||||
type Ilm struct {
|
||||
Description string
|
||||
|
||||
HotMinAge string
|
||||
HotRolloverMaxAge string
|
||||
HotRolloverMaxPrimaryShardSize int64
|
||||
HotRolloverMaxDocs int64
|
||||
HotReadonly bool
|
||||
|
||||
WarmMinAge string
|
||||
WarmForceMerge int
|
||||
WarmShrinkShards int
|
||||
WarmPriority int
|
||||
|
||||
ColdMinAge string
|
||||
ColdSearchableSnapshotRepo string
|
||||
ColdForceMerge int
|
||||
ColdPriority int
|
||||
|
||||
FrozenMinAge string
|
||||
|
||||
DeleteMinAge string
|
||||
DeleteSearchableSnapshots bool
|
||||
}
|
||||
|
||||
func (cfg *Ilm) HaveHot() bool {
|
||||
return cfg.HotMinAge != "" ||
|
||||
cfg.HotRolloverMaxAge != "" ||
|
||||
cfg.HotRolloverMaxDocs > 0 ||
|
||||
cfg.HotRolloverMaxPrimaryShardSize > 0 ||
|
||||
cfg.HotReadonly
|
||||
}
|
||||
|
||||
func (cfg *Ilm) HaveWarm() bool {
|
||||
return cfg.WarmMinAge != "" ||
|
||||
cfg.WarmForceMerge > 0 ||
|
||||
cfg.WarmShrinkShards > 0 ||
|
||||
cfg.WarmPriority > 0
|
||||
}
|
||||
|
||||
func (cfg *Ilm) HaveCold() bool {
|
||||
return cfg.ColdMinAge != "" ||
|
||||
cfg.ColdSearchableSnapshotRepo != "" ||
|
||||
cfg.ColdForceMerge > 0 ||
|
||||
cfg.ColdPriority > 0
|
||||
}
|
||||
|
||||
func (cfg *Ilm) HaveFrozen() bool {
|
||||
return cfg.FrozenMinAge != ""
|
||||
}
|
||||
|
||||
func (cfg *Ilm) HaveDelete() bool {
|
||||
return cfg.DeleteSearchableSnapshots || cfg.DeleteMinAge != ""
|
||||
}
|
||||
526
pkg/es/api.go
Normal file
526
pkg/es/api.go
Normal file
@@ -0,0 +1,526 @@
|
||||
/*
|
||||
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 (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"codeberg.org/scip/esctl/assets"
|
||||
"codeberg.org/scip/esctl/pkg/cfg"
|
||||
"codeberg.org/scip/esctl/pkg/printer"
|
||||
markdown "github.com/MichaelMure/go-term-markdown"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/chzyer/readline"
|
||||
"github.com/go-openapi/spec"
|
||||
"golang.org/x/term"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultMargin = 4
|
||||
intro = `Input format: verb path [data]"
|
||||
|
||||
Example:
|
||||
|
||||
post /yourindex/_ccr/pause_follow
|
||||
put /yourindex/_settings {"number_of_replicas": 1}
|
||||
|
||||
You can also put multiline JSON after the path like:
|
||||
|
||||
put /yourindex/_settings
|
||||
{
|
||||
"number_of_replicas": 1
|
||||
}
|
||||
|
||||
If you do NOT supply a JSON in the first line, you need to hit ENTER
|
||||
twice to complete.`
|
||||
)
|
||||
|
||||
// holds an API operation via go-openapi/spec
|
||||
type Op struct {
|
||||
Verb string
|
||||
Op *spec.Operation
|
||||
}
|
||||
|
||||
type Param struct {
|
||||
Description, Param string
|
||||
}
|
||||
|
||||
// API operation parameter type
|
||||
type Params struct {
|
||||
Path []Param
|
||||
Query []Param
|
||||
}
|
||||
|
||||
func ApiRepl(conf *cfg.Config) error {
|
||||
verbs := []string{"post", "get", "put", "delete"}
|
||||
|
||||
fmt.Println(intro)
|
||||
fmt.Println()
|
||||
|
||||
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], "/") {
|
||||
parts[1] = "/" + parts[1]
|
||||
}
|
||||
|
||||
json := ""
|
||||
if len(parts) == 3 {
|
||||
// put /uri {json}
|
||||
json = parts[2]
|
||||
}
|
||||
|
||||
// put /uri/<Ret> [json]
|
||||
data, err := readJSON(json)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
|
||||
raw, err := CallAPI(conf, parts[0], parts[1], data)
|
||||
if err != nil {
|
||||
fmt.Printf("failed to call API: %s\n", esErrorString(err))
|
||||
}
|
||||
|
||||
if err := prettyfiJson(conf, raw); err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func encodeAuth(username, password string) string {
|
||||
return base64.StdEncoding.EncodeToString([]byte(username + ":" + password))
|
||||
}
|
||||
|
||||
func CallAPI(conf *cfg.Config, verb, path, data string) ([]byte, error) {
|
||||
verb = strings.ToUpper(verb)
|
||||
|
||||
// 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 nil, 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 nil, err
|
||||
}
|
||||
|
||||
// Read and print response
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read response body: %s", err)
|
||||
}
|
||||
|
||||
return body, nil
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// interactively read arbitrary JSON data from STDIN, which is
|
||||
// virtually a repl inside the primary repl
|
||||
func readJSON(input string) (string, error) {
|
||||
data := ""
|
||||
|
||||
if input != "" {
|
||||
data = input
|
||||
} else {
|
||||
scanner := bufio.NewScanner(os.Stdin)
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
|
||||
if line == "" {
|
||||
break
|
||||
}
|
||||
|
||||
data += line
|
||||
}
|
||||
}
|
||||
|
||||
if data == "" {
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// validate
|
||||
check := map[string]any{}
|
||||
err := json.Unmarshal([]byte(data), &check)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error: input data is not proper JSON: %s", err)
|
||||
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func ApiList(conf *cfg.Config, pattern string) error {
|
||||
assets.LoadAssetOpenApi()
|
||||
|
||||
filter := regexp.MustCompile(pattern)
|
||||
|
||||
table := printer.NewTable(conf, 4, 0)
|
||||
table.Addheaders("path", "http verb", "tag", "description")
|
||||
|
||||
for path, item := range assets.OpenAPI.Spec().Paths.Paths {
|
||||
if pattern != "" {
|
||||
if !filter.MatchString(path) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
ops := findOperation(item.PathItemProps)
|
||||
|
||||
for _, op := range ops {
|
||||
tags := findTags(op)
|
||||
|
||||
if conf.Tag != "" {
|
||||
if !slices.Contains(tags, conf.Tag) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
table.AddRow(
|
||||
path,
|
||||
op.Verb,
|
||||
strings.Join(tags, ","),
|
||||
strings.TrimSpace(op.Op.Summary),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
table.Sort()
|
||||
return table.Print()
|
||||
}
|
||||
|
||||
// for completion
|
||||
func ApiPathNames() []string {
|
||||
assets.LoadAssetOpenApi()
|
||||
|
||||
paths := []string{}
|
||||
|
||||
for path := range assets.OpenAPI.Spec().Paths.Paths {
|
||||
paths = append(paths, path)
|
||||
}
|
||||
|
||||
return paths
|
||||
}
|
||||
|
||||
func ApiShow(conf *cfg.Config, showpath, verb string) error {
|
||||
assets.LoadAssetOpenApi()
|
||||
|
||||
op, err := matchOperation(showpath, verb)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
slog.Debug("api operation", "op", op)
|
||||
|
||||
cleanMarkup := regexp.MustCompile(`<[^<>]+>`)
|
||||
|
||||
width := getTermWidth()
|
||||
params := getApiParameters(op, showpath, width)
|
||||
sample := getApiExample(op)
|
||||
description := markdown.Render(cleanMarkup.ReplaceAllString(op.Op.Description, ""), width, DefaultMargin)
|
||||
|
||||
var bold = lipgloss.NewStyle().
|
||||
Bold(true)
|
||||
var paragraph = lipgloss.NewStyle().
|
||||
MarginBottom(1).
|
||||
MarginLeft(DefaultMargin)
|
||||
var boldparagraph = lipgloss.NewStyle().
|
||||
MarginBottom(1).
|
||||
MarginLeft(DefaultMargin).
|
||||
Bold(true)
|
||||
var indentparagraph = lipgloss.NewStyle().
|
||||
MarginBottom(1).
|
||||
MarginLeft(2)
|
||||
|
||||
fmt.Println(bold.Render("ID: " + op.Op.ID))
|
||||
fmt.Println(paragraph.Render(fmt.Sprintf("%s %s", op.Verb, showpath)))
|
||||
|
||||
fmt.Println(bold.Render("Tags"))
|
||||
fmt.Println(paragraph.Render(strings.Join(op.Op.Tags, ", ")))
|
||||
|
||||
fmt.Println(bold.Render("Summary"))
|
||||
fmt.Println(paragraph.Render(strings.TrimSpace(op.Op.Summary)))
|
||||
|
||||
fmt.Println(bold.Render("Description"))
|
||||
fmt.Println(string(description)) // already indented
|
||||
|
||||
if len(params.Path) > 0 {
|
||||
fmt.Println(bold.Render("Path Parameters"))
|
||||
for _, param := range params.Path {
|
||||
fmt.Println(boldparagraph.Render(param.Param))
|
||||
if param.Description != "" {
|
||||
fmt.Println(indentparagraph.Render(param.Description))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(params.Query) > 0 {
|
||||
fmt.Println(bold.Render("Query Parameters"))
|
||||
for _, param := range params.Query {
|
||||
fmt.Println(boldparagraph.Render(param.Param))
|
||||
if param.Description != "" {
|
||||
fmt.Println(indentparagraph.Render(param.Description))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if sample != "" {
|
||||
fmt.Println(bold.Render("Example"))
|
||||
fmt.Println(paragraph.Render(sample))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// find all tags associated with op
|
||||
func findTags(op *Op) []string {
|
||||
return op.Op.Tags
|
||||
}
|
||||
|
||||
// Get API parameters
|
||||
func getApiParameters(op *Op, path string, width int) Params {
|
||||
params := Params{}
|
||||
pathParams := []string{}
|
||||
pathParamsReg := regexp.MustCompile(`{([a-z_]+)}`)
|
||||
|
||||
// find params in url path
|
||||
for _, match := range pathParamsReg.FindAllStringSubmatch(path, -1) {
|
||||
if len(match) == 2 {
|
||||
pathParams = append(pathParams, match[1])
|
||||
}
|
||||
}
|
||||
|
||||
empty := spec.ParamProps{}
|
||||
for _, param := range op.Op.Parameters {
|
||||
if param.ParamProps == empty {
|
||||
for _, token := range param.Refable.Ref.Ref.GetPointer().DecodedTokens() {
|
||||
parts := strings.Split(token, "-")
|
||||
if len(parts) == 2 {
|
||||
param := parts[1]
|
||||
if slices.Contains(pathParams, param) {
|
||||
params.Path = append(params.Path, Param{Param: param})
|
||||
} else {
|
||||
params.Query = append(params.Query, Param{Param: param})
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
par := Param{
|
||||
Description: string(markdown.Render(param.Description, width, DefaultMargin)),
|
||||
Param: param.Name,
|
||||
}
|
||||
|
||||
if param.In == "query" {
|
||||
params.Query = append(params.Query, par)
|
||||
} else {
|
||||
params.Path = append(params.Path, par)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return params
|
||||
}
|
||||
|
||||
// Get console example
|
||||
func getApiExample(op *Op) string {
|
||||
sample := ""
|
||||
|
||||
samples, sampleexist := op.Op.Extensions["x-codeSamples"]
|
||||
if sampleexist {
|
||||
for _, itemany := range samples.([]any) {
|
||||
item := itemany.(map[string]any)
|
||||
|
||||
lang, haslang := item["lang"]
|
||||
if haslang {
|
||||
if lang == "Console" {
|
||||
source, hassource := item["source"]
|
||||
if hassource {
|
||||
sample = strings.TrimSpace(source.(string))
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return sample
|
||||
}
|
||||
|
||||
// Find an API operation which matches given path and verb.
|
||||
// If no verb is given and only 1 op exists, return this one,
|
||||
// otherwise showpath+verb have to match precisely.
|
||||
func matchOperation(showpath, verb string) (*Op, error) {
|
||||
ops := []*Op{}
|
||||
op := &Op{}
|
||||
var found bool
|
||||
|
||||
for path, item := range assets.OpenAPI.Spec().Paths.Paths {
|
||||
if path == showpath {
|
||||
ops = findOperation(item.PathItemProps)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if len(ops) == 0 {
|
||||
return nil, errors.New("no matching API call found")
|
||||
}
|
||||
|
||||
if len(ops) == 1 && verb == "" {
|
||||
return ops[0], nil
|
||||
}
|
||||
|
||||
for _, item := range ops {
|
||||
if strings.ToLower(item.Verb) == verb {
|
||||
op = item
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
return nil, errors.New("no matching API call found for given path+verb")
|
||||
}
|
||||
|
||||
return op, nil
|
||||
}
|
||||
|
||||
func findOperation(item spec.PathItemProps) []*Op {
|
||||
ops := []*Op{}
|
||||
|
||||
if item.Post != nil {
|
||||
ops = append(ops, &Op{"POST", item.Post})
|
||||
}
|
||||
|
||||
if item.Get != nil {
|
||||
ops = append(ops, &Op{"GET", item.Get})
|
||||
}
|
||||
|
||||
if item.Delete != nil {
|
||||
ops = append(ops, &Op{"DELETE", item.Delete})
|
||||
}
|
||||
|
||||
if item.Put != nil {
|
||||
ops = append(ops, &Op{"PUT", item.Put})
|
||||
}
|
||||
|
||||
if item.Patch != nil {
|
||||
ops = append(ops, &Op{"PATCH", item.Patch})
|
||||
}
|
||||
|
||||
return ops
|
||||
}
|
||||
|
||||
func getTermWidth() int {
|
||||
if term.IsTerminal(int(os.Stdout.Fd())) {
|
||||
width, _, err := term.GetSize(int(os.Stdout.Fd()))
|
||||
if err == nil {
|
||||
return width - DefaultMargin
|
||||
}
|
||||
}
|
||||
|
||||
return 80
|
||||
}
|
||||
@@ -27,6 +27,8 @@ import (
|
||||
"codeberg.org/scip/esctl/pkg/cfg"
|
||||
"codeberg.org/scip/esctl/pkg/printer"
|
||||
"github.com/dustin/go-humanize"
|
||||
"github.com/elastic/go-elasticsearch/v9/typedapi/cat/indices"
|
||||
"github.com/elastic/go-elasticsearch/v9/typedapi/cat/tasks"
|
||||
"github.com/elastic/go-elasticsearch/v9/typedapi/ccr/stats"
|
||||
"github.com/elastic/go-elasticsearch/v9/typedapi/cluster/health"
|
||||
clusterstats "github.com/elastic/go-elasticsearch/v9/typedapi/cluster/stats"
|
||||
@@ -39,17 +41,21 @@ const (
|
||||
ResponseInfo
|
||||
ResponseCcr
|
||||
ResponseStats
|
||||
ResponseIndices
|
||||
ResponseTasks
|
||||
)
|
||||
|
||||
type ClusterIndices map[string]map[string]*types.IndicesRecord
|
||||
|
||||
type apiResponse struct {
|
||||
error error
|
||||
info *info.Response
|
||||
health *health.Response
|
||||
ccr *stats.Response
|
||||
stats *clusterstats.Response
|
||||
which int
|
||||
error error
|
||||
info *info.Response
|
||||
health *health.Response
|
||||
ccr *stats.Response
|
||||
stats *clusterstats.Response
|
||||
indices *indices.Response
|
||||
tasks *tasks.Response
|
||||
which int
|
||||
}
|
||||
|
||||
func ClusterList(conf *cfg.Config) error {
|
||||
@@ -94,7 +100,7 @@ func ClusterList(conf *cfg.Config) error {
|
||||
// have to do 3 of'em for each cluster. This speeds things up.
|
||||
func ClusterStatus(conf *cfg.Config) error {
|
||||
clusters := []string{}
|
||||
gocount := 3
|
||||
gocount := 5
|
||||
if conf.Verbose {
|
||||
gocount++
|
||||
}
|
||||
@@ -120,6 +126,8 @@ func ClusterStatus(conf *cfg.Config) error {
|
||||
go getClusterData(es, wg, responses, "health")
|
||||
go getClusterData(es, wg, responses, "info")
|
||||
go getClusterData(es, wg, responses, "ccrstats")
|
||||
go getClusterData(es, wg, responses, "indices")
|
||||
go getClusterData(es, wg, responses, "tasks")
|
||||
|
||||
if conf.Verbose {
|
||||
go getClusterData(es, wg, responses, "stats")
|
||||
@@ -131,6 +139,8 @@ func ClusterStatus(conf *cfg.Config) error {
|
||||
var info *info.Response
|
||||
var ccrstats *stats.Response
|
||||
var clusterstats *clusterstats.Response
|
||||
var indexstats *indices.Response
|
||||
var taskstatus *tasks.Response
|
||||
|
||||
for i := 0; i < gocount; i++ {
|
||||
r := <-responses
|
||||
@@ -148,11 +158,17 @@ func ClusterStatus(conf *cfg.Config) error {
|
||||
info = r.info
|
||||
case ResponseStats:
|
||||
clusterstats = r.stats
|
||||
case ResponseIndices:
|
||||
indexstats = r.indices
|
||||
case ResponseTasks:
|
||||
taskstatus = r.tasks
|
||||
}
|
||||
}
|
||||
|
||||
slog.Debug("ES result", "cluster health", clusterhealth)
|
||||
|
||||
isleader := len(ccrstats.AutoFollowStats.AutoFollowedClusters) == 0
|
||||
|
||||
ccrfollowing := ""
|
||||
if len(ccrstats.AutoFollowStats.AutoFollowedClusters) > 0 {
|
||||
// is following another cluster
|
||||
@@ -163,6 +179,22 @@ func ClusterStatus(conf *cfg.Config) error {
|
||||
)
|
||||
}
|
||||
|
||||
// look for red indices, if any
|
||||
redindices := 0
|
||||
for _, index := range *indexstats {
|
||||
if *index.Health == "red" {
|
||||
redindices++
|
||||
}
|
||||
}
|
||||
|
||||
// look for long running tasks
|
||||
longtasks := 0
|
||||
for _, task := range *taskstatus {
|
||||
if strings.Contains(*task.RunningTime, "d") {
|
||||
longtasks++
|
||||
}
|
||||
}
|
||||
|
||||
table := printer.NewTable(conf, 2, 7)
|
||||
table.Addheaders(cluster, "status")
|
||||
|
||||
@@ -170,10 +202,22 @@ func ClusterStatus(conf *cfg.Config) error {
|
||||
{"Cluster Name", clusterhealth.ClusterName},
|
||||
{"ES Status", printer.Colorize(conf, clusterhealth.Status.Name, clusterhealth.Status.Name)},
|
||||
{"ES Version", info.Version.Int},
|
||||
{"Is Leader", fmt.Sprintf("%t", isleader)},
|
||||
{"Active Shards", fmt.Sprintf("%d", clusterhealth.ActiveShards)},
|
||||
{"Active Primary Shards", fmt.Sprintf("%d", clusterhealth.ActivePrimaryShards)},
|
||||
{"Unassigned Shards", fmt.Sprintf("%d", clusterhealth.UnassignedShards)},
|
||||
{"Unassigned Primary Shards", fmt.Sprintf("%d", clusterhealth.UnassignedPrimaryShards)},
|
||||
{"Pending Tasks", fmt.Sprintf("%d", clusterhealth.NumberOfPendingTasks)},
|
||||
{"Nodes", fmt.Sprintf("%d", clusterhealth.NumberOfNodes)},
|
||||
{"AutoFollow (success/failed indices)", ccrfollowing},
|
||||
{"Red Indices", fmt.Sprintf("%d", redindices)},
|
||||
{"Long Running Tasks", fmt.Sprintf("%d", longtasks)},
|
||||
}
|
||||
|
||||
if !isleader {
|
||||
table.Entries = append(table.Entries, [][]string{
|
||||
{"AutoFollow (success/failed indices)", ccrfollowing},
|
||||
{"Followed Indices", fmt.Sprintf("%d", len(ccrstats.FollowStats.Indices))},
|
||||
}...)
|
||||
}
|
||||
|
||||
if conf.Verbose {
|
||||
@@ -202,11 +246,8 @@ func gatherClusterStats(conf *cfg.Config, clusterstats *clusterstats.Response, t
|
||||
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)},
|
||||
@@ -217,7 +258,7 @@ func gatherClusterStats(conf *cfg.Config, clusterstats *clusterstats.Response, t
|
||||
humanize.Bytes(uint64(clusterstats.Indices.Store.SizeInBytes)),
|
||||
humanize.Bytes(uint64(*clusterstats.Indices.Store.TotalDataSetSizeInBytes)),
|
||||
)},
|
||||
{"JVM Heap", fmt.Sprintf(
|
||||
{"JVM Heap Memory", fmt.Sprintf(
|
||||
"%s/%s",
|
||||
humanize.Bytes(uint64(clusterstats.Nodes.Jvm.Mem.HeapUsedInBytes)),
|
||||
humanize.Bytes(uint64(clusterstats.Nodes.Jvm.Mem.HeapMaxInBytes)),
|
||||
|
||||
@@ -27,36 +27,6 @@ import (
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
// recursively traverse the raw settings hash and build a flat map
|
||||
// consisting of the translated path and its value.
|
||||
//
|
||||
// e.g.
|
||||
// logger:
|
||||
//
|
||||
// org:
|
||||
// elasticsearch:
|
||||
// transport:
|
||||
// OutboundHandler: "ERROR"
|
||||
//
|
||||
// gets:
|
||||
//
|
||||
// logger.org.elasticsearch.transport.OutboundHandler: "ERROR"
|
||||
func getJsonPath(raw map[string]any, topic string) map[string]string {
|
||||
paths := map[string]string{}
|
||||
|
||||
for name, data := range raw {
|
||||
path := topic + "." + name
|
||||
switch value := data.(type) {
|
||||
case string:
|
||||
paths[path] = value
|
||||
case map[string]any:
|
||||
paths = getJsonPath(value, path)
|
||||
}
|
||||
}
|
||||
|
||||
return paths
|
||||
}
|
||||
|
||||
func ClusterSettingsList(conf *cfg.Config) error {
|
||||
res, err := conf.DefaultCluster.ES.Cluster.GetSettings().
|
||||
Header("content-type", "application/json").
|
||||
@@ -87,7 +57,7 @@ func ClusterSettingsList(conf *cfg.Config) error {
|
||||
return fmt.Errorf("failed to unmarshall setting for topic %s: %s", topic, err)
|
||||
}
|
||||
|
||||
paths := getJsonPath(data, topic)
|
||||
paths := getJsonPath(map[string]string{}, data, topic)
|
||||
slog.Debug("settings", topic, paths)
|
||||
|
||||
for setting, value := range paths {
|
||||
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
@@ -389,6 +390,26 @@ func getClusterData(es *elasticsearch.TypedClient, wg *sync.WaitGroup, reschan c
|
||||
ar.stats = res
|
||||
ar.which = ResponseStats
|
||||
arerr = err
|
||||
|
||||
case "indices":
|
||||
res, err := es.Cat.Indices().
|
||||
Header("content-type", "application/json").
|
||||
Header("accept", "application/json").
|
||||
Do(context.Background())
|
||||
|
||||
ar.indices = &res
|
||||
ar.which = ResponseIndices
|
||||
arerr = err
|
||||
|
||||
case "tasks":
|
||||
res, err := es.Cat.Tasks().
|
||||
Header("content-type", "application/json").
|
||||
Header("accept", "application/json").
|
||||
Do(context.Background())
|
||||
|
||||
ar.tasks = &res
|
||||
ar.which = ResponseTasks
|
||||
arerr = err
|
||||
}
|
||||
|
||||
if arerr != nil {
|
||||
@@ -397,3 +418,44 @@ func getClusterData(es *elasticsearch.TypedClient, wg *sync.WaitGroup, reschan c
|
||||
|
||||
reschan <- ar
|
||||
}
|
||||
|
||||
// recursively traverse the raw settings hash and build a flat map
|
||||
// consisting of the translated path and its value.
|
||||
//
|
||||
// e.g.
|
||||
// logger:
|
||||
//
|
||||
// org:
|
||||
// elasticsearch:
|
||||
// transport:
|
||||
// OutboundHandler: "ERROR"
|
||||
//
|
||||
// gets:
|
||||
//
|
||||
// logger.org.elasticsearch.transport.OutboundHandler: "ERROR"
|
||||
func getJsonPath(paths map[string]string, raw map[string]any, topic string) map[string]string {
|
||||
for name, data := range raw {
|
||||
path := topic + "." + name
|
||||
|
||||
switch value := data.(type) {
|
||||
case string:
|
||||
paths[path] = value
|
||||
case *string:
|
||||
paths[path] = *value
|
||||
case int:
|
||||
paths[path] = strconv.Itoa(value)
|
||||
case *int:
|
||||
paths[path] = strconv.Itoa(*value)
|
||||
case map[string]any:
|
||||
paths = getJsonPath(paths, value, path)
|
||||
case []any:
|
||||
val := []string{}
|
||||
for _, item := range value {
|
||||
val = append(val, fmt.Sprintf("%v", item))
|
||||
}
|
||||
paths[path] = strings.Join(val, ",")
|
||||
}
|
||||
}
|
||||
|
||||
return paths
|
||||
}
|
||||
|
||||
259
pkg/es/datastream.go
Normal file
259
pkg/es/datastream.go
Normal file
@@ -0,0 +1,259 @@
|
||||
/*
|
||||
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"
|
||||
"regexp"
|
||||
|
||||
"codeberg.org/scip/esctl/pkg/cfg"
|
||||
"codeberg.org/scip/esctl/pkg/printer"
|
||||
"github.com/dustin/go-humanize"
|
||||
"github.com/elastic/go-elasticsearch/v9/typedapi/types"
|
||||
)
|
||||
|
||||
func DatastreamNames(conf *cfg.Config) ([]string, error) {
|
||||
res, err := conf.DefaultCluster.ES.Indices.GetDataStream().
|
||||
Header("content-type", "application/json").
|
||||
Header("accept", "application/json").
|
||||
Do(context.Background())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get data streams: %s", esErrorString(err))
|
||||
}
|
||||
|
||||
dss := make([]string, len(res.DataStreams))
|
||||
for idx, ds := range res.DataStreams {
|
||||
dss[idx] = ds.Name
|
||||
}
|
||||
|
||||
return dss, nil
|
||||
}
|
||||
|
||||
func DatastreamList(conf *cfg.Config) error {
|
||||
res, err := conf.DefaultCluster.ES.Indices.GetDataStream().
|
||||
Header("content-type", "application/json").
|
||||
Header("accept", "application/json").
|
||||
Do(context.Background())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get data streams: %s", esErrorString(err))
|
||||
}
|
||||
|
||||
slog.Debug("ES result", "data streams", res)
|
||||
|
||||
list := filterDatastreams(conf, res.DataStreams)
|
||||
|
||||
size := len(list)
|
||||
|
||||
if conf.MaxItems > 0 {
|
||||
if size > conf.MaxItems {
|
||||
size = conf.MaxItems
|
||||
}
|
||||
}
|
||||
|
||||
table := printer.NewTable(conf, 7, size)
|
||||
table.Addheaders("name", "ilm policy", "hidden", "system", "replicated", "generation", "timestamp field")
|
||||
|
||||
for idx, ds := range list {
|
||||
name := printer.Colorize(conf, ds.Status.String(), ds.Name)
|
||||
policy := ""
|
||||
if ds.IlmPolicy != nil {
|
||||
policy = *ds.IlmPolicy
|
||||
}
|
||||
|
||||
table.Entries[idx] = []string{
|
||||
name,
|
||||
policy,
|
||||
fmt.Sprintf("%t", ds.Hidden),
|
||||
fmt.Sprintf("%t", *ds.System),
|
||||
fmt.Sprintf("%t", *ds.Replicated),
|
||||
fmt.Sprintf("%d", ds.Generation),
|
||||
ds.TimestampField.Name,
|
||||
}
|
||||
|
||||
if idx == size-1 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
table.Sort()
|
||||
if err := table.Print(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func filterDatastreams(conf *cfg.Config, list []types.DataStream) []types.DataStream {
|
||||
selected := []types.DataStream{}
|
||||
|
||||
for _, ds := range list {
|
||||
if !conf.Hidden && ds.Hidden {
|
||||
continue
|
||||
}
|
||||
|
||||
selected = append(selected, ds)
|
||||
}
|
||||
|
||||
if len(conf.Filter) == 0 {
|
||||
return selected
|
||||
}
|
||||
|
||||
// we support just one filter here, for now
|
||||
filter := *regexp.MustCompile(conf.Filter[0])
|
||||
newlist := []types.DataStream{}
|
||||
|
||||
for _, ds := range selected {
|
||||
if filter.MatchString(ds.Name) {
|
||||
newlist = append(newlist, ds)
|
||||
}
|
||||
}
|
||||
|
||||
return newlist
|
||||
}
|
||||
|
||||
func DatastreamShow(conf *cfg.Config, dsname string) error {
|
||||
res, err := conf.DefaultCluster.ES.Indices.GetDataStream().
|
||||
Name(dsname).
|
||||
Header("content-type", "application/json").
|
||||
Header("accept", "application/json").
|
||||
Do(context.Background())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get data stream: %s", esErrorString(err))
|
||||
}
|
||||
|
||||
slog.Debug("ES result", "data stream", res)
|
||||
|
||||
if len(res.DataStreams) == 0 {
|
||||
return errors.New("no data stream found with that name")
|
||||
}
|
||||
|
||||
stats, err := conf.DefaultCluster.ES.Indices.DataStreamsStats().
|
||||
Name(dsname).
|
||||
Header("content-type", "application/json").
|
||||
Header("accept", "application/json").
|
||||
Do(context.Background())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get data stream stats: %s", esErrorString(err))
|
||||
}
|
||||
|
||||
table := printer.NewTable(conf, 11, 2)
|
||||
table.Addheaders("data stream property", "value")
|
||||
|
||||
ds := res.DataStreams[0]
|
||||
|
||||
name := printer.Colorize(conf, ds.Status.String(), ds.Name)
|
||||
policy := ""
|
||||
if ds.IlmPolicy != nil {
|
||||
policy = *ds.IlmPolicy
|
||||
}
|
||||
|
||||
table.Entries = [][]string{
|
||||
{"name", name},
|
||||
{"ilm policy", policy},
|
||||
{"hidden", fmt.Sprintf("%t", ds.Hidden)},
|
||||
{"system", fmt.Sprintf("%t", *ds.System)},
|
||||
{"replicated", fmt.Sprintf("%t", *ds.Replicated)},
|
||||
{"generation", fmt.Sprintf("%d", ds.Generation)},
|
||||
{"timestamp field", ds.TimestampField.Name},
|
||||
{"backing indices", fmt.Sprintf("%d", stats.BackingIndices)},
|
||||
{"size", humanize.Bytes(uint64(stats.TotalStoreSizeBytes))},
|
||||
{"shards-failed", fmt.Sprintf("%d", stats.Shards_.Failed)},
|
||||
{"shards-successful", fmt.Sprintf("%d", stats.Shards_.Successful)},
|
||||
}
|
||||
|
||||
if err := table.Print(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
table = printer.NewTable(conf, 5, len(ds.Indices))
|
||||
table.Addheaders("backing index name", "uuid", "prefer ilm", "ilm policy", "managed by")
|
||||
|
||||
for idx, index := range ds.Indices {
|
||||
policy := ""
|
||||
if ds.IlmPolicy != nil {
|
||||
policy = *ds.IlmPolicy
|
||||
}
|
||||
|
||||
table.Entries[idx] = []string{
|
||||
index.IndexName,
|
||||
index.IndexUuid,
|
||||
fmt.Sprintf("%t", *index.PreferIlm),
|
||||
policy,
|
||||
index.ManagedBy.Name,
|
||||
}
|
||||
|
||||
if idx < len(ds.Indices) {
|
||||
fmt.Println()
|
||||
}
|
||||
}
|
||||
|
||||
table.Sort()
|
||||
if err := table.Print(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func DatastreamCreate(conf *cfg.Config, dsname string) error {
|
||||
_, err := conf.DefaultCluster.ES.Indices.CreateDataStream(dsname).
|
||||
Header("content-type", "application/json").
|
||||
Header("accept", "application/json").
|
||||
Do(context.Background())
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create datastream: %s", esErrorString(err))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func DatastreamDelete(conf *cfg.Config, dsname string) error {
|
||||
_, err := conf.DefaultCluster.ES.Indices.DeleteDataStream(dsname).
|
||||
Header("content-type", "application/json").
|
||||
Header("accept", "application/json").
|
||||
Do(context.Background())
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to delete datastream: %s", esErrorString(err))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func DatastreamRollover(conf *cfg.Config, ds string) error {
|
||||
res, err := RolloverAlias(conf, ds)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to rollover data stream: %s", esErrorString(err))
|
||||
}
|
||||
|
||||
table := printer.NewTable(conf, 2, 5)
|
||||
table.Addheaders("rollover response", "value")
|
||||
table.Entries = [][]string{
|
||||
{"acknowledged", fmt.Sprintf("%t", res.Acknowledged)},
|
||||
{"rolled over", fmt.Sprintf("%t", res.RolledOver)},
|
||||
{"shards acknowledged", fmt.Sprintf("%t", res.ShardsAcknowledged)},
|
||||
{"old index", res.OldIndex},
|
||||
{"new index", res.NewIndex},
|
||||
}
|
||||
|
||||
return table.Print()
|
||||
}
|
||||
301
pkg/es/ilm.go
Normal file
301
pkg/es/ilm.go
Normal file
@@ -0,0 +1,301 @@
|
||||
/*
|
||||
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"
|
||||
"strings"
|
||||
|
||||
"codeberg.org/scip/esctl/pkg/cfg"
|
||||
"codeberg.org/scip/esctl/pkg/printer"
|
||||
"github.com/alecthomas/repr"
|
||||
"github.com/elastic/go-elasticsearch/v9/typedapi/esdsl"
|
||||
"github.com/elastic/go-elasticsearch/v9/typedapi/ilm/putlifecycle"
|
||||
"github.com/elastic/go-elasticsearch/v9/typedapi/types"
|
||||
)
|
||||
|
||||
func IlmRetry(conf *cfg.Config, index string) error {
|
||||
_, err := conf.DefaultCluster.ES.Ilm.Retry(index).
|
||||
Do(context.Background())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to retry ilm: %s", esErrorString(err))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func IlmStatus(conf *cfg.Config) error {
|
||||
res, err := conf.DefaultCluster.ES.Ilm.GetStatus().
|
||||
Do(context.Background())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get ilm status: %s", esErrorString(err))
|
||||
}
|
||||
|
||||
fmt.Println(res.OperationMode.Name)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func IlmNames(conf *cfg.Config) ([]string, error) {
|
||||
res, err := conf.DefaultCluster.ES.Ilm.GetLifecycle().
|
||||
Do(context.Background())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get ilm policies: %s", esErrorString(err))
|
||||
}
|
||||
|
||||
names := make([]string, len(res))
|
||||
idx := 0
|
||||
|
||||
for name := range res {
|
||||
names[idx] = name
|
||||
}
|
||||
|
||||
return names, nil
|
||||
}
|
||||
|
||||
func IlmList(conf *cfg.Config, pattern string) error {
|
||||
ilm := conf.DefaultCluster.ES.Ilm.GetLifecycle()
|
||||
|
||||
if pattern != "" {
|
||||
ilm.FilterPath(pattern)
|
||||
}
|
||||
|
||||
res, err := ilm.Do(context.Background())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get ilm policies: %s", esErrorString(err))
|
||||
}
|
||||
|
||||
if conf.Debug {
|
||||
repr.Println(res)
|
||||
}
|
||||
|
||||
table := printer.NewTable(conf, 5, 0)
|
||||
table.Addheaders("ilm policy", "hot", "warm", "frozen", "delete")
|
||||
|
||||
for name, ilm := range res {
|
||||
table.AddRow(name,
|
||||
ilmPhaseString(ilm.Policy.Phases.Hot, true),
|
||||
ilmPhaseString(ilm.Policy.Phases.Warm, true),
|
||||
ilmPhaseString(ilm.Policy.Phases.Frozen, true),
|
||||
ilmPhaseString(ilm.Policy.Phases.Delete, true),
|
||||
)
|
||||
}
|
||||
|
||||
table.Sort()
|
||||
return table.Print()
|
||||
}
|
||||
|
||||
func IlmShow(conf *cfg.Config, policy string) error {
|
||||
res, err := conf.DefaultCluster.ES.Ilm.GetLifecycle().
|
||||
Policy(policy).
|
||||
Do(context.Background())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get ilm status: %s", esErrorString(err))
|
||||
}
|
||||
|
||||
if conf.Debug {
|
||||
repr.Println(res)
|
||||
}
|
||||
|
||||
ilm, exists := res[policy]
|
||||
if !exists {
|
||||
return errors.New("no ilm policy retrieved")
|
||||
}
|
||||
|
||||
table := printer.NewTable(conf, 2, 5)
|
||||
table.Addheaders("ilm policy setting", "value")
|
||||
|
||||
table.Entries = [][]string{
|
||||
{"policy", policy},
|
||||
{"hot phase", ilmPhaseString(ilm.Policy.Phases.Hot, false)},
|
||||
{"warm phase", ilmPhaseString(ilm.Policy.Phases.Warm, false)},
|
||||
{"frozen phase", ilmPhaseString(ilm.Policy.Phases.Frozen, false)},
|
||||
{"delete phase", ilmPhaseString(ilm.Policy.Phases.Delete, false)},
|
||||
}
|
||||
|
||||
return table.Print()
|
||||
}
|
||||
|
||||
func ilmPhaseString(phase *types.Phase, short bool) string {
|
||||
if phase == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
out := []string{fmt.Sprintf("move-after:%s", phase.MinAge)}
|
||||
|
||||
if short {
|
||||
return strings.Join(out, ",")
|
||||
}
|
||||
|
||||
if phase.Actions.Rollover != nil {
|
||||
if phase.Actions.Rollover.MaxAge != "" {
|
||||
out = append(out, fmt.Sprintf("rollover:%s", phase.Actions.Rollover.MaxAge))
|
||||
if phase.Actions.Rollover.MaxPrimaryShardSize != nil {
|
||||
out = append(out, fmt.Sprintf("shardsize:%s", phase.Actions.Rollover.MaxPrimaryShardSize))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if phase.Actions.Delete != nil {
|
||||
out = append(out, fmt.Sprintf("delete-searchable-snapshot:%t", *phase.Actions.Delete.DeleteSearchableSnapshot))
|
||||
}
|
||||
|
||||
if phase.Actions.SearchableSnapshot != nil {
|
||||
out = append(out, fmt.Sprintf("searchable-snapshot:%s", phase.Actions.SearchableSnapshot.SnapshotRepository))
|
||||
}
|
||||
|
||||
return strings.Join(out, ",")
|
||||
}
|
||||
|
||||
func IlmExplain(conf *cfg.Config, index string) error {
|
||||
res, err := conf.DefaultCluster.ES.Ilm.ExplainLifecycle(index).
|
||||
Do(context.Background())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get ilm state: %s", esErrorString(err))
|
||||
}
|
||||
|
||||
slog.Debug("ilm status", "ilm", res)
|
||||
|
||||
explain, exists := res.Indices[index]
|
||||
if !exists {
|
||||
return errors.New("no ilm status retrieved")
|
||||
}
|
||||
|
||||
ilm := explain.(*types.LifecycleExplainManaged)
|
||||
|
||||
table := printer.NewTable(conf, 2, 0)
|
||||
table.Addheaders("ilm status field", "value")
|
||||
|
||||
table.Entries = [][]string{
|
||||
{"index", ilm.Index},
|
||||
{"ilm policy", *ilm.Policy},
|
||||
{"action", *ilm.Action},
|
||||
{"step", *ilm.Step},
|
||||
{"age", fmt.Sprintf("%s", ilm.Age)},
|
||||
{"managed", fmt.Sprintf("%t", ilm.Managed)},
|
||||
{"phase", *ilm.Phase},
|
||||
{"phase execution", ilmPhaseString(ilm.PhaseExecution.PhaseDefinition, false)},
|
||||
}
|
||||
|
||||
return table.Print()
|
||||
}
|
||||
|
||||
func IlmCreate(conf *cfg.Config, policyname string) error {
|
||||
ilm := conf.DefaultCluster.ES.Ilm.PutLifecycle(policyname)
|
||||
cfg := conf.Ilm
|
||||
|
||||
var phases types.PhasesVariant = esdsl.NewPhases()
|
||||
|
||||
if cfg.HaveHot() {
|
||||
hot := types.Phase{}
|
||||
var actions types.IlmActionsVariant = esdsl.NewIlmActions()
|
||||
rollover := &types.RolloverAction{}
|
||||
haveroll := false
|
||||
|
||||
if cfg.HotMinAge != "" {
|
||||
hot.MinAge = cfg.HotMinAge
|
||||
}
|
||||
|
||||
if cfg.HotRolloverMaxAge != "" {
|
||||
rollover.MaxAge = cfg.HotRolloverMaxAge
|
||||
haveroll = true
|
||||
}
|
||||
|
||||
if cfg.HotRolloverMaxDocs != 0 {
|
||||
rollover.MaxDocs = &cfg.HotRolloverMaxDocs
|
||||
haveroll = true
|
||||
}
|
||||
|
||||
if cfg.HotRolloverMaxPrimaryShardSize != 0 {
|
||||
rollover.MaxPrimaryShardSize = &cfg.HotRolloverMaxPrimaryShardSize
|
||||
}
|
||||
|
||||
if haveroll {
|
||||
actions.IlmActionsCaster().Rollover = rollover
|
||||
}
|
||||
|
||||
hot.Actions = actions.IlmActionsCaster()
|
||||
phases.PhasesCaster().Hot = &hot
|
||||
}
|
||||
|
||||
if cfg.HaveWarm() {
|
||||
warm := types.Phase{}
|
||||
var actions types.IlmActionsVariant = esdsl.NewIlmActions()
|
||||
|
||||
if cfg.WarmForceMerge != 0 {
|
||||
actions.IlmActionsCaster().Forcemerge = &types.ForceMergeAction{MaxNumSegments: cfg.WarmForceMerge}
|
||||
}
|
||||
|
||||
if cfg.WarmMinAge != "" {
|
||||
warm.MinAge = cfg.WarmMinAge
|
||||
}
|
||||
|
||||
if cfg.WarmPriority != 0 {
|
||||
actions.IlmActionsCaster().SetPriority = &types.SetPriorityAction{Priority: &cfg.WarmPriority}
|
||||
}
|
||||
|
||||
if cfg.WarmShrinkShards != 0 {
|
||||
actions.IlmActionsCaster().Shrink = &types.ShrinkAction{NumberOfShards: &cfg.WarmShrinkShards}
|
||||
}
|
||||
|
||||
warm.Actions = actions.IlmActionsCaster()
|
||||
phases.PhasesCaster().Warm = &warm
|
||||
}
|
||||
|
||||
if cfg.HaveFrozen() {
|
||||
froze := phases.PhasesCaster().Frozen
|
||||
|
||||
if cfg.FrozenMinAge != "" {
|
||||
froze.MinAge = cfg.FrozenMinAge
|
||||
}
|
||||
}
|
||||
|
||||
if cfg.HaveDelete() {
|
||||
del := phases.PhasesCaster().Delete
|
||||
delete := types.DeleteAction{}
|
||||
var actions types.IlmActionsVariant = esdsl.NewIlmActions()
|
||||
|
||||
if cfg.DeleteMinAge != "" {
|
||||
del.MinAge = cfg.DeleteMinAge
|
||||
}
|
||||
|
||||
if cfg.DeleteSearchableSnapshots {
|
||||
delete.DeleteSearchableSnapshot = &cfg.DeleteSearchableSnapshots
|
||||
}
|
||||
|
||||
actions.IlmActionsCaster().Delete = &delete
|
||||
del.Actions = actions.IlmActionsCaster()
|
||||
phases.PhasesCaster().Delete = del
|
||||
}
|
||||
|
||||
put := &putlifecycle.Request{}
|
||||
policy := &types.IlmPolicy{}
|
||||
policy.IlmPolicyCaster().Phases = *phases.PhasesCaster()
|
||||
put.Policy = policy
|
||||
|
||||
ilm.Request(put)
|
||||
|
||||
_, err := ilm.Do(context.Background())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed create ilm policy: %s", esErrorString(err))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -59,6 +59,12 @@ func filterIndices(conf *cfg.Config, list indices.Response) indices.Response {
|
||||
if !conf.Partials && strings.HasPrefix(*index.Index, "partial-") {
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.HasPrefix(*index.Index, ".ds-") {
|
||||
// ignore data stream backing indicies
|
||||
continue
|
||||
}
|
||||
|
||||
selectedlist = append(selectedlist, index)
|
||||
}
|
||||
|
||||
@@ -120,11 +126,7 @@ func IndexList(conf *cfg.Config) error {
|
||||
}
|
||||
|
||||
table.Sort()
|
||||
if err := table.Print(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
return table.Print()
|
||||
}
|
||||
|
||||
func IndexShow(conf *cfg.Config, indexpattern string) error {
|
||||
|
||||
@@ -113,3 +113,23 @@ func IndexAliasDelete(conf *cfg.Config, index, alias string) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func IndexAliasRollover(conf *cfg.Config, alias string) error {
|
||||
res, err := RolloverAlias(conf, alias)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to rollover index alias: %s", esErrorString(err))
|
||||
}
|
||||
|
||||
table := printer.NewTable(conf, 2, 5)
|
||||
table.Addheaders("rollover response", "value")
|
||||
table.Entries = [][]string{
|
||||
{"acknowledged", fmt.Sprintf("%t", res.Acknowledged)},
|
||||
{"rolled over", fmt.Sprintf("%t", res.RolledOver)},
|
||||
{"shards acknowledged", fmt.Sprintf("%t", res.ShardsAcknowledged)},
|
||||
{"old index", res.OldIndex},
|
||||
{"new index", res.NewIndex},
|
||||
}
|
||||
|
||||
return table.Print()
|
||||
}
|
||||
|
||||
503
pkg/es/index_template.go
Normal file
503
pkg/es/index_template.go
Normal file
@@ -0,0 +1,503 @@
|
||||
/*
|
||||
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"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"codeberg.org/scip/esctl/pkg/cfg"
|
||||
"codeberg.org/scip/esctl/pkg/printer"
|
||||
"github.com/elastic/go-elasticsearch/v9/typedapi/esdsl"
|
||||
"github.com/elastic/go-elasticsearch/v9/typedapi/types"
|
||||
)
|
||||
|
||||
// used for completion
|
||||
func IndexTemplateList(conf *cfg.Config) error {
|
||||
res, err := conf.DefaultCluster.ES.Indices.GetIndexTemplate().
|
||||
Header("content-type", "application/json").
|
||||
Header("accept", "application/json").
|
||||
Do(context.Background())
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get index templates: %s", esErrorString(err))
|
||||
}
|
||||
|
||||
slog.Debug("res", "index templates", res)
|
||||
|
||||
table := printer.NewTable(conf, 5, len(res.IndexTemplates))
|
||||
table.Addheaders("name", "description", "priority")
|
||||
|
||||
for idx, tpl := range res.IndexTemplates {
|
||||
desc, err := json.Marshal(tpl.IndexTemplate.Meta_["description"])
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to unmarshal meta json data: %w", err)
|
||||
}
|
||||
|
||||
table.Entries[idx] = []string{
|
||||
tpl.Name,
|
||||
string(desc),
|
||||
fmt.Sprintf("%d", tpl.IndexTemplate.Priority),
|
||||
}
|
||||
}
|
||||
|
||||
table.Sort()
|
||||
return table.Print()
|
||||
}
|
||||
|
||||
func IndexTemplateShow(conf *cfg.Config, tplname string) error {
|
||||
res, err := conf.DefaultCluster.ES.Indices.GetIndexTemplate().
|
||||
Name(tplname).
|
||||
Header("content-type", "application/json").
|
||||
Header("accept", "application/json").
|
||||
Do(context.Background())
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get index template: %s", esErrorString(err))
|
||||
}
|
||||
|
||||
slog.Debug("res", "index template", res)
|
||||
|
||||
if len(res.IndexTemplates) != 1 {
|
||||
return errors.New("multiple or no index template matched the pattern")
|
||||
}
|
||||
|
||||
tpl := res.IndexTemplates[0]
|
||||
|
||||
table := printer.NewTable(conf, 2, 6)
|
||||
table.Addheaders("index template property", "value")
|
||||
|
||||
desc, err := json.Marshal(tpl.IndexTemplate.Meta_["description"])
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to unmarshal meta json data: %w", err)
|
||||
}
|
||||
|
||||
hasds := tpl.IndexTemplate.DataStream != nil
|
||||
|
||||
aliases := []string{}
|
||||
for alias := range tpl.IndexTemplate.Template.Aliases {
|
||||
aliases = append(aliases, alias)
|
||||
}
|
||||
|
||||
table.Entries = [][]string{
|
||||
{"name", tpl.Name},
|
||||
{"description", string(desc)},
|
||||
{"index patterns", strings.Join(tpl.IndexTemplate.IndexPatterns, ",")},
|
||||
{"composed of", strings.Join(tpl.IndexTemplate.ComposedOf, ",")},
|
||||
{"data stream enabled", fmt.Sprintf("%t", hasds)},
|
||||
{"aliases", strings.Join(aliases, ",")},
|
||||
}
|
||||
|
||||
if err := table.Print(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
table = printer.NewTable(conf, 2, 0)
|
||||
table.Addheaders("index setting property", "value")
|
||||
|
||||
err = getIndexTemplateSettings(conf, tplname, table)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
if err := table.Print(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
table = printer.NewTable(conf, 2, 0)
|
||||
table.Addheaders("index field mapping", "type")
|
||||
|
||||
if tpl.IndexTemplate.Template.Mappings != nil {
|
||||
for name, field := range tpl.IndexTemplate.Template.Mappings.Properties {
|
||||
typeval := ""
|
||||
|
||||
switch val := field.(type) {
|
||||
case *types.IntegerNumberProperty:
|
||||
typeval = val.Type
|
||||
case *types.KeywordProperty:
|
||||
typeval = val.Type
|
||||
case *types.DateProperty:
|
||||
typeval = val.Type
|
||||
}
|
||||
|
||||
table.Entries = append(table.Entries, []string{
|
||||
name, typeval,
|
||||
})
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
if err := table.Print(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func IndexTemplateCreate(conf *cfg.Config, name string, mappings []string) error {
|
||||
settings := esdsl.NewIndexSettings()
|
||||
maps := esdsl.NewIndexTemplateMapping()
|
||||
|
||||
create := conf.DefaultCluster.ES.Indices.PutIndexTemplate(name).
|
||||
Header("content-type", "application/json").
|
||||
Header("accept", "application/json")
|
||||
|
||||
if conf.Shards > 0 {
|
||||
settings = settings.NumberOfShards(strconv.Itoa(conf.Shards))
|
||||
}
|
||||
|
||||
if conf.Replicas > 0 {
|
||||
settings = settings.NumberOfReplicas(strconv.Itoa(conf.Replicas))
|
||||
}
|
||||
|
||||
if conf.Stream {
|
||||
create.DataStream(esdsl.NewDataStreamVisibility())
|
||||
|
||||
if conf.Retention != "" {
|
||||
maps.Lifecycle(
|
||||
esdsl.NewDataStreamLifecycle().
|
||||
DataRetention(
|
||||
esdsl.NewDuration().String(conf.Retention)))
|
||||
}
|
||||
}
|
||||
|
||||
if conf.AutoCreate {
|
||||
create.AllowAutoCreate(true)
|
||||
}
|
||||
|
||||
if conf.Mode != "" {
|
||||
settings = settings.Mode(conf.Mode)
|
||||
}
|
||||
|
||||
if len(mappings) > 0 {
|
||||
typemaps, err := modMappings(mappings)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
maps.Mappings(typemaps)
|
||||
}
|
||||
|
||||
if len(conf.Components) > 0 {
|
||||
create.ComposedOf(conf.Components...)
|
||||
}
|
||||
|
||||
for _, alias := range conf.Aliases {
|
||||
maps.AddAlias(alias, esdsl.NewAlias())
|
||||
}
|
||||
|
||||
if len(conf.Meta) > 0 {
|
||||
metadata, err := modMeta(conf, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
create.Meta_(esdsl.NewMetadata(metadata))
|
||||
}
|
||||
|
||||
if len(conf.Settings) > 0 {
|
||||
usersettings, err := modSettings(conf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
settings = settings.IndexSettings(usersettings)
|
||||
}
|
||||
|
||||
maps.Settings(settings)
|
||||
create.Template(maps)
|
||||
create.IndexPatterns(conf.Patterns...)
|
||||
|
||||
_, err := create.Do(context.Background())
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create index template: %s", esErrorString(err))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// FIXME: func's too long, refactor
|
||||
// FIXME: it's not yet possible to remove items in lists (aliases, mappings, settings), just overwrite them
|
||||
func IndexTemplateModify(conf *cfg.Config, name string, mappings []string) error {
|
||||
maps := esdsl.NewIndexTemplateMapping()
|
||||
|
||||
// load existing index mapping
|
||||
res, err := conf.DefaultCluster.ES.Indices.GetIndexTemplate().
|
||||
Name(name).
|
||||
Header("content-type", "application/json").
|
||||
Header("accept", "application/json").
|
||||
Do(context.Background())
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get index template: %s", esErrorString(err))
|
||||
}
|
||||
|
||||
slog.Debug("res", "index template", res)
|
||||
|
||||
if len(res.IndexTemplates) != 1 {
|
||||
return errors.New("multiple or no index template matched the pattern")
|
||||
}
|
||||
|
||||
tpl := res.IndexTemplates[0]
|
||||
|
||||
// our modify PUT request
|
||||
modify := conf.DefaultCluster.ES.Indices.PutIndexTemplate(name).
|
||||
Header("content-type", "application/json").
|
||||
Header("accept", "application/json")
|
||||
|
||||
// load existing settings, if any
|
||||
settings := tpl.IndexTemplate.Template.Settings
|
||||
|
||||
// pre fill mappings and aliases
|
||||
maps.Mappings(tpl.IndexTemplate.Template.Mappings)
|
||||
maps.Aliases(tpl.IndexTemplate.Template.Aliases)
|
||||
|
||||
// pre fill components
|
||||
modify.ComposedOf(tpl.IndexTemplate.ComposedOf...)
|
||||
|
||||
// pre fill data stream config
|
||||
if conf.Stream {
|
||||
modify.DataStream(esdsl.NewDataStreamVisibility())
|
||||
|
||||
if conf.Retention != "" {
|
||||
maps.Lifecycle(
|
||||
esdsl.NewDataStreamLifecycle().
|
||||
DataRetention(
|
||||
esdsl.NewDuration().String(conf.Retention)))
|
||||
}
|
||||
}
|
||||
|
||||
modify.AllowAutoCreate(conf.AutoCreate)
|
||||
|
||||
if conf.Mode != "" {
|
||||
*settings.Mode = conf.Mode
|
||||
}
|
||||
|
||||
if len(mappings) > 0 {
|
||||
typemaps, err := modMappings(mappings)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
maps.Mappings(typemaps)
|
||||
}
|
||||
|
||||
if len(conf.Components) > 0 {
|
||||
modify.ComposedOf(conf.Components...)
|
||||
}
|
||||
|
||||
for _, alias := range conf.Aliases {
|
||||
maps.AddAlias(alias, esdsl.NewAlias())
|
||||
}
|
||||
|
||||
if len(conf.Meta) > 0 {
|
||||
// pre fill meta, if any
|
||||
metadata, err := modMeta(conf, tpl.IndexTemplate.Meta_)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
modify.Meta_(esdsl.NewMetadata(metadata))
|
||||
}
|
||||
|
||||
if len(conf.Settings) > 0 {
|
||||
usersettings, err := modSettings(conf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
settings.IndexSettings = usersettings
|
||||
}
|
||||
|
||||
patterns := tpl.IndexTemplate.IndexPatterns
|
||||
if len(conf.Patterns) > 0 {
|
||||
patterns = conf.Patterns
|
||||
}
|
||||
|
||||
maps.Settings(settings)
|
||||
modify.Template(maps)
|
||||
modify.IndexPatterns(patterns...)
|
||||
|
||||
_, err = modify.Do(context.Background())
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to modify index template: %s", esErrorString(err))
|
||||
}
|
||||
|
||||
if conf.Rollover {
|
||||
if err := rolloverAliasIndexTemplate(conf, name); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func IndexTemplateDelete(conf *cfg.Config, name string) error {
|
||||
_, err := conf.DefaultCluster.ES.Indices.DeleteIndexTemplate(name).
|
||||
Header("content-type", "application/json").
|
||||
Header("accept", "application/json").
|
||||
Do(context.Background())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to delete index template: %s", esErrorString(err))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Based on given index template find the associated index patterns,
|
||||
// find indices matching those, find their associated aliases and
|
||||
// rollover all we find. Only run when conf.Rollover==true
|
||||
func rolloverAliasIndexTemplate(conf *cfg.Config, name string) error {
|
||||
res, err := conf.DefaultCluster.ES.Indices.GetIndexTemplate().
|
||||
Name(name).
|
||||
Do(context.Background())
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get index template: %s", esErrorString(err))
|
||||
}
|
||||
|
||||
slog.Debug("res", "index template", res)
|
||||
|
||||
if len(res.IndexTemplates) != 1 {
|
||||
return errors.New("multiple or no index template matched the pattern")
|
||||
}
|
||||
|
||||
patterns := res.IndexTemplates[0].IndexTemplate.IndexPatterns
|
||||
aliases := map[string]int{}
|
||||
|
||||
// find all aliases matching the patterns
|
||||
for _, pattern := range patterns {
|
||||
res, err := conf.DefaultCluster.ES.Indices.ResolveIndex(pattern).
|
||||
Do(context.Background())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to resolve index pattern: %s", esErrorString(err))
|
||||
}
|
||||
|
||||
for _, index := range res.Indices {
|
||||
for _, alias := range index.Aliases {
|
||||
aliases[alias] = 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(aliases) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
table := printer.NewTable(conf, 4, 0)
|
||||
table.Addheaders("rollover alias", "status", "ack", "new index")
|
||||
|
||||
// apply rollover to all matching aliases, if any
|
||||
for alias := range aliases {
|
||||
res, err := RolloverAlias(conf, alias)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
table.Entries = append(table.Entries, []string{
|
||||
alias,
|
||||
fmt.Sprintf("%t", res.RolledOver),
|
||||
fmt.Sprintf("%t", res.Acknowledged),
|
||||
res.NewIndex,
|
||||
})
|
||||
|
||||
if err := table.Print(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
func modMappings(mappings []string) (types.TypeMappingVariant, error) {
|
||||
typemaps := esdsl.NewTypeMapping()
|
||||
|
||||
for _, mapping := range mappings {
|
||||
parts := strings.Split(mapping, ":")
|
||||
if len(parts) != 2 {
|
||||
return nil, fmt.Errorf(
|
||||
"invalid mapping %s, expect <name:type> (type: integer, text, date, keyword)",
|
||||
mapping)
|
||||
}
|
||||
|
||||
switch parts[1] {
|
||||
case "text":
|
||||
typemaps.AddProperty(parts[0], esdsl.NewTextProperty())
|
||||
case "integer":
|
||||
typemaps.AddProperty(parts[0], esdsl.NewIntegerNumberProperty())
|
||||
case "date":
|
||||
typemaps.AddProperty(parts[0], esdsl.NewDateProperty())
|
||||
case "keyword":
|
||||
typemaps.AddProperty(parts[0], esdsl.NewKeywordProperty())
|
||||
}
|
||||
}
|
||||
|
||||
return typemaps, nil
|
||||
}
|
||||
|
||||
func modMeta(conf *cfg.Config, meta types.Metadata) (map[string]json.RawMessage, error) {
|
||||
metadata := map[string]json.RawMessage{}
|
||||
|
||||
for key, value := range meta {
|
||||
metadata[key] = value
|
||||
}
|
||||
|
||||
for _, meta := range conf.Meta {
|
||||
parts := strings.Split(meta, ":")
|
||||
if len(parts) != 2 {
|
||||
return nil, errors.New("meta data must be in the form key:value")
|
||||
}
|
||||
|
||||
msg, err := json.Marshal(parts[1])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to json marshal metadata %s: %w", meta, err)
|
||||
}
|
||||
|
||||
metadata[parts[0]] = msg
|
||||
}
|
||||
|
||||
return metadata, nil
|
||||
}
|
||||
|
||||
func modSettings(conf *cfg.Config) (map[string]json.RawMessage, error) {
|
||||
usersettings := map[string]json.RawMessage{}
|
||||
|
||||
for _, meta := range conf.Settings {
|
||||
parts := strings.Split(meta, ":")
|
||||
if len(parts) != 2 {
|
||||
return nil, errors.New("settings data must be in the form key:value")
|
||||
}
|
||||
|
||||
msg, err := json.Marshal(parts[1])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to json marshal metadata %s: %w", meta, err)
|
||||
}
|
||||
|
||||
usersettings[parts[0]] = msg
|
||||
}
|
||||
|
||||
return usersettings, nil
|
||||
}
|
||||
61
pkg/es/index_template_settings.go
Normal file
61
pkg/es/index_template_settings.go
Normal file
@@ -0,0 +1,61 @@
|
||||
package es
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"codeberg.org/scip/esctl/pkg/cfg"
|
||||
"codeberg.org/scip/esctl/pkg/printer"
|
||||
)
|
||||
|
||||
type TplTemplateData struct {
|
||||
Settings map[string]any `json:"settings"`
|
||||
Aliases map[string]any `json:"aliases"`
|
||||
Lifecycle map[string]any `json:"lifecycle"`
|
||||
Mappings map[string]any `json:"mappings"`
|
||||
}
|
||||
|
||||
type TplTemplate struct {
|
||||
Template TplTemplateData `json:"template"`
|
||||
}
|
||||
|
||||
type TplTpl struct {
|
||||
IndexTemplate TplTemplate `json:"index_template"`
|
||||
}
|
||||
|
||||
type Tpl struct {
|
||||
IndexTemplates []TplTpl `json:"index_templates"`
|
||||
}
|
||||
|
||||
func getIndexTemplateSettings(conf *cfg.Config, tplname string, table *printer.Table) error {
|
||||
raw, err := CallAPI(conf, "GET", "/_index_template/"+tplname, "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
data := Tpl{}
|
||||
if err = json.Unmarshal(raw, &data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if conf.Debug {
|
||||
if err := prettyfiJson(conf, raw); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if len(data.IndexTemplates) == 0 {
|
||||
return fmt.Errorf("index_template %s not found", tplname)
|
||||
}
|
||||
|
||||
tpl := data.IndexTemplates[0].IndexTemplate.Template.Settings
|
||||
for topic, val := range tpl {
|
||||
paths := getJsonPath(map[string]string{}, val.(map[string]any), topic)
|
||||
|
||||
for setting, value := range paths {
|
||||
table.Entries = append(table.Entries, []string{setting, fmt.Sprintf("%v", value)})
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
224
pkg/es/repl.go
224
pkg/es/repl.go
@@ -1,224 +0,0 @@
|
||||
/*
|
||||
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 (
|
||||
"bufio"
|
||||
"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"
|
||||
)
|
||||
|
||||
const intro = `Input format: verb path [data]"
|
||||
|
||||
Example:
|
||||
|
||||
post /yourindex/_ccr/pause_follow
|
||||
put /yourindex/_settings {"number_of_replicas": 1}
|
||||
|
||||
You can also put multiline JSON after the path like:
|
||||
|
||||
put /yourindex/_settings
|
||||
{
|
||||
"number_of_replicas": 1
|
||||
}
|
||||
|
||||
If you do NOT supply a JSON in the first line, you need to hit ENTER
|
||||
twice to complete.`
|
||||
|
||||
func Repl(conf *cfg.Config) error {
|
||||
verbs := []string{"post", "get", "put", "delete"}
|
||||
|
||||
fmt.Println(intro)
|
||||
fmt.Println()
|
||||
|
||||
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], "/") {
|
||||
parts[1] = "/" + parts[1]
|
||||
}
|
||||
|
||||
json := ""
|
||||
if len(parts) == 3 {
|
||||
// put /uri {json}
|
||||
json = parts[2]
|
||||
}
|
||||
|
||||
// put /uri/<Ret> [json]
|
||||
data, err := readJSON(json)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
|
||||
err = CallAPI(conf, parts[0], parts[1], data)
|
||||
if err != nil {
|
||||
fmt.Printf("failed to call API: %s\n", esErrorString(err))
|
||||
}
|
||||
|
||||
reader.SetPrompt("> ")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func encodeAuth(username, password string) string {
|
||||
return base64.StdEncoding.EncodeToString([]byte(username + ":" + password))
|
||||
}
|
||||
|
||||
func CallAPI(conf *cfg.Config, verb, path, data string) error {
|
||||
verb = strings.ToUpper(verb)
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// interactively read arbitrary JSON data from STDIN, which is
|
||||
// virtually a repl inside the primary repl
|
||||
func readJSON(input string) (string, error) {
|
||||
data := ""
|
||||
|
||||
if input != "" {
|
||||
data = input
|
||||
} else {
|
||||
scanner := bufio.NewScanner(os.Stdin)
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
|
||||
if line == "" {
|
||||
break
|
||||
}
|
||||
|
||||
data += line
|
||||
}
|
||||
}
|
||||
|
||||
if data == "" {
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// validate
|
||||
check := map[string]any{}
|
||||
err := json.Unmarshal([]byte(data), &check)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error: input data is not proper JSON: %s", err)
|
||||
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
65
pkg/es/rollover.go
Normal file
65
pkg/es/rollover.go
Normal file
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
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"
|
||||
|
||||
"codeberg.org/scip/esctl/pkg/cfg"
|
||||
"github.com/elastic/go-elasticsearch/v9/typedapi/esdsl"
|
||||
"github.com/elastic/go-elasticsearch/v9/typedapi/indices/rollover"
|
||||
"github.com/elastic/go-elasticsearch/v9/typedapi/types"
|
||||
)
|
||||
|
||||
func RolloverConditions(conf *cfg.Config) types.RolloverConditionsVariant {
|
||||
cond := esdsl.NewRolloverConditions()
|
||||
|
||||
if conf.MaxAge != "" {
|
||||
cond.MaxAge(esdsl.NewDuration().String(conf.MaxAge))
|
||||
}
|
||||
if conf.MaxDocs > 0 {
|
||||
cond.MaxDocs(int64(conf.MaxDocs))
|
||||
}
|
||||
if conf.MaxShardSize > 0 {
|
||||
bs := esdsl.NewByteSize()
|
||||
bs.Int64(int64(conf.MaxShardSize))
|
||||
cond.MaxPrimaryShardSize(bs)
|
||||
}
|
||||
if conf.MaxShardDocs > 0 {
|
||||
cond.MaxPrimaryShardDocs(int64(conf.MaxShardDocs))
|
||||
}
|
||||
|
||||
return cond
|
||||
}
|
||||
|
||||
func RolloverAlias(conf *cfg.Config, alias string) (*rollover.Response, error) {
|
||||
roll := conf.DefaultCluster.ES.Indices.Rollover(alias)
|
||||
|
||||
if conf.Wait {
|
||||
roll.WaitForActiveShards("all")
|
||||
}
|
||||
|
||||
if conf.DryRun {
|
||||
roll.DryRun(true)
|
||||
}
|
||||
|
||||
cond := RolloverConditions(conf)
|
||||
|
||||
roll.Conditions(cond)
|
||||
|
||||
return roll.Do(context.Background())
|
||||
}
|
||||
79
pkg/es/task.go
Normal file
79
pkg/es/task.go
Normal file
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
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"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"codeberg.org/scip/esctl/pkg/cfg"
|
||||
"codeberg.org/scip/esctl/pkg/printer"
|
||||
)
|
||||
|
||||
func TaskList(conf *cfg.Config) error {
|
||||
res, err := conf.DefaultCluster.ES.Cat.Tasks().
|
||||
Header("content-type", "application/json").
|
||||
Header("accept", "application/json").
|
||||
Do(context.Background())
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get index templates: %s", esErrorString(err))
|
||||
}
|
||||
|
||||
slog.Debug("res", "tasks", res)
|
||||
|
||||
table := printer.NewTable(conf, 8, len(res))
|
||||
table.Addheaders("task id", "action", "start time", "run time", "node", "type")
|
||||
for idx, task := range res {
|
||||
var ts time.Time
|
||||
|
||||
epc, err := strconv.Atoi(*task.StartTime)
|
||||
if err != nil {
|
||||
ts = time.Now()
|
||||
} else {
|
||||
ts = time.Unix(int64(epc/1000), 0)
|
||||
}
|
||||
|
||||
table.Entries[idx] = []string{
|
||||
*task.TaskId,
|
||||
*task.Action,
|
||||
ts.Format("2006-01-02T15:04:05"),
|
||||
*task.RunningTime,
|
||||
*task.Node,
|
||||
*task.Type,
|
||||
}
|
||||
}
|
||||
|
||||
return table.Print()
|
||||
}
|
||||
|
||||
func TaskCancel(conf *cfg.Config, taskid string) error {
|
||||
_, err := conf.DefaultCluster.ES.Tasks.Cancel().
|
||||
TaskId(taskid).
|
||||
Header("content-type", "application/json").
|
||||
Header("accept", "application/json").
|
||||
Do(context.Background())
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to cancel task: %s", esErrorString(err))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -292,6 +292,10 @@ func (data *Table) Addheaders(headers ...string) {
|
||||
}
|
||||
}
|
||||
|
||||
func (data *Table) AddRow(fields ...string) {
|
||||
data.Entries = append(data.Entries, fields)
|
||||
}
|
||||
|
||||
// return the length of a string but only visible chars, w/o ansi color escapes
|
||||
func visibleLen(word string) int {
|
||||
return len(ansiCtrlSeq.ReplaceAllLiteralString(word, ""))
|
||||
|
||||
Reference in New Issue
Block a user