Compare commits

...

15 Commits

Author SHA1 Message Date
3abe8b62a2 remove unneccessary fmt.Sprintf's 2026-07-07 06:43:34 +02:00
86f17329b0 satisfy linter 2026-07-07 06:37:21 +02:00
2d8ecae73b conversion from string cells to any cells in printer.Table 2026-07-07 06:30:07 +02:00
033dd0ccd1 add type caster to table, convert cells automatically, users use any 2026-07-07 06:29:40 +02:00
0d81e5c3bc remove markdown output support 2026-07-07 05:29:31 +02:00
92865c0fe2 cosmetics 2026-07-06 14:29:36 +02:00
5475b9986d typo 2026-07-06 14:28:31 +02:00
T. von Dein
82c017144a various fixes (#70)
- dont show ccr status if license insufficient
- add automated tests
- fix config loading
- fix search output
- fix `license show` panics
- fix `ilm show` completion
- print ask passwd prompt to stderr
2026-07-06 14:22:59 +02:00
T. von Dein
a07f38e945 add 'license show', add printer.NewTableEmpty() along .WithHeaders() (#69) 2026-07-03 13:52:36 +02:00
T. von Dein
c197e138ef fix repl output: didn't print json smaller than term height, add debug-http to api repl as well (#68) 2026-07-03 13:30:37 +02:00
T. von Dein
fb6f2389fe add 'cluster settings set' settings completion (#67) 2026-07-03 12:52:13 +02:00
T. von Dein
c9fb572935 get rid of getJsonPath, use flat_settings query flag instead (#66) 2026-07-03 10:25:57 +02:00
T. von Dein
1fc2004474 add external pager support via ES_JSON_PAGER (#65) 2026-07-03 10:07:11 +02:00
T. von Dein
7b576c976c add option -f to usage and filter support (#64) 2026-07-03 08:54:19 +02:00
T. von Dein
b51ee109f2 tune cluster ls: parallized and use tcp connect instead of https (#63) 2026-07-03 08:30:35 +02:00
50 changed files with 2141 additions and 517 deletions

3
.gitignore vendored
View File

@@ -32,3 +32,6 @@ esctl
*.log *.log
cpu.profile cpu.profile
t
single

View File

@@ -62,9 +62,8 @@ install: buildlocal
clean: clean:
rm -rf $(tool) coverage.out testdata t/out pkg/es/openspec.go rm -rf $(tool) coverage.out testdata t/out pkg/es/openspec.go
test: clean test: clean buildlocal
mkdir -p t/out make -C t test
go test ./... $(ARGS)
testlint: test lint testlint: test lint

View File

@@ -23,6 +23,8 @@ Features:
- Shell completion support (bash, zsh and fish). Put this into your - Shell completion support (bash, zsh and fish). Put this into your
rc: `source <(esctl completion bash)`. rc: `source <(esctl completion bash)`.
- Cluster settings can be viewed and modified. - Cluster settings can be viewed and modified.
- Comprehensive cluster status.
- Cluster reroute support.
- Search: you can search indices using full text or by fields, select - Search: you can search indices using full text or by fields, select
logical condition (OR, AND), use PIT, limit datetime (ES date math logical condition (OR, AND), use PIT, limit datetime (ES date math
can be used), etc. It is however not yet possible to create can be used), etc. It is however not yet possible to create
@@ -47,7 +49,9 @@ Features:
elasticsearch API. You can run API calls on the current selected elasticsearch API. You can run API calls on the current selected
cluster w/o the hassle to specify the whole url, credentials etc. It cluster w/o the hassle to specify the whole url, credentials etc. It
has line editing and history support. If `jq` is installed output has line editing and history support. If `jq` is installed output
JSON will be syntax highlighted. JSON will be syntax highlighted. A simple internal pager will be
used if output exceeds the terminal height. You can tweak this using
the `$ES_JSON_PAGER` environment variable (I'd recommend [fx](https://fx.wtf/)).
- Doc support. You can put, delete and show docs for an index. Very - Doc support. You can put, delete and show docs for an index. Very
handy if you want to play with it. Just create a new index: handy if you want to play with it. Just create a new index:
`esctl index create foo` and then insert docs into it for search `esctl index create foo` and then insert docs into it for search
@@ -546,6 +550,8 @@ index - manage indicies
create - create a new index template create - create a new index template
update - update a new index template update - update a new index template
delete - delete an index template delete - delete an index template
license - manage cluster license
show - show details about the cluster license
node - manage nodes node - manage nodes
list - list nodes list - list nodes
show - show details about a node show - show details about a node
@@ -567,7 +573,7 @@ task - manage tasks
version - show esctl version information version - show esctl version information
debug - developer only debug - developer only
help-jsonpath - show jsonpath help help-jsonpath - show jsonpath help
help-command-overview - show overview of all available commands help-usage - show overview of all available commands
``` ```
# Development # Development

View File

@@ -11,3 +11,5 @@
https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-indices-get-data-stream https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-indices-get-data-stream
also exclude data stream backing indices from index ls also exclude data stream backing indices from index ls
- table any2string

View File

@@ -83,6 +83,16 @@ func ApiRepl(conf *cfg.Config) *cli.Command {
Aliases: []string{"shell"}, Aliases: []string{"shell"},
Usage: "interactive API repl", Usage: "interactive API repl",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "pager",
Usage: "external viewer program (default:internal)",
Destination: &conf.Pager,
Aliases: []string{"p"},
Sources: cli.EnvVars("PAGER", "ES_JSON_PAGER"),
},
},
Action: func(ctx context.Context, cmd *cli.Command) error { Action: func(ctx context.Context, cmd *cli.Command) error {
return es.ApiRepl(conf) return es.ApiRepl(conf)
}, },

View File

@@ -104,6 +104,10 @@ func ClusterSettingsSet(conf *cfg.Config) *cli.Command {
}, },
}, },
ShellComplete: func(ctx context.Context, cmd *cli.Command) {
complete(cmd, Cclustersettings)
},
Action: func(ctx context.Context, cmd *cli.Command) error { Action: func(ctx context.Context, cmd *cli.Command) error {
args := cmd.Args() args := cmd.Args()

View File

@@ -33,6 +33,7 @@ const (
Capi Capi
Cilm Cilm
Cnode Cnode
Cclustersettings
) )
func complete(cmd *cli.Command, what int) { func complete(cmd *cli.Command, what int) {
@@ -67,6 +68,8 @@ func complete(cmd *cli.Command, what int) {
list, err = es.IlmNames(conf) list, err = es.IlmNames(conf)
case Cnode: case Cnode:
list, err = es.NodeNames(conf) list, err = es.NodeNames(conf)
case Cclustersettings:
list, err = es.ClusterSettingsNames(conf)
} }
if err != nil { if err != nil {

49
cmd/license.go Normal file
View File

@@ -0,0 +1,49 @@
/*
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 License(conf *cfg.Config) *cli.Command {
return &cli.Command{
Name: "license",
Usage: "manage cluster license",
Commands: []*cli.Command{
LicenseShow(conf),
},
}
}
func LicenseShow(conf *cfg.Config) *cli.Command {
return &cli.Command{
Name: "show",
Aliases: []string{"sh"},
Usage: "show details about the cluster license",
Action: func(ctx context.Context, cmd *cli.Command) error {
return es.LicenseShow(conf)
},
}
}

View File

@@ -89,7 +89,7 @@ func Main() int {
Name: "output", Name: "output",
Aliases: []string{"o"}, Aliases: []string{"o"},
Value: "", Value: "",
Usage: "output mode (tsv, markdown, json, yaml) default: tsv", Usage: "output mode (tsv, json, yaml) default: tsv",
Destination: &conf.Output, Destination: &conf.Output,
}, },
&cli.StringFlag{ &cli.StringFlag{
@@ -108,6 +108,7 @@ func Main() int {
Doc(conf), Doc(conf),
Ilm(conf), Ilm(conf),
Index(conf), Index(conf),
License(conf),
Node(conf), Node(conf),
Roles(conf), Roles(conf),
Search(conf), Search(conf),
@@ -248,9 +249,11 @@ func Debug(conf *cfg.Config) *cli.Command {
func HelpUsage(conf *cfg.Config) *cli.Command { func HelpUsage(conf *cfg.Config) *cli.Command {
return &cli.Command{ return &cli.Command{
Name: "help-command-overview", Name: "help-usage",
Usage: "show overview of all available commands", Usage: "show overview of all available commands",
UsageText: "help-usage [<filter>]",
Aliases: []string{"usage"}, Aliases: []string{"usage"},
CustomHelpTemplate: addReference(`<filter> implies -f`),
Flags: []cli.Flag{ Flags: []cli.Flag{
&cli.BoolFlag{ &cli.BoolFlag{
@@ -259,16 +262,32 @@ func HelpUsage(conf *cfg.Config) *cli.Command {
Destination: &conf.Hidden, Destination: &conf.Hidden,
Aliases: []string{"H"}, Aliases: []string{"H"},
}, },
&cli.BoolFlag{
Name: "full-commands",
Usage: "show full commands",
Destination: &conf.Force,
Aliases: []string{"f"},
},
}, },
Action: func(ctx context.Context, cmd *cli.Command) error { Action: func(ctx context.Context, cmd *cli.Command) error {
maxCommandWidth := 0 maxCommandWidth := 0
filter := cmd.Args().Get(0)
if filter != "" {
conf.Force = true
}
// first pass, determine max command width // first pass, determine max command width
if err := walkVisible(conf, cmd.Root(), func(cmd *cli.Command) error { if err := walkVisible(conf, cmd.Root(), func(cmd *cli.Command) error {
path := cmd.Path() path := cmd.Path()
size := len(path[len(path)-1]) size := len(path[len(path)-1])
if conf.Force {
path := strings.Join(cmd.Path(), " ")
size = len(path)
}
if size > maxCommandWidth { if size > maxCommandWidth {
maxCommandWidth = size maxCommandWidth = size
} }
@@ -283,9 +302,19 @@ func HelpUsage(conf *cfg.Config) *cli.Command {
// second pass, build tree // second pass, build tree
return walkVisible(conf, cmd.Root(), func(cmd *cli.Command) error { return walkVisible(conf, cmd.Root(), func(cmd *cli.Command) error {
path := cmd.Path() path := cmd.Path()
command := path[len(path)-1]
if len(path) == 1 || command == "help" { if filter != "" {
if !strings.Contains(strings.Join(path, " "), filter) {
return nil
}
}
command := path[len(path)-1]
if conf.Force {
command = strings.Join(cmd.Path(), " ")
}
if len(path) == 1 || strings.HasSuffix(command, "help") {
return nil return nil
} }

15
go.mod
View File

@@ -19,6 +19,8 @@ go 1.25.8
require ( require (
github.com/MichaelMure/go-term-markdown v0.1.4 github.com/MichaelMure/go-term-markdown v0.1.4
github.com/alecthomas/repr v0.5.2 github.com/alecthomas/repr v0.5.2
github.com/charmbracelet/bubbles v1.0.0
github.com/charmbracelet/bubbletea v1.3.10
github.com/charmbracelet/lipgloss v1.1.0 github.com/charmbracelet/lipgloss v1.1.0
github.com/chzyer/readline v1.5.1 github.com/chzyer/readline v1.5.1
github.com/dustin/go-humanize v1.0.1 github.com/dustin/go-humanize v1.0.1
@@ -29,10 +31,11 @@ require (
github.com/go-openapi/spec v0.22.5 github.com/go-openapi/spec v0.22.5
github.com/go-openapi/swag/loading v0.26.1 github.com/go-openapi/swag/loading v0.26.1
github.com/mattn/go-isatty v0.0.22 github.com/mattn/go-isatty v0.0.22
github.com/olekukonko/tablewriter v1.1.4 github.com/seeruk/go-wordwrap v0.0.0-20191208221741-14ec4aac9550
github.com/tidwall/gjson v1.19.0 github.com/tidwall/gjson v1.19.0
github.com/tlinden/yadu v0.1.3 github.com/tlinden/yadu v0.1.3
github.com/urfave/cli/v3 v3.10.1-0.20260623012112-f980ca84bf65 github.com/urfave/cli/v3 v3.10.1-0.20260623012112-f980ca84bf65
golang.org/x/term v0.44.0
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
) )
@@ -40,9 +43,6 @@ require (
github.com/MichaelMure/go-term-text v0.3.1 // indirect github.com/MichaelMure/go-term-text v0.3.1 // indirect
github.com/alecthomas/chroma v0.7.1 // indirect github.com/alecthomas/chroma v0.7.1 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/charmbracelet/bubbles v1.0.0 // indirect
github.com/charmbracelet/bubbletea v1.3.10 // indirect
github.com/charmbracelet/colorprofile v0.4.1 // indirect github.com/charmbracelet/colorprofile v0.4.1 // indirect
github.com/charmbracelet/x/ansi v0.11.6 // indirect github.com/charmbracelet/x/ansi v0.11.6 // indirect
github.com/charmbracelet/x/cellbuf v0.0.15 // indirect github.com/charmbracelet/x/cellbuf v0.0.15 // indirect
@@ -69,7 +69,6 @@ require (
github.com/go-openapi/swag/typeutils 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-openapi/swag/yamlutils v0.26.1 // indirect
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
github.com/goccy/go-json v0.10.6 // indirect
github.com/gomarkdown/markdown v0.0.0-20191123064959-2c17d62f5098 // indirect github.com/gomarkdown/markdown v0.0.0-20191123064959-2c17d62f5098 // indirect
github.com/google/uuid v1.6.0 // indirect github.com/google/uuid v1.6.0 // indirect
github.com/kyokomi/emoji/v2 v2.2.8 // indirect github.com/kyokomi/emoji/v2 v2.2.8 // indirect
@@ -81,11 +80,7 @@ require (
github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/termenv v0.16.0 // indirect github.com/muesli/termenv v0.16.0 // indirect
github.com/oklog/ulid/v2 v2.1.1 // 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/rivo/uniseg v0.4.7 // indirect github.com/rivo/uniseg v0.4.7 // indirect
github.com/seeruk/go-wordwrap v0.0.0-20191208221741-14ec4aac9550 // indirect
github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.0 // indirect github.com/tidwall/pretty v1.2.0 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
@@ -94,10 +89,8 @@ require (
go.opentelemetry.io/otel/metric v1.35.0 // indirect go.opentelemetry.io/otel/metric v1.35.0 // indirect
go.opentelemetry.io/otel/trace v1.35.0 // indirect go.opentelemetry.io/otel/trace v1.35.0 // indirect
go.yaml.in/yaml/v3 v3.0.4 // 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/image v0.0.0-20191206065243-da761ea9ff43 // indirect
golang.org/x/net v0.55.0 // indirect golang.org/x/net v0.55.0 // indirect
golang.org/x/sys v0.46.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 golang.org/x/text v0.37.0 // indirect
) )

24
go.sum
View File

@@ -14,28 +14,18 @@ github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs
github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= 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/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/bubbles v1.0.0 h1:12J8/ak/uCZEMQ6KU7pcfwceyjLlWsDLAxB5fXonfvc= github.com/charmbracelet/bubbles v1.0.0 h1:12J8/ak/uCZEMQ6KU7pcfwceyjLlWsDLAxB5fXonfvc=
github.com/charmbracelet/bubbles v1.0.0/go.mod h1:9d/Zd5GdnauMI5ivUIVisuEm3ave1XwXtD1ckyV6r3E= github.com/charmbracelet/bubbles v1.0.0/go.mod h1:9d/Zd5GdnauMI5ivUIVisuEm3ave1XwXtD1ckyV6r3E=
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw= github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4= github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
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/colorprofile v0.4.1 h1:a1lO03qTrSIRaK8c3JRxJDZOvhvIeSco3ej+ngLk1kk= github.com/charmbracelet/colorprofile v0.4.1 h1:a1lO03qTrSIRaK8c3JRxJDZOvhvIeSco3ej+ngLk1kk=
github.com/charmbracelet/colorprofile v0.4.1/go.mod h1:U1d9Dljmdf9DLegaJ0nGZNJvoXAhayhmidOdcBwAvKk= github.com/charmbracelet/colorprofile v0.4.1/go.mod h1:U1d9Dljmdf9DLegaJ0nGZNJvoXAhayhmidOdcBwAvKk=
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= 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/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/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8= github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8=
github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ= github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ=
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/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI= github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI=
github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q= github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q=
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk= github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI= github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM= github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM=
@@ -114,8 +104,6 @@ github.com/go-openapi/testify/v2 v2.5.1 h1:TMdhCaw8fUNraVSf3Omoob1dO/AzBfhtFAPW0
github.com/go-openapi/testify/v2 v2.5.1/go.mod h1:SgsVHtfooshd0tublTtJ50FPKhujf47YRqauXXOUxfw= 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 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= 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/gomarkdown/markdown v0.0.0-20191123064959-2c17d62f5098 h1:Qxs3bNRWe8GTcKMxYOSXm0jx6j0de8XUtb/fsP3GZ0I= 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/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 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
@@ -154,14 +142,6 @@ 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/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 h1:suPZ4ARWLOJLegGFiZZ1dFAkqzhMjL3J1TzI+5wHz8s=
github.com/oklog/ulid/v2 v2.1.1/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ= 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.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/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/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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@@ -189,8 +169,6 @@ github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= 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 h1:5cRCUmj+l5yvlM2irtpFBIJwVV2DPEgYSaWvF19FtcY=
github.com/tlinden/yadu v0.1.3/go.mod h1:l3bRmHKL9zGAR6pnBHY2HRPxBecf7L74BoBgOOpTcUA= github.com/tlinden/yadu v0.1.3/go.mod h1:l3bRmHKL9zGAR6pnBHY2HRPxBecf7L74BoBgOOpTcUA=
github.com/urfave/cli/v3 v3.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/urfave/cli/v3 v3.10.1-0.20260623012112-f980ca84bf65 h1:NXitXXO9DupDLEFj8hIyYtsubRxSFTp5JXqohElT0nU= github.com/urfave/cli/v3 v3.10.1-0.20260623012112-f980ca84bf65 h1:NXitXXO9DupDLEFj8hIyYtsubRxSFTp5JXqohElT0nU=
github.com/urfave/cli/v3 v3.10.1-0.20260623012112-f980ca84bf65/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso= github.com/urfave/cli/v3 v3.10.1-0.20260623012112-f980ca84bf65/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 h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
@@ -223,8 +201,6 @@ golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5h
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
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 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= 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 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc=

View File

@@ -17,10 +17,10 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
package cfg package cfg
import ( import (
"context"
"crypto/tls" "crypto/tls"
"errors" "errors"
"fmt" "fmt"
"net"
"net/http" "net/http"
"os" "os"
"strings" "strings"
@@ -113,7 +113,7 @@ func (cluster *Cluster) CheckAuth() error {
cluster.Pass = pass cluster.Pass = pass
} else { } else {
// k, try interactively // k, try interactively
fmt.Printf("Enter password for elasticsearch user %s@%s: ", cluster.User, cluster.Name) fmt.Fprintf(os.Stderr, "Enter password for elasticsearch user %s@%s: ", cluster.User, cluster.Name)
pass, err := term.ReadPassword(int(syscall.Stdin)) pass, err := term.ReadPassword(int(syscall.Stdin))
if err != nil { if err != nil {
return err return err
@@ -185,42 +185,24 @@ func (conf *Config) SwitchCluster(name string) error {
// We do NOT use go-elasticsearch to check for cluster reachability, // We do NOT use go-elasticsearch to check for cluster reachability,
// because at this stage, auth may not have been configured. So // because at this stage, auth may not have been configured. So
// instead we just connect to the cluster using plan net/http, ignore // instead we just connect to the cluster using plan net/tcp
// HTTP response status and return true if we could just reach ith
func (cluster *Cluster) IsReachable() (bool, error) { func (cluster *Cluster) IsReachable() (bool, error) {
ctx, cancel := context.WithTimeout( timeout := 500 * time.Millisecond
context.Background(),
time.Duration(500)*time.Millisecond)
defer cancel()
req, err := http.NewRequestWithContext( url := strings.TrimPrefix(strings.TrimPrefix(cluster.Uri, "https://"), "http://")
ctx,
"GET",
cluster.Uri,
nil,
)
host := strings.Split(url, "/")
if !strings.Contains(host[0], ":") {
host[0] += ":443"
}
conn, err := net.DialTimeout("tcp", host[0], timeout)
if err != nil { if err != nil {
return false, err return false, err
} }
client := &http.Client{Transport: &http.Transport{ return true, conn.Close()
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}}
resp, err := client.Do(req)
if err != nil {
return false, err
}
if resp != nil {
// at this stage we do not care if the elasticsearch cluster
// accepts our request or if it's misconfigured in some way
return true, nil
}
return false, nil
} }
func (conf *Config) SetupES() error { func (conf *Config) SetupES() error {

View File

@@ -28,7 +28,7 @@ import (
) )
const ( const (
Version string = `v0.0.24` Version string = `v0.0.26`
) )
var ( var (
@@ -106,6 +106,8 @@ type Config struct {
FromNode, ToNode string // cluster reroute move: -f + -t FromNode, ToNode string // cluster reroute move: -f + -t
AllowPrimary, AcceptDataLoss bool // cluster reroute cancel: -p,-a AllowPrimary, AcceptDataLoss bool // cluster reroute cancel: -p,-a
Pager string // api repl: -p || PAGER
} }
func NewConfig() *Config { func NewConfig() *Config {
@@ -119,10 +121,11 @@ func getDefaultPath() string {
func (conf *Config) Init() error { func (conf *Config) Init() error {
DefaultConfig := getDefaultPath() DefaultConfig := getDefaultPath()
switch { if conf.ConfigFile == "" && fileExists(DefaultConfig) {
case fileExists(DefaultConfig):
conf.ConfigFile = DefaultConfig conf.ConfigFile = DefaultConfig
fallthrough }
switch {
case conf.ConfigFile != "": case conf.ConfigFile != "":
if err := conf.LoadConfig(); err != nil { if err := conf.LoadConfig(); err != nil {
return err return err
@@ -137,6 +140,18 @@ func (conf *Config) Init() error {
return err return err
} }
if err := conf.determineDefaultCluster(); err != nil {
return err
}
conf.HaveJQ = isJQinstalled()
conf.PrintDebug()
return nil
}
func (conf *Config) determineDefaultCluster() error {
if conf.CurrentCluster != "" { if conf.CurrentCluster != "" {
// -C specified, set current cluster explicitly, no matter what the config says // -C specified, set current cluster explicitly, no matter what the config says
current, exists := conf.Clusters[conf.CurrentCluster] current, exists := conf.Clusters[conf.CurrentCluster]
@@ -172,10 +187,6 @@ func (conf *Config) Init() error {
} }
} }
conf.HaveJQ = isJQinstalled()
conf.PrintDebug()
return nil return nil
} }

View File

@@ -36,3 +36,14 @@ func GetTermWidth() int {
return 80 return 80
} }
func GetTermHeight() int {
if term.IsTerminal(int(os.Stdout.Fd())) {
_, height, err := term.GetSize(int(os.Stdout.Fd()))
if err == nil {
return height
}
}
return 25
}

View File

@@ -141,14 +141,54 @@ func ApiRepl(conf *cfg.Config) error {
fmt.Printf("failed to call API: %s\n", esErrorString(err)) fmt.Printf("failed to call API: %s\n", esErrorString(err))
} }
if err := prettyfiJson(conf, raw); err != nil { pageJsonOutput(conf, raw)
fmt.Println(err)
}
} }
return nil return nil
} }
func pageJsonOutput(conf *cfg.Config, raw []byte) {
tmpconf := &cfg.Config{HaveJQ: conf.HaveJQ}
if conf.Pager != "" {
tmpconf.HaveJQ = false
}
output, err := prettyfiJson(tmpconf, raw)
if err != nil {
fmt.Println(err)
}
lines := len(strings.Split(output, "\n"))
height := cfg.GetTermHeight()
if lines > height {
if conf.Pager != "" {
cmd := strings.Split(conf.Pager, " ")
pager := exec.Command(cmd[0], cmd[1:]...)
var buf bytes.Buffer
buf.WriteString(output)
pager.Stdout = os.Stdout
pager.Stdin = &buf
pager.Stderr = os.Stderr
err := pager.Run()
if err != nil {
fmt.Printf("failed to execute pager '%s': %s", conf.Pager, err)
}
} else {
printer.Pager("json output", output)
}
return
}
fmt.Println(output)
}
func encodeAuth(username, password string) string { func encodeAuth(username, password string) string {
return base64.StdEncoding.EncodeToString([]byte(username + ":" + password)) return base64.StdEncoding.EncodeToString([]byte(username + ":" + password))
} }
@@ -163,6 +203,12 @@ func CallAPI(conf *cfg.Config, verb, path, data string) ([]byte, error) {
client := &http.Client{Transport: tr} client := &http.Client{Transport: tr}
if conf.DebugHTTP {
client = &http.Client{
Transport: &cfg.DebugTransport{
Transport: tr}}
}
req, err := http.NewRequest(verb, conf.DefaultCluster.Uri+path, bytes.NewBuffer([]byte(data))) req, err := http.NewRequest(verb, conf.DefaultCluster.Uri+path, bytes.NewBuffer([]byte(data)))
if err != nil { if err != nil {
return nil, err return nil, err
@@ -197,7 +243,7 @@ func CallAPI(conf *cfg.Config, verb, path, data string) ([]byte, error) {
return body, nil return body, nil
} }
func prettyfiJson(conf *cfg.Config, raw []byte) error { func prettyfiJson(conf *cfg.Config, raw []byte) (string, error) {
if conf.HaveJQ { if conf.HaveJQ {
cmd := exec.CommandContext(context.Background(), "jq", "-C") cmd := exec.CommandContext(context.Background(), "jq", "-C")
cmd.Stdin = bytes.NewReader(raw) cmd.Stdin = bytes.NewReader(raw)
@@ -207,23 +253,18 @@ func prettyfiJson(conf *cfg.Config, raw []byte) error {
err := cmd.Run() err := cmd.Run()
if err != nil { if err != nil {
return err return "", err
} }
fmt.Println(out.String()) return out.String(), nil
return nil
} }
var pretty bytes.Buffer var pretty bytes.Buffer
err := json.Indent(&pretty, raw, "", "\t") err := json.Indent(&pretty, raw, "", "\t")
if err != nil { if err != nil {
return fmt.Errorf("json parse error: %s", err) return "", fmt.Errorf("json parse error: %s", err)
} }
fmt.Println(pretty.String()) return pretty.String(), nil
return nil
} }
// interactively read arbitrary JSON data from STDIN, which is // interactively read arbitrary JSON data from STDIN, which is

View File

@@ -102,7 +102,7 @@ func CcrRemoteInfo(conf *cfg.Config, index string) error {
} }
if remote == "" { if remote == "" {
return fmt.Errorf("cluster doesn't follow any other: %s", err) return errors.New("cluster doesn't follow any other")
} }
mode := "follower" mode := "follower"
@@ -113,11 +113,11 @@ func CcrRemoteInfo(conf *cfg.Config, index string) error {
table := printer.NewTable(conf, 2, 5) table := printer.NewTable(conf, 2, 5)
table.Addheaders("ccr remote property", "value") table.Addheaders("ccr remote property", "value")
table.Entries = [][]string{ table.Entries = [][]any{
{"Remote Cluster", remote}, {"Remote Cluster", remote},
{"CCR Mode", mode}, {"CCR Mode", mode},
{"Connected", fmt.Sprintf("%t", info.Connected)}, {"Connected", info.Connected},
{"Num Proxy Sockets Connected", fmt.Sprintf("%d", info.NumProxySocketsConnected)}, {"Num Proxy Sockets Connected", info.NumProxySocketsConnected},
{"Proxy Address", info.ProxyAddress}, {"Proxy Address", info.ProxyAddress},
} }

View File

@@ -164,16 +164,16 @@ func CcrFollowerShow(conf *cfg.Config, index string) error {
table := printer.NewTable(conf, 2, 9) table := printer.NewTable(conf, 2, 9)
table.Addheaders("ccr follower property", "value") table.Addheaders("ccr follower property", "value")
table.Entries = [][]string{ table.Entries = [][]any{
{"name", index}, {"name", index},
{"remote_cluster", follower.RemoteCluster}, {"remote_cluster", follower.RemoteCluster},
{"leader_checkpoint", fmt.Sprintf("%d", follower.LeaderGlobalCheckpoint)}, {"leader_checkpoint", follower.LeaderGlobalCheckpoint},
{"follower_checkpoint", fmt.Sprintf("%d", follower.FollowerGlobalCheckpoint)}, {"follower_checkpoint", follower.FollowerGlobalCheckpoint},
{"bytes_read", fmt.Sprintf("%d", follower.BytesRead)}, {"bytes_read", follower.BytesRead},
{"failed_read_requests", fmt.Sprintf("%d", follower.FailedReadRequests)}, {"failed_read_requests", follower.FailedReadRequests},
{"failed_write_requests", fmt.Sprintf("%d", follower.FailedWriteRequests)}, {"failed_write_requests", follower.FailedWriteRequests},
{"successful_read_requests", fmt.Sprintf("%d", follower.SuccessfulReadRequests)}, {"successful_read_requests", follower.SuccessfulReadRequests},
{"successful_write_requests", fmt.Sprintf("%d", follower.SuccessfulWriteRequests)}, {"successful_write_requests", follower.SuccessfulWriteRequests},
} }
if err := table.Print(); err != nil { if err := table.Print(); err != nil {

View File

@@ -17,6 +17,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
package es package es
import ( import (
"errors"
"fmt" "fmt"
"log/slog" "log/slog"
"strings" "strings"
@@ -31,44 +32,62 @@ import (
type ClusterIndices map[string]map[string]*types.IndicesRecord type ClusterIndices map[string]map[string]*types.IndicesRecord
func ClusterList(conf *cfg.Config) error { type clusterReachable struct {
table := printer.NewTable(conf, 5, len(conf.Clusters)) reachable bool
err error
}
func ClusterList(conf *cfg.Config) error {
var mu sync.Mutex
var wg sync.WaitGroup
reachable := make(map[string]clusterReachable, len(conf.Clusters))
// check endpoints in parallel to speed things up
for name, cluster := range conf.Clusters {
wg.Add(1)
go func() {
defer wg.Done()
online, err := cluster.IsReachable()
mu.Lock()
reachable[name] = clusterReachable{reachable: online, err: err}
mu.Unlock()
}()
}
wg.Wait()
table := printer.NewTable(conf, 5, len(conf.Clusters))
table.Addheaders("cluster", "uri", "reachable", "current", "error") table.Addheaders("cluster", "uri", "reachable", "current", "error")
idx := 0 idx := 0
for name, cluster := range conf.Clusters { for name, cluster := range conf.Clusters {
reachable := "no" reachableStr := "no"
current := "no" current := "no"
errmsg := "" errmsg := ""
online, err := cluster.IsReachable() if reachable[name].reachable {
reachableStr = printer.Colorize(conf, "green", "reachable")
if online {
reachable = printer.Colorize(conf, "green", "reachable")
} }
if cluster.Default { if cluster.Default {
current = printer.Colorize(conf, "green", "yes") current = printer.Colorize(conf, "green", "yes")
if !online { if !reachable[name].reachable {
reachable = printer.Colorize(conf, "red", "no") reachableStr = printer.Colorize(conf, "red", "no")
errmsg = err.Error() errmsg = reachable[name].err.Error()
} }
} }
table.Entries[idx] = []string{name, cluster.Uri, reachable, current, errmsg} table.Entries[idx] = []any{name, cluster.Uri, reachableStr, current, errmsg}
idx++ idx++
} }
table.Sort() table.Sort()
if err := table.Print(); err != nil { return table.Print()
return err
}
return nil
} }
// We're using goroutines here to parallelize API requests, since we // We're using goroutines here to parallelize API requests, since we
@@ -100,12 +119,12 @@ func getClusterStatus(conf *cfg.Config) (*apiResponse, error) {
all := apiResponse{} all := apiResponse{}
var err error
for i := 0; i < gocount; i++ { for i := 0; i < gocount; i++ {
r := <-responses r := <-responses
if r.error != nil { err = errors.Join(err, r.error)
return nil, r.error
}
switch r.which { switch r.which {
case ResponseHealth: case ResponseHealth:
@@ -125,20 +144,23 @@ func getClusterStatus(conf *cfg.Config) (*apiResponse, error) {
} }
} }
return &all, nil return &all, err
} }
func ClusterStatus(conf *cfg.Config) error { func ClusterStatus(conf *cfg.Config) error {
res, err := getClusterStatus(conf) res, err := getClusterStatus(conf)
if err != nil { if err != nil && !strings.Contains(err.Error(), "current license is non-compliant") {
return err return err
} }
slog.Debug("ES result", "cluster health", res.health) slog.Debug("ES result", "cluster health", res.health)
isleader := len(res.ccr.AutoFollowStats.AutoFollowedClusters) == 0 var isleader bool
var ccrfollowing string
if res.ccr != nil {
isleader = len(res.ccr.AutoFollowStats.AutoFollowedClusters) == 0
ccrfollowing := ""
if len(res.ccr.AutoFollowStats.AutoFollowedClusters) > 0 { if len(res.ccr.AutoFollowStats.AutoFollowedClusters) > 0 {
// is following another cluster // is following another cluster
ccrfollowing = fmt.Sprintf("%s (%d/%d)", ccrfollowing = fmt.Sprintf("%s (%d/%d)",
@@ -147,6 +169,7 @@ func ClusterStatus(conf *cfg.Config) error {
res.ccr.AutoFollowStats.NumberOfFailedFollowIndices, res.ccr.AutoFollowStats.NumberOfFailedFollowIndices,
) )
} }
}
// look for red indices, if any // look for red indices, if any
redindices := 0 redindices := 0
@@ -167,25 +190,25 @@ func ClusterStatus(conf *cfg.Config) error {
table := printer.NewTable(conf, 2, 7) table := printer.NewTable(conf, 2, 7)
table.Addheaders(conf.DefaultCluster.Name, "status") table.Addheaders(conf.DefaultCluster.Name, "status")
table.Entries = [][]string{ table.Entries = [][]any{
{"Cluster Name", res.health.ClusterName}, {"Cluster Name", res.health.ClusterName},
{"ES Status", printer.Colorize(conf, res.health.Status.Name, res.health.Status.Name)}, {"ES Status", printer.Colorize(conf, res.health.Status.Name, res.health.Status.Name)},
{"ES Version", res.info.Version.Int}, {"ES Version", res.info.Version.Int},
{"Is Leader", fmt.Sprintf("%t", isleader)}, {"Is Leader", isleader},
{"Active Shards", fmt.Sprintf("%d", res.health.ActiveShards)}, {"Active Shards", res.health.ActiveShards},
{"Active Primary Shards", fmt.Sprintf("%d", res.health.ActivePrimaryShards)}, {"Active Primary Shards", res.health.ActivePrimaryShards},
{"Unassigned Shards", fmt.Sprintf("%d", res.health.UnassignedShards)}, {"Unassigned Shards", res.health.UnassignedShards},
{"Unassigned Primary Shards", fmt.Sprintf("%d", res.health.UnassignedPrimaryShards)}, {"Unassigned Primary Shards", res.health.UnassignedPrimaryShards},
{"Pending Tasks", fmt.Sprintf("%d", res.health.NumberOfPendingTasks)}, {"Pending Tasks", res.health.NumberOfPendingTasks},
{"Nodes", fmt.Sprintf("%d", res.health.NumberOfNodes)}, {"Nodes", res.health.NumberOfNodes},
{"Red Indices", fmt.Sprintf("%d", redindices)}, {"Red Indices", redindices},
{"Long Running Tasks", fmt.Sprintf("%d", longtasks)}, {"Long Running Tasks", longtasks},
} }
if !isleader { if !isleader && res.ccr != nil {
table.Entries = append(table.Entries, [][]string{ table.Entries = append(table.Entries, [][]any{
{"AutoFollow (success/failed indices)", ccrfollowing}, {"AutoFollow (success/failed indices)", ccrfollowing},
{"Followed Indices", fmt.Sprintf("%d", len(res.ccr.FollowStats.Indices))}, {"Followed Indices", len(res.ccr.FollowStats.Indices)},
}...) }...)
} }
@@ -196,15 +219,15 @@ func ClusterStatus(conf *cfg.Config) error {
if res.health.Status.Name != "green" { if res.health.Status.Name != "green" {
for name, indicator := range res.healthreport.Indicators { for name, indicator := range res.healthreport.Indicators {
if indicator.Status != "green" { if indicator.Status != "green" {
table.Entries = append(table.Entries, []string{ table.Entries = append(table.Entries, []any{
printer.Colorize(conf, indicator.Status, "Bad health "+name), indicator.Symptom, printer.Colorize(conf, indicator.Status, "Bad health "+name), indicator.Symptom,
}) })
for _, diag := range indicator.Diagnosis { for _, diag := range indicator.Diagnosis {
table.Entries = append(table.Entries, []string{" -> cause", diag.Cause}) table.Entries = append(table.Entries, []any{" -> cause", diag.Cause})
for resource, items := range diag.AffectedResources { for resource, items := range diag.AffectedResources {
table.Entries = append(table.Entries, []string{" -> affected " + resource, strings.Join(items, ",")}) table.Entries = append(table.Entries, []any{" -> affected " + resource, strings.Join(items, ",")})
} }
} }
} }
@@ -232,13 +255,13 @@ func gatherClusterStats(conf *cfg.Config, clusterstats *clusterstats.Response, t
clusterstats.Nodes.Jvm.Versions[0].VmVersion}, " ") clusterstats.Nodes.Jvm.Versions[0].VmVersion}, " ")
} }
table.Entries = append(table.Entries, [][]string{ table.Entries = append(table.Entries, [][]any{
{"Indicies", fmt.Sprintf("%d", clusterstats.Indices.Count)}, {"Indicies", clusterstats.Indices.Count},
{"Docs", fmt.Sprintf("%d", clusterstats.Indices.Docs.Count)}, {"Docs", clusterstats.Indices.Docs.Count},
{"Total Size", humanize.Bytes(uint64(clusterstats.Indices.Docs.TotalSizeInBytes))}, {"Total Size", printer.Bytes(clusterstats.Indices.Docs.TotalSizeInBytes)},
{"Total Queries", fmt.Sprintf("%d", querycount)}, {"Total Queries", "%d", querycount},
{"Shards Primaries", fmt.Sprintf("%d", clusterstats.Indices.Shards.Primaries)}, {"Shards Primaries", clusterstats.Indices.Shards.Primaries},
{"Shards Total", fmt.Sprintf("%d", clusterstats.Indices.Shards.Total)}, {"Shards Total", clusterstats.Indices.Shards.Total},
{"Storage", fmt.Sprintf( {"Storage", fmt.Sprintf(
"%s/%s", "%s/%s",
humanize.Bytes(uint64(clusterstats.Indices.Store.SizeInBytes)), humanize.Bytes(uint64(clusterstats.Indices.Store.SizeInBytes)),
@@ -249,11 +272,11 @@ func gatherClusterStats(conf *cfg.Config, clusterstats *clusterstats.Response, t
humanize.Bytes(uint64(clusterstats.Nodes.Jvm.Mem.HeapUsedInBytes)), humanize.Bytes(uint64(clusterstats.Nodes.Jvm.Mem.HeapUsedInBytes)),
humanize.Bytes(uint64(clusterstats.Nodes.Jvm.Mem.HeapMaxInBytes)), humanize.Bytes(uint64(clusterstats.Nodes.Jvm.Mem.HeapMaxInBytes)),
)}, )},
{"JVM Threads", fmt.Sprintf("%d", clusterstats.Nodes.Jvm.Threads)}, {"JVM Threads", clusterstats.Nodes.Jvm.Threads},
{"JVM Version", vmversion}, {"JVM Version", vmversion},
{"CPUs", fmt.Sprintf("%d", clusterstats.Nodes.Os.AllocatedProcessors)}, {"CPUs", clusterstats.Nodes.Os.AllocatedProcessors},
{"CPU Usage", fmt.Sprintf("%d%%", clusterstats.Nodes.Process.Cpu.Percent)}, {"CPU Usage", fmt.Sprintf("%d%%", clusterstats.Nodes.Process.Cpu.Percent)},
{"Open FDs", fmt.Sprintf("%d", clusterstats.Nodes.Process.OpenFileDescriptors.Avg)}, {"Open FDs", clusterstats.Nodes.Process.OpenFileDescriptors.Avg},
}...) }...)
return table return table

View File

@@ -20,15 +20,19 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"log/slog"
"codeberg.org/scip/esctl/pkg/cfg" "codeberg.org/scip/esctl/pkg/cfg"
"codeberg.org/scip/esctl/pkg/printer" "codeberg.org/scip/esctl/pkg/printer"
"github.com/urfave/cli/v3" "github.com/urfave/cli/v3"
) )
func ClusterSettingsNames(conf *cfg.Config) ([]string, error) {
return validClusterSettings, nil
}
func ClusterSettingsList(conf *cfg.Config) error { func ClusterSettingsList(conf *cfg.Config) error {
res, err := conf.DefaultCluster.ES().Cluster.GetSettings(). res, err := conf.DefaultCluster.ES().Cluster.GetSettings().
FlatSettings(true).
Do(context.Background()) Do(context.Background())
if err != nil { if err != nil {
return fmt.Errorf("failed to get cluster settings: %s", esErrorString(err)) return fmt.Errorf("failed to get cluster settings: %s", esErrorString(err))
@@ -36,7 +40,7 @@ func ClusterSettingsList(conf *cfg.Config) error {
table := printer.NewTable(conf, 2, 0) table := printer.NewTable(conf, 2, 0)
table.Addheaders("setting", "value") table.Addheaders("setting", "value")
entries := [][]string{} entries := [][]any{}
settingshash := res.Persistent // == map[string]json.RawMessage settingshash := res.Persistent // == map[string]json.RawMessage
@@ -48,19 +52,7 @@ func ClusterSettingsList(conf *cfg.Config) error {
} }
for topic, val := range settingshash { for topic, val := range settingshash {
data := map[string]any{} entries = append(entries, []any{topic, string(val)})
err := json.Unmarshal(val, &data)
if err != nil {
return fmt.Errorf("failed to unmarshall setting for topic %s: %s", topic, err)
}
paths := getJsonPath(map[string]string{}, data, topic)
slog.Debug("settings", topic, paths)
for setting, value := range paths {
entries = append(entries, []string{setting, fmt.Sprintf("%v", value)})
}
} }
table.Entries = entries table.Entries = entries

View File

@@ -0,0 +1,914 @@
/*
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
// manually extracted from https://www.elastic.co/docs/reference/elasticsearch/configuration-reference
var validClusterSettings []string = []string{
"xpack.security.audit.enabled",
"xpack.security.audit.logfile.events.include",
"xpack.security.audit.logfile.events.exclude",
"xpack.security.audit.logfile.events.emit_request_body",
"xpack.security.audit.logfile.emit_node_name",
"xpack.security.audit.logfile.emit_node_host_address",
"xpack.security.audit.logfile.emit_node_host_name",
"xpack.security.audit.logfile.emit_node_id",
"indices.breaker.total.use_real_memory",
"indices.breaker.total.limit",
"indices.breaker.fielddata.limit",
"indices.breaker.fielddata.overhead",
"indices.breaker.request.limit",
"indices.breaker.request.overhead",
"network.breaker.inflight_requests.limit",
"network.breaker.inflight_requests.overhead",
"script.max_compilations_rate",
"script.painless.regex.enabled",
"breaker.eql_sequence.limit",
"breaker.eql_sequence.overhead",
"breaker.eql_sequence.type",
"breaker.model_inference.limit",
"breaker.model_inference.overhead",
"breaker.model_inference.type",
"cluster.routing.allocation.enable",
"cluster.routing.allocation.same_shard.host",
"cluster.routing.allocation.total_shards_per_node",
"cluster.routing.allocation.node_concurrent_incoming_recoveries",
"cluster.routing.allocation.node_concurrent_outgoing_recoveries",
"cluster.routing.allocation.node_concurrent_recoveries",
"cluster.routing.allocation.node_initial_primaries_recoveries",
"cluster.routing.allocation.allow_rebalance",
"cluster.routing.rebalance.enable",
"cluster.routing.allocation.cluster_concurrent_rebalance",
"cluster.routing.allocation.type",
"cluster.routing.allocation.balance.threshold",
"cluster.routing.allocation.balance.shard",
"cluster.routing.allocation.balance.index",
"cluster.routing.allocation.balance.disk_usage",
"cluster.routing.allocation.balance.write_load",
"cluster.routing.allocation.disk.threshold_enabled",
"cluster.routing.allocation.disk.watermark.low",
"cluster.routing.allocation.disk.watermark.low.max_headroom",
"cluster.routing.allocation.disk.watermark.high",
"cluster.routing.allocation.disk.watermark.high.max_headroom",
"cluster.routing.allocation.disk.watermark.enable_for_single_data_node",
"cluster.routing.allocation.disk.watermark.flood_stage",
"cluster.routing.allocation.disk.watermark.flood_stage.max_headroom",
"cluster.routing.allocation.disk.watermark.flood_stage.frozen",
"cluster.routing.allocation.disk.watermark.flood_stage.frozen.max_headroom",
"cluster.info.update.interval",
"cluster.routing.allocation.awareness.attributes",
"cluster.routing.allocation.awareness.force.*",
"cluster.routing.allocation.include.{{attribute}}",
"cluster.routing.allocation.require.{{attribute}}",
"cluster.routing.allocation.exclude.{{attribute}}",
"cluster.routing.allocation.stats.cache.ttl",
"ccr.indices.recovery.max_bytes_per_sec",
"ccr.indices.recovery.max_concurrent_file_chunks",
"ccr.indices.recovery.chunk_size",
"ccr.indices.recovery.recovery_activity_timeout",
"ccr.indices.recovery.internal_action_timeout",
"data_streams.lifecycle.retention.max",
"data_streams.lifecycle.retention.default",
"data_streams.lifecycle.poll_interval",
"cluster.lifecycle.default.rollover",
"data_streams.lifecycle.target.merge.policy.merge_factor",
"data_streams.lifecycle.target.merge.policy.floor_segment",
"data_streams.lifecycle.signalling.error_retry_interval",
"data_streams.lifecycle.downsampling.max_indices_in_progress",
"dlm.frozen.transition.poll_interval",
"dlm.frozen.transition.thread_pool.size",
"dlm.frozen.transition.thread_pool.queue_size",
"dlm.frozen.cleanup.poll_interval",
"index.lifecycle.prefer_ilm",
"index.lifecycle.origination_date",
"index.dlm.frozen.created",
"discovery.seed_hosts",
"discovery.seed_providers",
"discovery.type",
"cluster.initial_master_nodes",
"discovery.cluster_formation_warning_timeout",
"discovery.find_peers_interval",
"discovery.probe.connect_timeout",
"discovery.probe.handshake_timeout",
"discovery.request_peers_timeout",
"discovery.find_peers_warning_timeout",
"discovery.seed_resolver.max_concurrent_resolvers",
"discovery.seed_resolver.timeout",
"cluster.auto_shrink_voting_configuration",
"cluster.election.duration",
"cluster.election.initial_timeout",
"cluster.election.max_timeout",
"cluster.fault_detection.follower_check.timeout",
"cluster.fault_detection.follower_check.retry_count",
"cluster.fault_detection.leader_check.interval",
"cluster.fault_detection.leader_check.timeout",
"cluster.fault_detection.leader_check.retry_count",
"cluster.follower_lag.timeout",
"cluster.max_voting_config_exclusions",
"cluster.publish.info_timeout",
"cluster.publish.timeout",
"cluster.discovery_configuration_check.interval",
"cluster.join_validation.cache_timeout",
"cluster.no_master_block",
"monitor.fs.health.enabled",
"monitor.fs.health.refresh_interval",
"monitor.fs.health.slow_path_logging_threshold",
"enrich.cache_size",
"enrich.coordinator_proxy.max_concurrent_requests",
"enrich.coordinator_proxy.max_lookups_per_request",
"enrich.coordinator_proxy.queue_capacity",
"enrich.fetch_size",
"enrich.max_force_merge_attempts",
"enrich.cleanup_period",
"enrich.max_concurrent_policy_executions",
"indices.fielddata.cache.size",
"health.master_history.has_master_lookup_timeframe",
"master_history.max_age",
"health.master_history.identity_changes_threshold",
"health.master_history.no_master_transitions_threshold",
"health.node.enabled",
"health.reporting.local.monitor.interval",
"health.ilm.max_time_on_action",
"health.ilm.max_time_on_step",
"health.ilm.max_retries_per_step",
"health.periodic_logger.enabled",
"health.periodic_logger.poll_interval",
"health.shard_capacity.unhealthy_threshold.yellow",
"health.shard_capacity.unhealthy_threshold.red",
"health.master_history.has_master_lookup_timeframe",
"master_history.max_age",
"health.master_history.identity_changes_threshold",
"health.master_history.no_master_transitions_threshold",
"health.node.enabled",
"health.reporting.local.monitor.interval",
"health.ilm.max_time_on_action",
"health.ilm.max_time_on_step",
"health.ilm.max_retries_per_step",
"health.periodic_logger.enabled",
"health.periodic_logger.poll_interval",
"health.shard_capacity.unhealthy_threshold.yellow",
"health.shard_capacity.unhealthy_threshold.red",
"indices.memory.index_buffer_size",
"indices.memory.min_index_buffer_size",
"indices.memory.max_index_buffer_size",
"indexing_pressure.memory.limit",
"xpack.ilm.enabled",
"indices.lifecycle.history_index_enabled",
"indices.lifecycle.poll_interval",
"indices.lifecycle.rollover.only_if_has_documents",
"index.lifecycle.indexing_complete",
"index.lifecycle.name",
"index.lifecycle.origination_date",
"index.lifecycle.parse_origination_date",
"index.lifecycle.step.wait_time_threshold",
"index.lifecycle.rollover_alias",
"action.auto_create_index",
"action.destructive_requires_name",
"cluster.indices.close.enable",
"stack.templates.enabled",
"xpack.profiling.enabled",
"xpack.profiling.templates.enabled",
"xpack.otel_data.registry.enabled",
"xpack.otel_data.histogram_field_type",
"reindex.remote.whitelist",
"reindex.remote.blocklist",
"cluster.reindex.pit.keep_alive",
"reindex.ssl.certificate",
"reindex.ssl.certificate_authorities",
"reindex.ssl.key",
"reindex.ssl.key_passphrase",
"reindex.ssl.keystore.key_password",
"reindex.ssl.keystore.password",
"reindex.ssl.keystore.path",
"reindex.ssl.keystore.type",
"reindex.ssl.secure_key_passphrase",
"reindex.ssl.keystore.secure_key_password",
"reindex.ssl.keystore.secure_password",
"reindex.ssl.truststore.password",
"reindex.ssl.truststore.path",
"reindex.ssl.truststore.secure_password",
"reindex.ssl.truststore.type",
"reindex.ssl.verification_mode",
"indices.recovery.max_bytes_per_sec",
"indices.recovery.max_concurrent_file_chunks",
"indices.recovery.max_concurrent_operations",
"indices.recovery.use_snapshots",
"indices.recovery.max_concurrent_snapshot_file_downloads",
"indices.recovery.max_concurrent_snapshot_file_downloads_per_node",
"node.bandwidth.recovery.disk.read",
"node.bandwidth.recovery.disk.write",
"node.bandwidth.recovery.network",
"node.bandwidth.recovery.factor.read",
"node.bandwidth.recovery.factor.write",
"node.bandwidth.recovery.operator.factor.read",
"node.bandwidth.recovery.operator.factor.write",
"node.bandwidth.recovery.operator.factor",
"node.bandwidth.recovery.operator.factor.max_overcommit",
"xpack.inference.query_timeout",
"xpack.inference.logging.reset_interval",
"xpack.inference.logging.wait_duration",
"xpack.inference.http.max_response_size",
"xpack.inference.http.max_total_connections",
"xpack.inference.http.max_route_connections",
"xpack.inference.http.connection_eviction_interval",
"xpack.inference.http.connection_eviction_max_idle_time",
"xpack.inference.http.request_executor.queue_capacity",
"xpack.inference.http.retry.initial_delay",
"xpack.inference.http.retry.max_delay_bound",
"xpack.inference.http.retry.timeout",
"xpack.inference.truncator.reduction_percentage",
"xpack.inference.endpoint.cache.enabled",
"xpack.inference.endpoint.cache.weight",
"xpack.inference.endpoint.cache.expiry_time",
"xpack.inference.oauth2.token_cache.enabled",
"xpack.inference.oauth2.token_cache.weight",
"xpack.inference.oauth2.token_cache.expiry_time",
"xpack.inference.ccm.cache.weight",
"xpack.inference.ccm.cache.expiry_time",
"xpack.license.self_generated.type",
"gateway.expected_data_nodes",
"gateway.recover_after_time",
"gateway.recover_after_data_nodes",
"node.roles: [ ml ]",
"xpack.ml.enabled",
"xpack.ml.inference_model.cache_size",
"xpack.ml.inference_model.time_to_live",
"xpack.ml.max_inference_processors",
"xpack.ml.max_machine_memory_percent",
"xpack.ml.max_model_memory_limit",
"xpack.ml.max_open_jobs",
"xpack.ml.nightly_maintenance_requests_per_second",
"xpack.ml.results_index_rollover_max_size",
"xpack.ml.anomalies.heal_reindexed_v7.enabled",
"xpack.ml.idle_job_auto_close_timeout",
"xpack.ml.node_concurrent_job_allocations",
"xpack.ml.enable_config_migration",
"xpack.ml.max_anomaly_records",
"xpack.ml.max_lazy_ml_nodes",
"xpack.ml.max_ml_node_size",
"xpack.ml.trained_models.graph_validation_enabled",
"xpack.ml.model_repository",
"xpack.ml.persist_results_max_retries",
"xpack.ml.process_connect_timeout",
"xpack.ml.use_auto_machine_memory_percent",
"xpack.monitoring.enabled",
"xpack.monitoring.collection.enabled",
"xpack.monitoring.collection.interval",
"xpack.monitoring.elasticsearch.collection.enabled",
"xpack.monitoring.collection.cluster.stats.timeout",
"xpack.monitoring.collection.node.stats.timeout",
"xpack.monitoring.collection.indices",
"xpack.monitoring.collection.index.stats.timeout",
"xpack.monitoring.collection.index.recovery.active_only",
"xpack.monitoring.collection.index.recovery.timeout",
"xpack.monitoring.history.duration",
"xpack.monitoring.exporters",
"cluster_alerts.management.enabled",
"wait_master.timeout",
"auth.username",
"auth.secure_password",
"connection.timeout",
"connection.read_timeout",
"proxy.base_path",
"index.name.time_format",
"cluster_alerts.management.enabled",
"cluster_alerts.management.blacklist",
"xpack.monitoring.exporters.$NAME.ssl.supported_protocols",
"xpack.monitoring.exporters.$NAME.ssl.verification_mode",
"xpack.monitoring.exporters.$NAME.ssl.cipher_suites",
"xpack.monitoring.exporters.$NAME.ssl.key",
"xpack.monitoring.exporters.$NAME.ssl.key_passphrase",
"xpack.monitoring.exporters.$NAME.ssl.secure_key_passphrase",
"xpack.monitoring.exporters.$NAME.ssl.certificate",
"xpack.monitoring.exporters.$NAME.ssl.certificate_authorities",
"xpack.monitoring.exporters.$NAME.ssl.keystore.path",
"xpack.monitoring.exporters.$NAME.ssl.keystore.password",
"xpack.monitoring.exporters.$NAME.ssl.keystore.secure_password",
"xpack.monitoring.exporters.$NAME.ssl.keystore.key_password",
"xpack.monitoring.exporters.$NAME.ssl.keystore.secure_key_password",
"xpack.monitoring.exporters.$NAME.ssl.truststore.path",
"xpack.monitoring.exporters.$NAME.ssl.truststore.password",
"xpack.monitoring.exporters.$NAME.ssl.truststore.secure_password",
"xpack.monitoring.exporters.$NAME.ssl.keystore.path",
"xpack.monitoring.exporters.$NAME.ssl.keystore.type",
"xpack.monitoring.exporters.$NAME.ssl.keystore.password",
"xpack.monitoring.exporters.$NAME.ssl.keystore.secure_password",
"xpack.monitoring.exporters.$NAME.ssl.keystore.key_password",
"xpack.monitoring.exporters.$NAME.ssl.keystore.secure_key_password",
"xpack.monitoring.exporters.$NAME.ssl.truststore.path",
"xpack.monitoring.exporters.$NAME.ssl.truststore.type",
"xpack.monitoring.exporters.$NAME.ssl.truststore.password",
"xpack.monitoring.exporters.$NAME.ssl.truststore.secure_password",
"network.host",
"http.port",
"transport.port",
"remote_cluster.port",
"0.0.0.0",
"network.bind_host",
"network.publish_host",
"network.tcp.keep_alive",
"network.tcp.keep_idle",
"network.tcp.keep_interval",
"network.tcp.keep_count",
"network.tcp.no_delay",
"network.tcp.reuse_address",
"network.tcp.send_buffer_size",
"network.tcp.receive_buffer_size",
"http.host",
"http.bind_host",
"http.publish_host",
"http.publish_port",
"http.max_content_length",
"http.max_initial_line_length",
"http.max_header_size",
"http.compression",
"http.compression_level",
"http.cors.enabled",
"http.detailed_errors.enabled",
"http.pipelining.max_events",
"http.max_warning_header_count",
"http.max_warning_header_size",
"http.tcp.keep_alive",
"http.tcp.keep_idle",
"http.tcp.keep_interval",
"http.tcp.keep_count",
"http.tcp.no_delay",
"http.tcp.reuse_address",
"http.tcp.send_buffer_size",
"http.tcp.receive_buffer_size",
"http.client_stats.enabled",
"http.client_stats.closed_channels.max_count",
"http.client_stats.closed_channels.max_age",
"transport.host",
"transport.bind_host",
"transport.publish_host",
"transport.publish_port",
"transport.connect_timeout",
"transport.compress",
"transport.compression_scheme",
"transport.tcp.keep_alive",
"transport.tcp.keep_idle",
"transport.tcp.keep_interval",
"transport.tcp.keep_count",
"transport.tcp.no_delay",
"transport.tcp.reuse_address",
"transport.tcp.send_buffer_size",
"transport.tcp.receive_buffer_size",
"transport.ping_schedule",
"remote_cluster_server.enabled",
"remote_cluster.host",
"remote_cluster.bind_host",
"remote_cluster.publish_host",
"remote_cluster.publish_port",
"remote_cluster.tcp.keep_alive",
"remote_cluster.tcp.keep_idle",
"remote_cluster.tcp.keep_interval",
"remote_cluster.tcp.keep_count",
"remote_cluster.tcp.no_delay",
"remote_cluster.tcp.reuse_address",
"remote_cluster.tcp.send_buffer_size",
"remote_cluster.tcp.receive_buffer_size",
"org.elasticsearch.transport.InboundHandler",
"org.elasticsearch.transport.OutboundHandler",
"org.elasticsearch.common.network.ThreadWatchdog",
"network.thread.watchdog.interval",
"network.thread.watchdog.quiet_time",
"indices.queries.cache.size",
"index.queries.cache.enabled",
"vectors.indexing.use_gpu",
"cluster.remote.initial_connect_timeout",
"cluster.remote.node.attr",
"cluster.remote.signing.certificate_authorities",
"cluster.remote.signing.truststore.path",
"cluster.remote.signing.truststore.secure_password",
"cluster.remote.signing.truststore.algorithm",
"cluster.remote.signing.truststore.type",
"cluster.remote.signing.diagnose.trust",
"indices.query.bool.max_clause_count",
"search.max_buckets",
"search.aggs.only_allowed_metric_scripts",
"search.aggs.allowed_inline_metric_scripts",
"search.aggs.allowed_stored_metric_scripts",
"indices.query.bool.max_nested_depth",
"search.task_watchdog.enabled",
"search.task_watchdog.coordinator_threshold",
"search.task_watchdog.data_node_threshold",
"search.task_watchdog.interval",
"search.task_watchdog.cooldown_period",
"xpack.security.enabled",
"xpack.security.autoconfiguration.enabled",
"xpack.security.enrollment.enabled",
"xpack.security.hide_settings",
"xpack.security.fips_mode.enabled",
"xpack.security.fips_mode.required_providers",
"xpack.security.authc.password_hashing.algorithm",
"xpack.security.authc.anonymous.username",
"xpack.security.authc.anonymous.roles",
"xpack.security.authc.anonymous.authz_exception",
"xpack.security.automata.max_determinized_states",
"xpack.security.automata.cache.enabled",
"xpack.security.automata.cache.size",
"xpack.security.automata.cache.ttl",
"xpack.security.dls_fls.enabled",
"xpack.security.dls.bitset.cache.ttl",
"xpack.security.dls.bitset.cache.size",
"xpack.security.authc.token.enabled",
"xpack.security.authc.token.timeout",
"xpack.security.authc.api_key.enabled",
"xpack.security.authc.api_key.cache.ttl",
"xpack.security.authc.api_key.cache.max_keys",
"xpack.security.authc.api_key.cache.hash_algo",
"xpack.security.authc.api_key.delete.retention_period",
"xpack.security.authc.api_key.delete.interval",
"xpack.security.authc.api_key.delete.timeout",
"xpack.security.authc.api_key.hashing.algorithm",
"xpack.security.authc.realms.saml.*",
"xpack.security.authc.realms.oidc.*",
"xpack.security.authc.realms.kerberos.*",
"xpack.security.authc.realms.jwt.*",
"cache.ttl",
"cache.max_users",
"cache.hash_algo",
"authentication.enabled",
"cache.ttl",
"cache.max_users",
"cache.hash_algo",
"authentication.enabled",
"load_balance.type",
"load_balance.cache_ttl",
"user_search.base_dn",
"user_search.scope",
"user_search.filter",
"user_search.attribute",
"user_search.pool.enabled",
"user_search.pool.size",
"user_search.pool.initial_size",
"user_search.pool.health_check.enabled",
"user_search.pool.health_check.dn",
"user_search.pool.health_check.interval",
"group_search.base_dn",
"group_search.scope",
"group_search.filter",
"group_search.user_attribute",
"files.role_mapping",
"timeout.tcp_connect",
"timeout.tcp_read",
"timeout.response",
"timeout.ldap_search",
"ssl.key",
"ssl.key_passphrase",
"ssl.secure_key_passphrase",
"ssl.certificate",
"ssl.certificate_authorities",
"ssl.keystore.path",
"ssl.keystore.type",
"ssl.keystore.password",
"ssl.keystore.secure_password",
"ssl.keystore.key_password",
"ssl.keystore.secure_key_password",
"ssl.truststore.path",
"ssl.truststore.password",
"ssl.truststore.secure_password",
"ssl.truststore.type",
"ssl.verification_mode",
"ssl.supported_protocols",
"ssl.cipher_suites",
"cache.ttl",
"cache.max_users",
"cache.hash_algo",
"authentication.enabled",
"load_balance.type",
"load_balance.cache_ttl",
"files.role_mapping",
"user_search.base_dn",
"user_search.scope",
"user_search.filter",
"user_search.upn_filter",
"user_search.down_level_filter",
"user_search.pool.enabled",
"user_search.pool.size",
"user_search.pool.initial_size",
"user_search.pool.health_check.enabled",
"user_search.pool.health_check.dn",
"user_search.pool.health_check.interval",
"group_search.base_dn",
"group_search.scope",
"timeout.tcp_connect",
"timeout.tcp_read",
"timeout.response",
"timeout.ldap_search",
"ssl.certificate",
"ssl.certificate_authorities",
"ssl.key",
"ssl.key_passphrase",
"ssl.secure_key_passphrase",
"ssl.keystore.key_password",
"ssl.keystore.secure_key_password",
"ssl.keystore.password",
"ssl.secure_keystore.password",
"ssl.keystore.path",
"ssl.keystore.type",
"ssl.truststore.password",
"ssl.truststore.secure_password",
"ssl.truststore.path",
"ssl.truststore.type",
"ssl.verification_mode",
"ssl.supported_protocols",
"ssl.cipher_suites",
"cache.ttl",
"cache.max_users",
"cache.hash_algo",
"authentication.enabled",
"truststore.algorithm",
"truststore.password",
"truststore.secure_password",
"truststore.path",
"files.role_mapping",
"cache.ttl",
"cache.max_users",
"delegation.enabled",
"idp.entity_id",
"idp.metadata.path",
"idp.metadata.http.fail_on_error",
"idp.metadata.http.connect_timeout",
"idp.metadata.http.read_timeout",
"idp.metadata.http.refresh",
"idp.metadata.http.minimum_refresh",
"idp.use_single_logout",
"sp.entity_id",
"sp.acs",
"sp.logout",
"attributes.principal",
"attributes.groups",
"attributes.name",
"attributes.mail",
"attributes.dn",
"attribute_patterns.principal",
"attribute_patterns.groups",
"attribute_patterns.name",
"attribute_patterns.mail",
"attribute_patterns.dn",
"attribute_delimiters.groups",
"nameid.allow_create",
"nameid.sp_qualifier",
"signing.saml_messages",
"signing.key",
"signing.secure_key_passphrase",
"signing.certificate",
"signing.keystore.path",
"signing.keystore.type",
"signing.keystore.alias",
"signing.keystore.secure_password",
"signing.keystore.secure_key_password",
"encryption.key",
"encryption.secure_key_passphrase",
"encryption.certificate",
"encryption.keystore.path",
"encryption.keystore.type",
"encryption.keystore.alias",
"encryption.keystore.secure_password",
"encryption.keystore.secure_key_password",
"ssl.key",
"ssl.key_passphrase",
"ssl.secure_key_passphrase",
"ssl.certificate",
"ssl.certificate_authorities",
"ssl.keystore.path",
"ssl.keystore.type",
"ssl.keystore.password",
"ssl.keystore.secure_password",
"ssl.keystore.key_password",
"ssl.keystore.secure_key_password",
"ssl.truststore.path",
"ssl.truststore.type",
"ssl.truststore.password",
"ssl.truststore.secure_password",
"ssl.verification_mode",
"ssl.supported_protocols",
"ssl.cipher_suites",
"keytab.path",
"krb.debug",
"cache.ttl",
"cache.max_users",
"op.issuer",
"op.authorization_endpoint",
"op.token_endpoint",
"op.userinfo_endpoint",
"op.endsession_endpoint",
"op.jwkset_path",
"rp.client_id",
"rp.client_secret",
"rp.client_auth_method",
"rp.client_auth_jwt_signature_algorithm",
"rp.redirect_uri",
"rp.response_type",
"rp.signature_algorithm",
"rp.requested_scopes",
"rp.post_logout_redirect_uri",
"claims.principal",
"claims.groups",
"claims.name",
"claims.mail",
"claims.dn",
"claim_patterns.principal",
"claim_patterns.groups",
"claim_patterns.name",
"claim_patterns.mail",
"claim_patterns.dn",
"http.proxy.host",
"http.proxy.scheme",
"http.proxy.port",
"http.connect_timeout",
"http.connection_read_timeout",
"http.socket_timeout",
"http.max_connections",
"http.max_endpoint_connections",
"http.tcp.keep_alive",
"http.connection_pool_ttl",
"ssl.key",
"ssl.key_passphrase",
"ssl.secure_key_passphrase",
"ssl.certificate",
"ssl.certificate_authorities",
"ssl.keystore.path",
"ssl.keystore.type",
"ssl.keystore.password",
"ssl.keystore.secure_password",
"ssl.keystore.key_password",
"ssl.keystore.secure_key_password",
"ssl.truststore.path",
"ssl.truststore.type",
"ssl.truststore.password",
"ssl.truststore.secure_password",
"ssl.verification_mode",
"ssl.supported_protocols",
"ssl.cipher_suites",
"fallback_claims.sub",
"fallback_claims.aud",
"claims.dn",
"claim_patterns.dn",
"claims.groups",
"claim_patterns.group",
"claims.mail",
"claim_patterns.mail",
"claims.name",
"claim_patterns.name",
"claims.principal",
"claim_patterns.principal",
"client_authentication.type",
"client_authentication.shared_secret",
"client_authentication.rotation_grace_period",
"http.proxy.host",
"http.proxy.scheme",
"http.proxy.port",
"http.connect_timeout",
"http.connection_read_timeout",
"http.socket_timeout",
"http.max_connections",
"http.max_endpoint_connections",
"jwt.cache.size",
"jwt.cache.ttl",
"pkc_jwkset_reload.enabled",
"pkc_jwkset_reload.file_interval",
"pkc_jwkset_reload.url_interval_min",
"pkc_jwkset_reload.url_interval_max",
"ssl.key",
"ssl.key_passphrase",
"ssl.secure_key_passphrase",
"ssl.certificate",
"ssl.certificate_authorities",
"ssl.keystore.path",
"ssl.keystore.type",
"ssl.keystore.password",
"ssl.keystore.secure_password",
"ssl.keystore.key_password",
"ssl.keystore.secure_key_password",
"ssl.truststore.path",
"ssl.truststore.type",
"ssl.truststore.password",
"ssl.truststore.secure_password",
"ssl.verification_mode",
"ssl.supported_protocols",
"ssl.cipher_suites",
"xpack.security.ssl.diagnose.trust",
"xpack.security.http.ssl.enabled",
"xpack.security.http.ssl.supported_protocols",
"xpack.security.http.ssl.client_authentication",
"xpack.security.http.ssl.verification_mode",
"xpack.security.http.ssl.cipher_suites",
"xpack.security.http.ssl.key",
"xpack.security.http.ssl.key_passphrase",
"xpack.security.http.ssl.secure_key_passphrase",
"xpack.security.http.ssl.certificate",
"xpack.security.http.ssl.certificate_authorities",
"xpack.security.http.ssl.keystore.path",
"xpack.security.http.ssl.keystore.password",
"xpack.security.http.ssl.keystore.secure_password",
"xpack.security.http.ssl.keystore.key_password",
"xpack.security.http.ssl.keystore.secure_key_password",
"xpack.security.http.ssl.truststore.path",
"xpack.security.http.ssl.truststore.password",
"xpack.security.http.ssl.truststore.secure_password",
"xpack.security.http.ssl.keystore.path",
"xpack.security.http.ssl.keystore.type",
"xpack.security.http.ssl.keystore.password",
"xpack.security.http.ssl.keystore.secure_password",
"xpack.security.http.ssl.keystore.key_password",
"xpack.security.http.ssl.keystore.secure_key_password",
"xpack.security.http.ssl.truststore.path",
"xpack.security.http.ssl.truststore.type",
"xpack.security.http.ssl.truststore.password",
"xpack.security.http.ssl.truststore.secure_password",
"xpack.security.transport.ssl.enabled",
"xpack.security.transport.ssl.supported_protocols",
"xpack.security.transport.ssl.client_authentication",
"xpack.security.transport.ssl.verification_mode",
"xpack.security.transport.ssl.cipher_suites",
"xpack.security.transport.ssl.trust_restrictions.x509_fields",
"xpack.security.transport.ssl.handshake_timeout",
"xpack.security.transport.ssl.key",
"xpack.security.transport.ssl.key_passphrase",
"xpack.security.transport.ssl.secure_key_passphrase",
"xpack.security.transport.ssl.certificate",
"xpack.security.transport.ssl.certificate_authorities",
"xpack.security.loginAssistanceMessage",
"xpack.security.transport.ssl.keystore.path",
"xpack.security.transport.ssl.keystore.password",
"xpack.security.transport.ssl.keystore.secure_password",
"xpack.security.transport.ssl.keystore.key_password",
"xpack.security.transport.ssl.keystore.secure_key_password",
"xpack.security.transport.ssl.truststore.path",
"xpack.security.transport.ssl.truststore.password",
"xpack.security.transport.ssl.truststore.secure_password",
"xpack.security.transport.ssl.keystore.path",
"xpack.security.transport.ssl.keystore.type",
"xpack.security.transport.ssl.keystore.password",
"xpack.security.transport.ssl.keystore.secure_password",
"xpack.security.transport.ssl.keystore.key_password",
"xpack.security.transport.ssl.keystore.secure_key_password",
"xpack.security.transport.ssl.truststore.path",
"xpack.security.transport.ssl.truststore.type",
"xpack.security.transport.ssl.truststore.password",
"xpack.security.transport.ssl.truststore.secure_password",
"xpack.security.remote_cluster_server.ssl.enabled",
"xpack.security.remote_cluster_server.ssl.supported_protocols",
"xpack.security.remote_cluster_server.ssl.client_authentication",
"xpack.security.remote_cluster_server.ssl.verification_mode",
"xpack.security.remote_cluster_server.ssl.cipher_suites",
"xpack.security.remote_cluster_server.ssl.handshake_timeout",
"xpack.security.remote_cluster_server.ssl.key",
"xpack.security.remote_cluster_server.ssl.secure_key_passphrase",
"xpack.security.remote_cluster_server.ssl.certificate",
"xpack.security.remote_cluster_server.ssl.certificate_authorities",
"xpack.security.remote_cluster_server.ssl.keystore.path",
"xpack.security.remote_cluster_server.ssl.keystore.secure_password",
"xpack.security.remote_cluster_server.ssl.keystore.secure_key_password",
"xpack.security.remote_cluster_server.ssl.truststore.path",
"xpack.security.remote_cluster_server.ssl.truststore.secure_password",
"xpack.security.remote_cluster_server.ssl.keystore.path",
"xpack.security.remote_cluster_server.ssl.keystore.type",
"xpack.security.remote_cluster_server.ssl.keystore.secure_password",
"xpack.security.remote_cluster_server.ssl.keystore.secure_key_password",
"xpack.security.remote_cluster_server.ssl.truststore.path",
"xpack.security.remote_cluster_server.ssl.truststore.type",
"xpack.security.remote_cluster_server.ssl.truststore.secure_password",
"xpack.security.remote_cluster_client.ssl.enabled",
"xpack.security.remote_cluster_client.ssl.supported_protocols",
"xpack.security.remote_cluster_client.ssl.verification_mode",
"xpack.security.remote_cluster_client.ssl.cipher_suites",
"xpack.security.remote_cluster_client.ssl.handshake_timeout",
"xpack.security.remote_cluster_client.ssl.key",
"xpack.security.remote_cluster_client.ssl.secure_key_passphrase",
"xpack.security.remote_cluster_client.ssl.certificate",
"xpack.security.remote_cluster_client.ssl.certificate_authorities",
"xpack.security.remote_cluster_client.ssl.keystore.path",
"xpack.security.remote_cluster_client.ssl.keystore.secure_password",
"xpack.security.remote_cluster_client.ssl.keystore.secure_key_password",
"xpack.security.remote_cluster_client.ssl.truststore.path",
"xpack.security.remote_cluster_client.ssl.truststore.secure_password",
"xpack.security.remote_cluster_client.ssl.keystore.path",
"xpack.security.remote_cluster_client.ssl.keystore.type",
"xpack.security.remote_cluster_client.ssl.keystore.secure_password",
"xpack.security.remote_cluster_client.ssl.keystore.secure_key_password",
"xpack.security.remote_cluster_client.ssl.truststore.path",
"xpack.security.remote_cluster_client.ssl.truststore.type",
"xpack.security.remote_cluster_client.ssl.truststore.secure_password",
"xpack.security.transport.filter.allow",
"xpack.security.transport.filter.deny",
"xpack.security.http.filter.allow",
"xpack.security.http.filter.deny",
"transport.profiles.$PROFILE.xpack.security.filter.allow",
"transport.profiles.$PROFILE.xpack.security.filter.deny",
"xpack.security.remote_cluster.filter.allow",
"xpack.security.remote_cluster.filter.deny",
"indices.requests.cache.size",
"indices.requests.cache.expire",
"snapshot.max_concurrent_operations",
"repositories.default_repository",
"slm.history_index_enabled",
"slm.retention_schedule",
"slm.retention_duration",
"slm.health.failed_snapshot_warn_threshold",
"node.roles: [ transform ]",
"xpack.transform.enabled",
"xpack.transform.num_transform_failure_retries",
"xpack.watcher.enabled",
"xpack.watcher.encrypt_sensitive_data",
"xpack.watcher.encryption_key",
"xpack.watcher.max.history.record.size",
"xpack.watcher.trigger.schedule.engine",
"xpack.watcher.history.cleaner_service.enabled",
"xpack.http.proxy.host",
"xpack.http.proxy.port",
"xpack.http.proxy.scheme",
"xpack.http.default_connection_timeout",
"xpack.http.default_read_timeout",
"xpack.http.tcp.keep_alive",
"xpack.http.connection_pool_ttl",
"xpack.http.max_response_size",
"xpack.http.whitelist",
"xpack.http.ssl.supported_protocols",
"xpack.http.ssl.verification_mode",
"xpack.http.ssl.cipher_suites",
"xpack.http.ssl.key",
"xpack.http.ssl.secure_key_passphrase",
"xpack.http.ssl.certificate",
"xpack.http.ssl.certificate_authorities",
"xpack.http.ssl.keystore.path",
"xpack.http.ssl.keystore.secure_password",
"xpack.http.ssl.keystore.secure_key_password",
"xpack.http.ssl.truststore.path",
"xpack.http.ssl.truststore.secure_password",
"xpack.http.ssl.keystore.path",
"xpack.http.ssl.keystore.type",
"xpack.http.ssl.keystore.secure_password",
"xpack.http.ssl.keystore.secure_key_password",
"xpack.http.ssl.truststore.path",
"xpack.http.ssl.truststore.type",
"xpack.http.ssl.truststore.secure_password",
"xpack.notification.email.default_account",
"xpack.notification.email.recipient_allowlist",
"xpack.notification.email.account",
"xpack.notification.email.account.domain_allowlist",
"email_defaults.*",
"smtp.auth",
"smtp.host",
"smtp.port",
"smtp.user",
"smtp.secure_password",
"smtp.starttls.enable",
"smtp.starttls.required",
"smtp.ssl.trust",
"smtp.timeout",
"smtp.connection_timeout",
"smtp.write_timeout",
"smtp.local_address",
"smtp.local_port",
"smtp.send_partial",
"smtp.wait_on_quit",
"xpack.notification.email.html.sanitization.allow",
"xpack.notification.email.html.sanitization.disallow",
"xpack.notification.email.html.sanitization.enabled",
"xpack.notification.email.ssl.supported_protocols",
"xpack.notification.email.ssl.verification_mode",
"xpack.notification.email.ssl.cipher_suites",
"xpack.notification.email.ssl.key",
"xpack.notification.email.ssl.secure_key_passphrase",
"xpack.notification.email.ssl.certificate",
"xpack.notification.email.ssl.certificate_authorities",
"xpack.notification.email.ssl.keystore.path",
"xpack.notification.email.ssl.keystore.secure_password",
"xpack.notification.email.ssl.keystore.secure_key_password",
"xpack.notification.email.ssl.truststore.path",
"xpack.notification.email.ssl.truststore.secure_password",
"xpack.notification.email.ssl.keystore.path",
"xpack.notification.email.ssl.keystore.type",
"xpack.notification.email.ssl.keystore.secure_password",
"xpack.notification.email.ssl.keystore.secure_key_password",
"xpack.notification.email.ssl.truststore.path",
"xpack.notification.email.ssl.truststore.type",
"xpack.notification.email.ssl.truststore.secure_password",
"xpack.notification.slack",
"xpack.notification.slack.default_account",
"xpack.notification.slack.account",
"xpack.notification.jira.default_account",
"xpack.notification.jira.account",
"xpack.notification.pagerduty",
"xpack.notification.pagerduty.default_account",
"xpack.notification.pagerduty.account",
"xpack.notification.webhook.additional_token_enabled",
}

View File

@@ -20,7 +20,6 @@ import (
"context" "context"
"fmt" "fmt"
"regexp" "regexp"
"strconv"
"strings" "strings"
"codeberg.org/scip/esctl/pkg/cfg" "codeberg.org/scip/esctl/pkg/cfg"
@@ -73,27 +72,27 @@ func checkClusterStatus(conf *cfg.Config, leader, follower string) bool {
table.Addheaders("setting", "leader:"+leader, "follower:"+follower) table.Addheaders("setting", "leader:"+leader, "follower:"+follower)
table.Entries = [][]string{ table.Entries = [][]any{
{"Cluster Name", status[leader].ClusterName, status[follower].ClusterName}, {"Cluster Name", status[leader].ClusterName, status[follower].ClusterName},
{"Cluster Status", {"Cluster Status",
printer.Colorize(conf, status[leader].Status.Name, status[leader].Status.Name), printer.Colorize(conf, status[leader].Status.Name, status[leader].Status.Name),
printer.Colorize(conf, status[follower].Status.Name, status[follower].Status.Name), printer.Colorize(conf, status[follower].Status.Name, status[follower].Status.Name),
}, },
{"Active Shards", {"Active Shards",
fmt.Sprintf("%d", status[leader].ActiveShards), status[leader].ActiveShards,
fmt.Sprintf("%d", status[follower].ActiveShards), status[follower].ActiveShards,
}, },
{"Active Primary Shards", {"Active Primary Shards",
fmt.Sprintf("%d", status[leader].ActivePrimaryShards), status[leader].ActivePrimaryShards,
fmt.Sprintf("%d", status[follower].ActivePrimaryShards), status[follower].ActivePrimaryShards,
}, },
{"Indicies", {"Indicies",
fmt.Sprintf("%d", len(status[leader].Indices)), len(status[leader].Indices),
fmt.Sprintf("%d", len(status[follower].Indices)), len(status[follower].Indices),
}, },
{"Nodes", {"Nodes",
fmt.Sprintf("%d", status[leader].NumberOfNodes), status[leader].NumberOfNodes,
fmt.Sprintf("%d", status[follower].NumberOfNodes), status[follower].NumberOfNodes,
}, },
} }
@@ -113,7 +112,7 @@ func checkClusterStatus(conf *cfg.Config, leader, follower string) bool {
// finds indices on both clusters which have ilm errors // finds indices on both clusters which have ilm errors
func findIlmErrors(conf *cfg.Config, leader, follower string) bool { func findIlmErrors(conf *cfg.Config, leader, follower string) bool {
failed := map[string]map[string]string{} failed := map[string]map[string]any{}
for _, cluster := range []string{leader, follower} { for _, cluster := range []string{leader, follower} {
ilm, err := conf.Clusters[cluster].ES().Ilm.ExplainLifecycle("_all"). ilm, err := conf.Clusters[cluster].ES().Ilm.ExplainLifecycle("_all").
@@ -124,13 +123,13 @@ func findIlmErrors(conf *cfg.Config, leader, follower string) bool {
return false return false
} }
failed[cluster] = map[string]string{} failed[cluster] = map[string]any{}
for name, ilmstate := range ilm.Indices { for name, ilmstate := range ilm.Indices {
count := ilmstate.(*types.LifecycleExplainManaged).FailedStepRetryCount count := ilmstate.(*types.LifecycleExplainManaged).FailedStepRetryCount
if count != nil && *count > 0 { if count != nil && *count > 0 {
failed[cluster][name] = fmt.Sprintf("%d", *count) failed[cluster][name] = *count
} }
} }
} }
@@ -151,7 +150,7 @@ func findIlmErrors(conf *cfg.Config, leader, follower string) bool {
table.Addheaders("ilm errors on "+which, "errors") table.Addheaders("ilm errors on "+which, "errors")
for name, count := range failed[cluster] { for name, count := range failed[cluster] {
table.Entries[idx] = []string{name, count} table.Entries[idx] = []any{name, count}
idx++ idx++
} }
@@ -223,7 +222,7 @@ func findIndicesOnlyOnLeader(conf *cfg.Config, indices ClusterIndices, leader, f
for name, index := range indexOnlyOnLeader { for name, index := range indexOnlyOnLeader {
name := printer.Colorize(conf, "red", name) name := printer.Colorize(conf, "red", name)
table.Entries[idx] = []string{name, *index.DatasetSize, *index.DocsCount} table.Entries[idx] = []any{name, *index.DatasetSize, *index.DocsCount}
idx++ idx++
} }
@@ -271,7 +270,7 @@ func findOrphanedIndices(conf *cfg.Config, indices ClusterIndices, leader, follo
for name, index := range orphaned { for name, index := range orphaned {
name := printer.Colorize(conf, "red", name) name := printer.Colorize(conf, "red", name)
table.Entries[idx] = []string{name, *index.DatasetSize, *index.DocsCount} table.Entries[idx] = []any{name, *index.DatasetSize, *index.DocsCount}
idx++ idx++
} }
@@ -305,7 +304,7 @@ func findFailedFollowerIndices(conf *cfg.Config, indices ClusterIndices, followe
for name, index := range red { for name, index := range red {
name := printer.Colorize(conf, "red", name) name := printer.Colorize(conf, "red", name)
table.Entries[idx] = []string{name, *index.DatasetSize, *index.DocsCount} table.Entries[idx] = []any{name, *index.DatasetSize, *index.DocsCount}
idx++ idx++
} }
@@ -330,44 +329,3 @@ func splitArg(arg string) (string, string) {
return parts[0], parts[1] return parts[0], parts[1]
} }
} }
// 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
}

View File

@@ -25,7 +25,6 @@ import (
"codeberg.org/scip/esctl/pkg/cfg" "codeberg.org/scip/esctl/pkg/cfg"
"codeberg.org/scip/esctl/pkg/printer" "codeberg.org/scip/esctl/pkg/printer"
"github.com/dustin/go-humanize"
"github.com/elastic/go-elasticsearch/v9/typedapi/types" "github.com/elastic/go-elasticsearch/v9/typedapi/types"
) )
@@ -73,13 +72,13 @@ func DatastreamList(conf *cfg.Config) error {
policy = *ds.IlmPolicy policy = *ds.IlmPolicy
} }
table.Entries[idx] = []string{ table.Entries[idx] = []any{
name, name,
policy, policy,
fmt.Sprintf("%t", ds.Hidden), ds.Hidden,
fmt.Sprintf("%t", *ds.System), *ds.System,
fmt.Sprintf("%t", *ds.Replicated), *ds.Replicated,
fmt.Sprintf("%d", ds.Generation), ds.Generation,
ds.TimestampField.Name, ds.TimestampField.Name,
} }
@@ -156,18 +155,18 @@ func DatastreamShow(conf *cfg.Config, dsname string) error {
policy = *ds.IlmPolicy policy = *ds.IlmPolicy
} }
table.Entries = [][]string{ table.Entries = [][]any{
{"name", name}, {"name", name},
{"ilm policy", policy}, {"ilm policy", policy},
{"hidden", fmt.Sprintf("%t", ds.Hidden)}, {"hidden", ds.Hidden},
{"system", fmt.Sprintf("%t", *ds.System)}, {"system", *ds.System},
{"replicated", fmt.Sprintf("%t", *ds.Replicated)}, {"replicated", *ds.Replicated},
{"generation", fmt.Sprintf("%d", ds.Generation)}, {"generation", ds.Generation},
{"timestamp field", ds.TimestampField.Name}, {"timestamp field", ds.TimestampField.Name},
{"backing indices", fmt.Sprintf("%d", stats.BackingIndices)}, {"backing indices", stats.BackingIndices},
{"size", humanize.Bytes(uint64(stats.TotalStoreSizeBytes))}, {"size", printer.Bytes(stats.TotalStoreSizeBytes)},
{"shards-failed", fmt.Sprintf("%d", stats.Shards_.Failed)}, {"shards-failed", stats.Shards_.Failed},
{"shards-successful", fmt.Sprintf("%d", stats.Shards_.Successful)}, {"shards-successful", stats.Shards_.Successful},
} }
if err := table.Print(); err != nil { if err := table.Print(); err != nil {
@@ -183,10 +182,10 @@ func DatastreamShow(conf *cfg.Config, dsname string) error {
policy = *ds.IlmPolicy policy = *ds.IlmPolicy
} }
table.Entries[idx] = []string{ table.Entries[idx] = []any{
index.IndexName, index.IndexName,
index.IndexUuid, index.IndexUuid,
fmt.Sprintf("%t", *index.PreferIlm), *index.PreferIlm,
policy, policy,
index.ManagedBy.Name, index.ManagedBy.Name,
} }
@@ -235,10 +234,10 @@ func DatastreamRollover(conf *cfg.Config, ds string) error {
table := printer.NewTable(conf, 2, 5) table := printer.NewTable(conf, 2, 5)
table.Addheaders("rollover response", "value") table.Addheaders("rollover response", "value")
table.Entries = [][]string{ table.Entries = [][]any{
{"acknowledged", fmt.Sprintf("%t", res.Acknowledged)}, {"acknowledged", res.Acknowledged},
{"rolled over", fmt.Sprintf("%t", res.RolledOver)}, {"rolled over", res.RolledOver},
{"shards acknowledged", fmt.Sprintf("%t", res.ShardsAcknowledged)}, {"shards acknowledged", res.ShardsAcknowledged},
{"old index", res.OldIndex}, {"old index", res.OldIndex},
{"new index", res.NewIndex}, {"new index", res.NewIndex},
} }

View File

@@ -67,6 +67,7 @@ func IlmNames(conf *cfg.Config) ([]string, error) {
for name := range res { for name := range res {
names[idx] = name names[idx] = name
idx++
} }
return names, nil return names, nil
@@ -128,7 +129,7 @@ func IlmShow(conf *cfg.Config, policy string) error {
table := printer.NewTable(conf, 2, 5) table := printer.NewTable(conf, 2, 5)
table.Addheaders("ilm policy setting", "value") table.Addheaders("ilm policy setting", "value")
table.Entries = [][]string{ table.Entries = [][]any{
{"policy", policy}, {"policy", policy},
{"hot phase", ilmPhaseString(ilm.Policy.Phases.Hot, false)}, {"hot phase", ilmPhaseString(ilm.Policy.Phases.Hot, false)},
{"warm phase", ilmPhaseString(ilm.Policy.Phases.Warm, false)}, {"warm phase", ilmPhaseString(ilm.Policy.Phases.Warm, false)},
@@ -290,17 +291,17 @@ func IlmExplain(conf *cfg.Config, index string) error {
return fmt.Errorf("failed to unmarshal step info: %w", err) return fmt.Errorf("failed to unmarshal step info: %w", err)
} }
table.Entries = [][]string{ table.Entries = [][]any{
{"index", ilm.Index}, {"index", ilm.Index},
{"ilm policy", *ilm.Policy}, {"ilm policy", *ilm.Policy},
{"action", *ilm.Action}, {"action", *ilm.Action},
{"age", fmt.Sprintf("%s", ilm.Age)}, {"age", ilm.Age},
{"managed", fmt.Sprintf("%t", ilm.Managed)}, {"managed", ilm.Managed},
{"phase", *ilm.Phase}, {"phase", *ilm.Phase},
{"phase execution", ilmPhaseString(ilm.PhaseExecution.PhaseDefinition, false)}, {"phase execution", ilmPhaseString(ilm.PhaseExecution.PhaseDefinition, false)},
{"step", *ilm.Step}, {"step", *ilm.Step},
{"failed step", *ilm.FailedStep}, {"failed step", *ilm.FailedStep},
{"failed step retry count", fmt.Sprintf("%d", ilm.FailedStepRetryCount)}, {"failed step retry count", ilm.FailedStepRetryCount},
} }
if err := table.Print(); err != nil { if err := table.Print(); err != nil {

View File

@@ -119,13 +119,13 @@ func IlmForecastList(conf *cfg.Config, filter string) error {
virtualAge := virtualAge(&phase) virtualAge := virtualAge(&phase)
row := []string{ row := []any{
phase.index, phase.index,
humanize.Bytes(uint64(phase.size)), printer.Bytes(phase.size),
formatDuration(phase.age), formatDuration(phase.age),
formatDuration(virtualAge), formatDuration(virtualAge),
formatDuration(phase.minage), formatDuration(phase.minage),
humanize.Bytes(uint64(phase.minsize)), printer.Bytes(phase.minsize),
phase.currentPhase, phase.currentPhase,
phase.nextPhase, phase.nextPhase,
} }

View File

@@ -118,7 +118,7 @@ func IndexList(conf *cfg.Config) error {
for idx, index := range list { for idx, index := range list {
name := printer.Colorize(conf, *index.Health, *index.Index) name := printer.Colorize(conf, *index.Health, *index.Index)
table.Entries[idx] = []string{name, *index.DatasetSize, *index.DocsCount} table.Entries[idx] = []any{name, *index.DatasetSize, *index.DocsCount}
if idx == size-1 { if idx == size-1 {
break break
@@ -156,18 +156,18 @@ func IndexShow(conf *cfg.Config, indexpattern string) error {
created := time.Unix(ts/1000, 0) created := time.Unix(ts/1000, 0)
table.Entries = [][]string{ table.Entries = [][]any{
{"name", name}, {"name", name},
{"replicas", *index.Settings.Index.NumberOfReplicas}, {"replicas", *index.Settings.Index.NumberOfReplicas},
{"shards", *index.Settings.Index.NumberOfShards}, {"shards", *index.Settings.Index.NumberOfShards},
{"created", created.Format("2006-01-02 15:04:05")}, {"created", created},
{"uuid", *index.Settings.Index.Uuid}, {"uuid", *index.Settings.Index.Uuid},
{"version", *index.Settings.Index.Version.Created}, {"version", *index.Settings.Index.Version.Created},
{"fields", strings.Join(fields, ",")}, {"fields", fields},
} }
if index.Settings.Index.Lifecycle != nil { if index.Settings.Index.Lifecycle != nil {
table.Entries = append(table.Entries, [][]string{ table.Entries = append(table.Entries, [][]any{
{"ilm policy", *index.Settings.Index.Lifecycle.Name}, {"ilm policy", *index.Settings.Index.Lifecycle.Name},
{"ilm rollover alias", *index.Settings.Index.Lifecycle.RolloverAlias}, {"ilm rollover alias", *index.Settings.Index.Lifecycle.RolloverAlias},
}...) }...)
@@ -283,14 +283,14 @@ func IndexAllocation(conf *cfg.Config, index string) error {
roles[idx] = role.Name roles[idx] = role.Name
} }
table.Entries = [][]string{ table.Entries = [][]any{
{"Index", index}, {"Index", index},
{"Current node", currentNode.Name}, {"Current node", currentNode.Name},
{"Current k8s node", currentNode.Attributes["k8s_node_name"]}, {"Current k8s node", currentNode.Attributes["k8s_node_name"]},
{"Current node address", currentNode.TransportAddress}, {"Current node address", currentNode.TransportAddress},
{"Current node id", currentNode.Id}, {"Current node id", currentNode.Id},
{"Current node weight", fmt.Sprintf("%d", currentNode.WeightRanking)}, {"Current node weight", currentNode.WeightRanking},
{"Current node roles", strings.Join(roles, ",")}, {"Current node roles", roles},
{"Can rebalance cluster", res.CanRebalanceCluster.Name}, {"Can rebalance cluster", res.CanRebalanceCluster.Name},
{"Can rebalance to another node", res.CanRebalanceToOtherNode.Name}, {"Can rebalance to another node", res.CanRebalanceToOtherNode.Name},
{"Can remain on current node", res.CanRemainOnCurrentNode.Name}, {"Can remain on current node", res.CanRemainOnCurrentNode.Name},
@@ -344,10 +344,11 @@ func IndexFields(conf *cfg.Config, index string) error {
continue continue
} }
table.Entries = append(table.Entries, []string{name, fieldtype, table.AddRow(name, fieldtype,
fmt.Sprintf("%t", caps.Searchable), caps.Searchable,
fmt.Sprintf("%t", caps.Aggregatable), caps.Aggregatable,
fmt.Sprintf("%t", *caps.MetadataField)}) *caps.MetadataField)
break break
} }

View File

@@ -78,7 +78,7 @@ func IndexAliasList(conf *cfg.Config) error {
idx := 0 idx := 0
for index, aliases := range aliaslist { for index, aliases := range aliaslist {
table.Entries[idx] = []string{ table.Entries[idx] = []any{
index, index,
strings.Join(aliases, ","), strings.Join(aliases, ","),
} }
@@ -117,10 +117,10 @@ func IndexAliasRollover(conf *cfg.Config, alias string) error {
table := printer.NewTable(conf, 2, 5) table := printer.NewTable(conf, 2, 5)
table.Addheaders("rollover response", "value") table.Addheaders("rollover response", "value")
table.Entries = [][]string{ table.Entries = [][]any{
{"acknowledged", fmt.Sprintf("%t", res.Acknowledged)}, {"acknowledged", res.Acknowledged},
{"rolled over", fmt.Sprintf("%t", res.RolledOver)}, {"rolled over", res.RolledOver},
{"shards acknowledged", fmt.Sprintf("%t", res.ShardsAcknowledged)}, {"shards acknowledged", res.ShardsAcknowledged},
{"old index", res.OldIndex}, {"old index", res.OldIndex},
{"new index", res.NewIndex}, {"new index", res.NewIndex},
} }

View File

@@ -51,11 +51,7 @@ func IndexTemplateList(conf *cfg.Config) error {
return fmt.Errorf("failed to unmarshal meta json data: %w", err) return fmt.Errorf("failed to unmarshal meta json data: %w", err)
} }
table.Entries[idx] = []string{ table.Entries[idx] = []any{tpl.Name, desc, tpl.IndexTemplate.Priority}
tpl.Name,
string(desc),
fmt.Sprintf("%d", tpl.IndexTemplate.Priority),
}
} }
table.Sort() table.Sort()
@@ -94,13 +90,13 @@ func IndexTemplateShow(conf *cfg.Config, tplname string) error {
aliases = append(aliases, alias) aliases = append(aliases, alias)
} }
table.Entries = [][]string{ table.Entries = [][]any{
{"name", tpl.Name}, {"name", tpl.Name},
{"description", string(desc)}, {"description", desc},
{"index patterns", strings.Join(tpl.IndexTemplate.IndexPatterns, ",")}, {"index patterns", tpl.IndexTemplate.IndexPatterns},
{"composed of", strings.Join(tpl.IndexTemplate.ComposedOf, ",")}, {"composed of", tpl.IndexTemplate.ComposedOf},
{"data stream enabled", fmt.Sprintf("%t", hasds)}, {"data stream enabled", hasds},
{"aliases", strings.Join(aliases, ",")}, {"aliases", aliases},
} }
if err := table.Print(); err != nil { if err := table.Print(); err != nil {
@@ -136,9 +132,7 @@ func IndexTemplateShow(conf *cfg.Config, tplname string) error {
typeval = val.Type typeval = val.Type
} }
table.Entries = append(table.Entries, []string{ table.AddRow(name, typeval)
name, typeval,
})
} }
fmt.Println() fmt.Println()
@@ -414,12 +408,7 @@ func rolloverAliasIndexTemplate(conf *cfg.Config, name string) error {
return err return err
} }
table.Entries = append(table.Entries, []string{ table.AddRow(alias, res.RolledOver, res.Acknowledged, res.NewIndex)
alias,
fmt.Sprintf("%t", res.RolledOver),
fmt.Sprintf("%t", res.Acknowledged),
res.NewIndex,
})
if err := table.Print(); err != nil { if err := table.Print(); err != nil {
return err return err

View File

@@ -28,7 +28,7 @@ type Tpl struct {
} }
func getIndexTemplateSettings(conf *cfg.Config, tplname string, table *printer.Table) error { func getIndexTemplateSettings(conf *cfg.Config, tplname string, table *printer.Table) error {
raw, err := CallAPI(conf, "GET", "/_index_template/"+tplname, "") raw, err := CallAPI(conf, "GET", "/_index_template/"+tplname+"?flat_settings", "")
if err != nil { if err != nil {
return err return err
} }
@@ -39,9 +39,11 @@ func getIndexTemplateSettings(conf *cfg.Config, tplname string, table *printer.T
} }
if conf.Debug { if conf.Debug {
if err := prettyfiJson(conf, raw); err != nil { output, err := prettyfiJson(conf, raw)
if err != nil {
return err return err
} }
fmt.Println(output)
} }
if len(data.IndexTemplates) == 0 { if len(data.IndexTemplates) == 0 {
@@ -50,11 +52,7 @@ func getIndexTemplateSettings(conf *cfg.Config, tplname string, table *printer.T
tpl := data.IndexTemplates[0].IndexTemplate.Template.Settings tpl := data.IndexTemplates[0].IndexTemplate.Template.Settings
for topic, val := range tpl { for topic, val := range tpl {
paths := getJsonPath(map[string]string{}, val.(map[string]any), topic) table.AddRow(topic, val)
for setting, value := range paths {
table.Entries = append(table.Entries, []string{setting, fmt.Sprintf("%v", value)})
}
} }
return nil return nil

69
pkg/es/license.go Normal file
View 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 es
import (
"context"
"fmt"
"log/slog"
"codeberg.org/scip/esctl/pkg/cfg"
"codeberg.org/scip/esctl/pkg/printer"
)
func LicenseShow(conf *cfg.Config) error {
res, err := conf.DefaultCluster.ES().License.Get().
Do(context.Background())
if err != nil {
return fmt.Errorf("failed to get license: %s", esErrorString(err))
}
slog.Debug("license show", "license", res)
table := printer.NewTableEmpty(conf).WithHeaders("license setting", "value")
lic := res.License
var maxnodes = "infinite"
var maxunits = "infinite"
var expire = "never"
if lic.MaxNodes != nil {
maxnodes = fmt.Sprintf("%d", *lic.MaxNodes)
}
if lic.ExpiryDate != nil {
expire = lic.ExpiryDate.(string)
}
if lic.MaxResourceUnits != nil {
maxunits = fmt.Sprintf("%d", *lic.MaxResourceUnits)
}
table.Entries = [][]any{
{"UID", lic.Uid},
{"Issued to", lic.IssuedTo},
{"Expires", expire},
{"Issued", lic.IssueDate},
{"Max nodes", maxnodes},
{"Max resource units", maxunits},
{"Type", lic.Type.Name},
{"Status", lic.Status.Name},
}
return table.Print()
}

View File

@@ -41,14 +41,14 @@ func NodeList(conf *cfg.Config) error {
table.Addheaders("name", "ip", "load1m", "load5m", "load15m", "ram %", "heap %") table.Addheaders("name", "ip", "load1m", "load5m", "load15m", "ram %", "heap %")
for idx, node := range nodes { for idx, node := range nodes {
table.Entries[idx] = []string{ table.Entries[idx] = []any{
*node.Name, *node.Name,
*node.Ip, *node.Ip,
*node.Load1M, *node.Load1M,
*node.Load5M, *node.Load5M,
*node.Load15M, *node.Load15M,
node.RamPercent.(string), node.RamPercent,
node.HeapPercent.(string), node.HeapPercent,
} }
} }
@@ -113,25 +113,25 @@ func NodeShow(conf *cfg.Config, nodename string) error {
k8snode := info.Attributes["k8s_node_name"] k8snode := info.Attributes["k8s_node_name"]
table.Entries = [][]string{ table.Entries = [][]any{
{"Id", id}, {"Id", id},
{"Name", nodename}, {"Name", nodename},
{"Kubernetes node", k8snode}, {"Kubernetes node", k8snode},
{"Ip address", info.Ip}, {"Ip address", info.Ip},
{"Node rank", *stat.AdaptiveSelection[id].Rank}, {"Node rank", *stat.AdaptiveSelection[id].Rank},
{"JVM", info.Jvm.VmName + " " + info.Jvm.Version}, {"JVM", info.Jvm.VmName + " " + info.Jvm.Version},
{"JVM Started", time.UnixMilli(info.Jvm.StartTimeInMillis).String()}, {"JVM Started", time.UnixMilli(info.Jvm.StartTimeInMillis)},
{"OS", info.Os.PrettyName + " " + info.Os.Version}, {"OS", info.Os.PrettyName + " " + info.Os.Version},
{"Node roles", strings.Join(roles, ",")}, {"Node roles", roles},
{"Node version", info.Version}, {"Node version", info.Version},
{"HTTP clients", fmt.Sprintf("%d", *stat.Http.CurrentOpen)}, {"HTTP clients", *stat.Http.CurrentOpen},
{"CPUs", fmt.Sprintf("%d", *info.Os.AllocatedProcessors)}, {"CPUs", *info.Os.AllocatedProcessors},
{"Load 15m/5m/1m", fmt.Sprintf("%.2f/%.2f/%.2f", {"Load 15m/5m/1m", fmt.Sprintf("%.2f/%.2f/%.2f",
stat.Os.Cpu.LoadAverage["15m"], stat.Os.Cpu.LoadAverage["15m"],
stat.Os.Cpu.LoadAverage["5m"], stat.Os.Cpu.LoadAverage["5m"],
stat.Os.Cpu.LoadAverage["1m"], stat.Os.Cpu.LoadAverage["1m"],
)}, )},
{"Open FD's", fmt.Sprintf("%d", *stat.Process.OpenFileDescriptors)}, {"Open FD's", *stat.Process.OpenFileDescriptors},
{"Response time avg", fmt.Sprintf("%dns", *stat.AdaptiveSelection[id].AvgResponseTimeNs)}, {"Response time avg", fmt.Sprintf("%dns", *stat.AdaptiveSelection[id].AvgResponseTimeNs)},
{"Memory usage (used/avail)", {"Memory usage (used/avail)",
humanize.Bytes(uint64(*stat.Os.Mem.UsedInBytes)) + " / " + humanize.Bytes(uint64(*stat.Os.Mem.TotalInBytes))}, humanize.Bytes(uint64(*stat.Os.Mem.UsedInBytes)) + " / " + humanize.Bytes(uint64(*stat.Os.Mem.TotalInBytes))},
@@ -139,7 +139,7 @@ func NodeShow(conf *cfg.Config, nodename string) error {
if len(stat.Fs.Data) > 0 { if len(stat.Fs.Data) > 0 {
fs := stat.Fs.Data[0] fs := stat.Fs.Data[0]
table.Entries = append(table.Entries, [][]string{ table.Entries = append(table.Entries, [][]any{
{"Storage usage (used/avail)", {"Storage usage (used/avail)",
humanize.Bytes(uint64(*fs.AvailableInBytes)) + " / " + humanize.Bytes(uint64(*fs.TotalInBytes))}, humanize.Bytes(uint64(*fs.AvailableInBytes)) + " / " + humanize.Bytes(uint64(*fs.TotalInBytes))},
{"Storage mount", *fs.Mount}, {"Storage mount", *fs.Mount},

View File

@@ -20,7 +20,6 @@ import (
"context" "context"
"fmt" "fmt"
"log/slog" "log/slog"
"strings"
"codeberg.org/scip/esctl/pkg/cfg" "codeberg.org/scip/esctl/pkg/cfg"
"codeberg.org/scip/esctl/pkg/printer" "codeberg.org/scip/esctl/pkg/printer"
@@ -58,10 +57,10 @@ func RoleList(conf *cfg.Config) error {
idx := 0 idx := 0
for name, role := range res { for name, role := range res {
table.Entries[idx] = []string{ table.Entries[idx] = []any{
name, name,
fmt.Sprintf("%d", len(role.Cluster)), role.Cluster,
fmt.Sprintf("%d", len(role.Indices)), role.Indices,
} }
idx++ idx++
} }
@@ -140,10 +139,8 @@ func roleRemoteClusters(conf *cfg.Config, role types.Role) error {
perms = append(perms, perm.Name) perms = append(perms, perm.Name)
} }
table.Entries[idx] = []string{ table.Entries[idx] = []any{priv.Clusters, perms}
strings.Join(priv.Clusters, ","),
strings.Join(perms, ","),
}
idx++ idx++
} }
@@ -160,7 +157,7 @@ func roleClusters(conf *cfg.Config, role types.Role) error {
table.Addheaders("cluster rights") table.Addheaders("cluster rights")
idx := 0 idx := 0
for _, cluster := range role.Cluster { for _, cluster := range role.Cluster {
table.Entries[idx] = []string{cluster.Name} table.Entries[idx] = []any{cluster.Name}
idx++ idx++
} }
@@ -182,11 +179,7 @@ func roleRemoteIndices(conf *cfg.Config, role types.Role) error {
perms = append(perms, perm.Name) perms = append(perms, perm.Name)
} }
table.Entries[idx] = []string{ table.Entries[idx] = []any{priv.Names, perms, *priv.AllowRestrictedIndices}
strings.Join(priv.Names, ", "),
strings.Join(perms, ", "),
fmt.Sprintf("%t", *priv.AllowRestrictedIndices),
}
idx++ idx++
} }
@@ -209,11 +202,7 @@ func roleIndices(conf *cfg.Config, role types.Role) error {
perms = append(perms, perm.Name) perms = append(perms, perm.Name)
} }
table.Entries[idx] = []string{ table.Entries[idx] = []any{priv.Names, perms, *priv.AllowRestrictedIndices}
strings.Join(priv.Names, ", "),
strings.Join(perms, ", "),
fmt.Sprintf("%t", *priv.AllowRestrictedIndices),
}
idx++ idx++
} }
@@ -231,10 +220,10 @@ func roleApplications(conf *cfg.Config, role types.Role) error {
table.Addheaders("application", "privileges", "resources") table.Addheaders("application", "privileges", "resources")
idx := 0 idx := 0
for _, priv := range role.Applications { for _, priv := range role.Applications {
table.Entries[idx] = []string{ table.Entries[idx] = []any{
priv.Application, priv.Application,
strings.Join(priv.Privileges, ", "), priv.Privileges,
strings.Join(priv.Resources, ", "), priv.Resources,
} }
idx++ idx++

View File

@@ -229,7 +229,7 @@ func RoleDiff(conf *cfg.Config, csvfile, role string) error {
defined = printer.Colorize(conf, "red", "undefined") defined = printer.Colorize(conf, "red", "undefined")
} }
table.Entries[idx] = []string{ table.Entries[idx] = []any{
row.name, row.name,
deployed, deployed,
defined, defined,

View File

@@ -107,7 +107,7 @@ func printShards(conf *cfg.Config, shardlist shards.Response) error {
for idx, shard := range shardlist { for idx, shard := range shardlist {
name := colorzizeShard(conf, *shard.State, *shard.Index) name := colorzizeShard(conf, *shard.State, *shard.Index)
table.Entries[idx] = []string{ table.Entries[idx] = []any{
name, name,
*shard.Shard, *shard.Shard,
resolvePrirep(*shard.Prirep), resolvePrirep(*shard.Prirep),

View File

@@ -88,7 +88,7 @@ func SnapshotList(conf *cfg.Config) error {
table.Addheaders("name", "index", "start", "orphaned", "status") table.Addheaders("name", "index", "start", "orphaned", "status")
for idx, snap := range snapshots { for idx, snap := range snapshots {
table.Entries[idx] = []string{ table.Entries[idx] = []any{
snap.Name, snap.Name,
snap.Forindex, snap.Forindex,
snap.Start, snap.Start,
@@ -122,20 +122,20 @@ func SnapshotShow(conf *cfg.Config, snapshot string) error {
snap := res.Snapshots[0] snap := res.Snapshots[0]
table.Entries = [][]string{ table.Entries = [][]any{
{"snapshot", snapshot}, {"snapshot", snapshot},
{"uuid", snap.Uuid}, {"uuid", snap.Uuid},
{"repository", *snap.Repository}, {"repository", *snap.Repository},
{"version_id", fmt.Sprintf("%d", snap.VersionId)}, {"version_id", snap.VersionId},
{"version", *snap.Version}, {"version", *snap.Version},
{"include_global_state", fmt.Sprintf("%t", *snap.IncludeGlobalState)}, {"include_global_state", *snap.IncludeGlobalState},
{"state", *snap.State}, {"state", *snap.State},
{"start_time", fmt.Sprintf("%s", snap.StartTime)}, {"start_time", snap.StartTime},
{"end_time", fmt.Sprintf("%s", snap.EndTime)}, {"end_time", snap.EndTime},
{"duration_in_millis", fmt.Sprintf("%d", *snap.DurationInMillis)}, {"duration_in_millis", *snap.DurationInMillis},
{"shards-total", fmt.Sprintf("%d", snap.Shards.Total)}, {"shards-total", snap.Shards.Total},
{"shards-failed", fmt.Sprintf("%d", snap.Shards.Failed)}, {"shards-failed", snap.Shards.Failed},
{"shards-successful", fmt.Sprintf("%d", snap.Shards.Successful)}, {"shards-successful", snap.Shards.Successful},
} }
if err := table.Print(); err != nil { if err := table.Print(); err != nil {

View File

@@ -49,10 +49,10 @@ func TaskList(conf *cfg.Config) error {
ts = time.Unix(int64(epc/1000), 0) ts = time.Unix(int64(epc/1000), 0)
} }
table.Entries[idx] = []string{ table.Entries[idx] = []any{
*task.TaskId, *task.TaskId,
*task.Action, *task.Action,
ts.Format("2006-01-02T15:04:05"), ts,
*task.RunningTime, *task.RunningTime,
*task.Node, *task.Node,
*task.Type, *task.Type,

31
pkg/printer/bytes.go Normal file
View File

@@ -0,0 +1,31 @@
/*
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 printer
import "github.com/dustin/go-humanize"
type ByteSize struct {
size uint64
}
func Bytes(size int64) ByteSize {
return ByteSize{size: uint64(size)}
}
func (b *ByteSize) String() string {
return humanize.Bytes(b.size)
}

97
pkg/printer/cast.go Normal file
View File

@@ -0,0 +1,97 @@
/*
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 printer
import (
"fmt"
"strconv"
"strings"
"time"
"github.com/elastic/go-elasticsearch/v9/typedapi/types"
)
func any2string(in any) string {
switch val := in.(type) {
case string:
return val
case bool:
return strconv.FormatBool(val)
case int64:
return strconv.FormatInt(val, 10)
case int:
return strconv.Itoa(val)
case float64:
return fmt.Sprintf("%.2f", val)
case []string:
return strings.Join(val, ",")
case ByteSize:
return val.String()
case time.Time:
return val.Format("2006-01-02 15:04:05")
case []byte:
return string(val)
case types.DateTime, types.Percentage:
return val.(string)
case nil:
return "null"
}
return ""
}
func (data *Table) preprocessRows() {
if data.processed {
// only do it once
return
}
// convert entries to strings
data.rows = make([][]string, len(data.Entries))
for rowidx, entries := range data.Entries {
data.rows[rowidx] = make([]string, len(entries))
for colidx, entry := range data.Entries[rowidx] {
data.rows[rowidx][colidx] = any2string(entry)
}
}
// determine header lenght's
for idx, head := range data.Headers {
data.lenHeaders[idx] = visibleLen(head)
}
// determine max width per column
for _, entries := range data.rows {
currentWidth := 0
for idx, entry := range entries {
length := visibleLen(entry)
if data.lenHeaders[idx] < length {
if length > currentWidth+data.maxwidth {
data.lenHeaders[idx] = data.maxwidth - currentWidth
} else {
data.lenHeaders[idx] = length
}
}
currentWidth += data.lenHeaders[idx]
}
}
data.processed = true
}

View File

@@ -52,6 +52,6 @@ func PrintDoc(conf *cfg.Config, hit types.Hit) {
value := gjson.Get(docjson, conf.Path) value := gjson.Get(docjson, conf.Path)
fmt.Println(value.String()) fmt.Println(value.String())
} else { } else {
fmt.Print(docjson) fmt.Println(docjson)
} }
} }

View File

@@ -25,18 +25,17 @@ import (
"strings" "strings"
"codeberg.org/scip/esctl/pkg/cfg" "codeberg.org/scip/esctl/pkg/cfg"
"github.com/olekukonko/tablewriter"
"github.com/olekukonko/tablewriter/renderer"
"github.com/olekukonko/tablewriter/tw"
"github.com/seeruk/go-wordwrap" "github.com/seeruk/go-wordwrap"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
) )
type Table struct { type Table struct {
Mode string // table, markdown, json Mode string // tsv, json, yaml
Headers []string Headers []string
Entries [][]string Entries [][]any
rows [][]string // representation used for printing
processed bool
lenHeaders []int lenHeaders []int
alignInts bool alignInts bool
maxwidth int maxwidth int
@@ -46,17 +45,33 @@ func NewTable(conf *cfg.Config, columns, rows int) *Table {
table := Table{Mode: conf.Output, maxwidth: cfg.GetTermWidth()} table := Table{Mode: conf.Output, maxwidth: cfg.GetTermWidth()}
table.Headers = make([]string, columns) table.Headers = make([]string, columns)
table.Entries = make([][]string, rows) table.Entries = make([][]any, rows)
table.lenHeaders = make([]int, columns) table.lenHeaders = make([]int, columns)
table.alignInts = conf.AlignInts table.alignInts = conf.AlignInts
return &table return &table
} }
func NewTableEmpty(conf *cfg.Config) *Table {
table := Table{Mode: conf.Output, maxwidth: cfg.GetTermWidth()}
table.alignInts = conf.AlignInts
return &table
}
func (table *Table) WithHeaders(headers ...string) *Table {
count := len(headers)
table.Entries = [][]any{}
table.lenHeaders = make([]int, count)
table.Headers = make([]string, count)
table.Addheaders(headers...)
return table
}
func (data *Table) Print() error { func (data *Table) Print() error {
switch data.Mode { switch data.Mode {
case "markdown", "md":
return data.PrintMarkdown()
case "json": case "json":
return data.PrintJSON() return data.PrintJSON()
case "yaml": case "yaml":
@@ -71,11 +86,11 @@ var (
) )
// needed for json and yaml output // needed for json and yaml output
func (data *Table) toMap() []map[string]string { func (data *Table) toMap() []map[string]any {
raw := make([]map[string]string, len(data.Entries)) raw := make([]map[string]any, len(data.Entries))
for idx, entries := range data.Entries { for idx, entries := range data.Entries {
raw[idx] = make(map[string]string, len(data.Headers)) raw[idx] = make(map[string]any, len(data.Headers))
for eidx, entry := range entries { for eidx, entry := range entries {
raw[idx][data.Headers[eidx]] = entry raw[idx][data.Headers[eidx]] = entry
} }
@@ -110,31 +125,9 @@ func (data *Table) PrintJSON() error {
return nil return nil
} }
// Well. tablewriter should be able to print in TSV mode, but it
// doesn't work because of the various ansic color escapes we're
// using. Therefore we do it the hard way and use our own TSV printer.
func (data *Table) PrintTSV() error { func (data *Table) PrintTSV() error {
// determine lenght's // length's, convert cell types
for idx, head := range data.Headers { data.preprocessRows()
data.lenHeaders[idx] = visibleLen(head)
}
for _, entries := range data.Entries {
currentWidth := 0
for idx, entry := range entries {
length := visibleLen(entry)
if data.lenHeaders[idx] < length {
if length > currentWidth+data.maxwidth {
data.lenHeaders[idx] = data.maxwidth - currentWidth
} else {
data.lenHeaders[idx] = length
}
}
currentWidth += data.lenHeaders[idx]
}
}
// output headers // output headers
for idx, header := range data.Headers { for idx, header := range data.Headers {
@@ -151,13 +144,13 @@ func (data *Table) PrintTSV() error {
} }
fmt.Println() fmt.Println()
for _, entries := range data.Entries { for _, entries := range data.rows {
currentWidth := 0 currentWidth := 0
for idx, entry := range entries { for idx, entry := range entries {
length := visibleLen(entry) length := visibleLen(entry)
if length+currentWidth > data.maxwidth { if length+currentWidth > data.maxwidth && data.maxwidth-currentWidth > 1 {
// text is too wide to be put into one line, wrap it // text is too wide to be put into one line, wrap it
wrapper := wordwrap.Wrapper(data.maxwidth-currentWidth, false) wrapper := wordwrap.Wrapper(data.maxwidth-currentWidth, false)
wrapped := wrapper(entry) wrapped := wrapper(entry)
@@ -195,130 +188,16 @@ func (data *Table) PrintTSV() error {
return nil return nil
} }
func (data *Table) PrintTabularFail() error {
OFS := " "
tableString := &strings.Builder{}
styleTSV := tw.NewSymbolCustom("space").WithColumn("\t")
table := tablewriter.NewTable(tableString,
tablewriter.WithRenderer(
renderer.NewBlueprint(tw.Rendition{
Borders: tw.BorderNone,
Symbols: styleTSV,
Settings: tw.Settings{
Separators: tw.SeparatorsNone,
Lines: tw.LinesNone,
},
})),
tablewriter.WithConfig(tablewriter.Config{
Header: tw.CellConfig{
Formatting: tw.CellFormatting{
AutoFormat: tw.Off,
},
Padding: tw.CellPadding{Global: tw.Padding{Left: "", Right: OFS}},
},
Row: tw.CellConfig{
Formatting: tw.CellFormatting{
AutoWrap: tw.WrapNone,
Alignment: tw.AlignLeft,
},
Padding: tw.CellPadding{Global: tw.Padding{Right: OFS}},
},
Debug: true,
}),
)
table.Header(data.Headers)
if err := table.Bulk(data.Entries); err != nil {
return fmt.Errorf("failed to add data to table renderer: %s", err)
}
if err := table.Render(); err != nil {
return fmt.Errorf("failed to render table: %s", err)
}
// we need to trim our output here, because tablewriter appends
// excess whitespace to our rows.
cleanedString := &strings.Builder{}
for _, row := range strings.Split(strings.TrimSpace(tableString.String()), "\n") {
cleanedString.WriteString(strings.TrimSpace(row))
cleanedString.WriteString("\n")
}
fmt.Println(cleanedString.String())
return nil
}
func (data *Table) PrintMarkdown() error {
tableString := &strings.Builder{}
table := tablewriter.NewTable(tableString,
tablewriter.WithRenderer(
renderer.NewBlueprint(
tw.Rendition{
Borders: tw.Border{
Left: tw.On,
Right: tw.On,
Top: tw.Off,
Bottom: tw.Off,
},
Settings: tw.Settings{
Separators: tw.Separators{
ShowHeader: tw.On,
ShowFooter: tw.Off,
BetweenRows: tw.Off,
BetweenColumns: 0,
},
},
Symbols: tw.NewSymbols(tw.StyleMarkdown),
})),
tablewriter.WithConfig(
tablewriter.Config{
Header: tw.CellConfig{
Formatting: tw.CellFormatting{
Alignment: tw.AlignLeft,
AutoFormat: tw.Off,
},
},
Row: tw.CellConfig{
Formatting: tw.CellFormatting{
Alignment: tw.AlignLeft,
},
},
},
),
)
table.Header(data.Headers)
if err := table.Bulk(data.Entries); err != nil {
return fmt.Errorf("failed to add data to table renderer: %s", err)
}
if err := table.Render(); err != nil {
return fmt.Errorf("failed to render table: %s", err)
}
fmt.Println(tableString.String())
return nil
}
func (data *Table) Sort() { func (data *Table) Sort() {
// sanity checks // sanity checks
if len(data.Entries) == 0 { if len(data.Entries) == 0 {
return return
} }
sort.Slice(data.Entries, func(i, j int) bool { data.preprocessRows()
return data.Entries[i][0] < data.Entries[j][0]
sort.Slice(data.rows, func(i, j int) bool {
return data.rows[i][0] < data.rows[j][0]
}) })
} }
@@ -333,7 +212,7 @@ func (data *Table) Addheaders(headers ...string) {
} }
} }
func (data *Table) AddRow(fields ...string) { func (data *Table) AddRow(fields ...any) {
data.Entries = append(data.Entries, fields) data.Entries = append(data.Entries, fields)
} }

6
t/.env Normal file
View File

@@ -0,0 +1,6 @@
ES_PASS=tuscador1
STACK_VERSION=9.4.2
CLUSTER_NAME=domdoc
LICENSE=basic
ES_PORT=9200
MEM_LIMIT=1073741824

46
t/Makefile Normal file
View File

@@ -0,0 +1,46 @@
.PHONY: test clean cluster docs search up down delete
# docker stuff
up:
@echo "booting up docker containers"
docker compose up -d --remove-orphans
waitup:
@echo "wait til es cluster is up and running"
mosscap msc-actions.yaml msc-docker.yaml -n -t 300
down:
@echo "shutting down docker containers"
docker compose down
delete:
@echo "delete docker volumes"
docker volume rm t_certs t_esdata01 t_esdata02 t_esdata03
clean-docker: down delete
# mosscap esctl stuff
test: up waitup cluster docs wait search
cluster:
@echo "setting up elastisearch cluster"
mosscap msc-actions.yaml msc-cluster.yaml -n
docs:
@echo "put some docs"
mosscap msc-actions.yaml msc-docs.yaml -n -c 1000 -p 100
wait:
sleep 10
search:
@echo "make some searches"
mosscap msc-actions.yaml msc-search.yaml -n -t 60
clean:
@echo "cleaning up cluster"
mosscap msc-actions.yaml msc-clean.yaml -n
rm -f *.state debug.log

77
t/README.md Normal file
View File

@@ -0,0 +1,77 @@
# Testing
Building regular unit tests would require to create a mock
elasticsearch cluster, which is too much a chore for a one man
show. Running a regular elasticsearch cluster on Codeberg CI is not
economically reasonable.
Therefore I run tests manually. Here's how.
# Dependencies
The following is required:
- linux, any distro will do
- docker in a decent version
- [mosscap](https://codeberg.org/scip/mosscap) version `0.0.17` or higher.
# Docker
There's a ready to use docker compose file, which fires up an
elasticsearch cluster with 3 nodes. To start it manually just execute
`make up`.
When it's up and running it should look like this:
```console
$ docker ps
NAMES STATUS PORTS
t-es03-1 Up 16 minutes (healthy) 9200/tcp, 9300/tcp
t-es02-1 Up 16 minutes (healthy) 9200/tcp, 9300/tcp
t-es01-1 Up 16 minutes (healthy) 0.0.0.0:9200->9200/tcp, [::]:9200->9200/tcp, 9300/tcp
```
There's an esctl config file `cluster.yaml`, which you can use to
access that elasticsearch cluster, e.g.:
```console
$ esctl -c cluster.yaml cluster status
DOCKER/DOMDOC STATUS
Cluster Name domdoc
ES Status green
ES Version 9.4.2
Is Leader false
Active Shards 8
Active Primary Shards 4
Unassigned Shards 0
Unassigned Primary Shards 0
Pending Tasks 0
Nodes 3
Red Indices 0
Long Running Tasks 0
```
# Automated Tests
To execute the automated tests, just execute `make test`. This boots
up the elasticsearch containers, waits until they are up and running,
creates data on it and queries it.
# Cleanup
To just clean up the elasticsearch cluster run `make clean`.
To remove everything, run `make clean-docker`.
# Reference
## Mosscap configs
| CONFIG | DESCRIPTION |
|------------------|----------------------------------------------------------------------|
| msc-actions.yaml | contains all actions, used by all item configs |
| msc-docker.yaml | single item: used to wait until elasticsearch containers are healthy |
| msc-cluster.yaml | single item: create indices etc |
| msc-docs.yaml | generator items: put docs into the index |
| msc-search.yaml | single item: do some searches |
| msc-clean.yaml | single item: clean up the elasticsearch cluster |

7
t/cluster.yaml Normal file
View File

@@ -0,0 +1,7 @@
# esctl config
clusters:
docker/domdoc:
uri: https://elastic:9200
user: elastic
token: ""
default: false

207
t/docker-compose.yaml Normal file
View File

@@ -0,0 +1,207 @@
services:
setup:
image: docker.elastic.co/elasticsearch/elasticsearch:${STACK_VERSION}
volumes:
- certs:/usr/share/elasticsearch/config/certs
user: "0"
command: >
bash -c '
if [ x${ES_PASS} == x ]; then
echo "Set the ES_PASS environment variable in the .env file";
exit 1;
fi;
if [ ! -f config/certs/ca.zip ]; then
echo "Creating CA";
bin/elasticsearch-certutil ca --silent --pem -out config/certs/ca.zip;
unzip config/certs/ca.zip -d config/certs;
fi;
if [ ! -f config/certs/certs.zip ]; then
echo "Creating certs";
echo -ne \
"instances:\n"\
" - name: es01\n"\
" dns:\n"\
" - es01\n"\
" - localhost\n"\
" ip:\n"\
" - 127.0.0.1\n"\
" - name: es02\n"\
" dns:\n"\
" - es02\n"\
" - localhost\n"\
" ip:\n"\
" - 127.0.0.1\n"\
" - name: es03\n"\
" dns:\n"\
" - es03\n"\
" - localhost\n"\
" ip:\n"\
" - 127.0.0.1\n"\
> config/certs/instances.yml;
bin/elasticsearch-certutil cert --silent --pem -out config/certs/certs.zip --in config/certs/instances.yml --ca-cert config/certs/ca/ca.crt --ca-key config/certs/ca/ca.key;
unzip config/certs/certs.zip -d config/certs;
fi;
echo "Setting file permissions"
chown -R root:root config/certs;
find . -type d -exec chmod 750 \{\} \;;
find . -type f -exec chmod 640 \{\} \;;
echo "Waiting for Elasticsearch availability";
until curl -s --cacert config/certs/ca/ca.crt https://es01:9200 | grep -q "missing authentication credentials"; do sleep 30; done;
echo "All done!";
'
healthcheck:
test: ["CMD-SHELL", "[ -f config/certs/es01/es01.crt ]"]
interval: 1s
timeout: 5s
retries: 120
es01:
depends_on:
setup:
condition: service_healthy
image: docker.elastic.co/elasticsearch/elasticsearch:${STACK_VERSION}
volumes:
- certs:/usr/share/elasticsearch/config/certs
- esdata01:/usr/share/elasticsearch/data
ports:
- ${ES_PORT}:9200
environment:
- node.name=es01
- cluster.name=${CLUSTER_NAME}
- cluster.initial_master_nodes=es01
- discovery.seed_hosts=es02
- ELASTIC_PASSWORD=${ES_PASS}
- bootstrap.memory_lock=true
- xpack.security.enabled=true
- xpack.security.http.ssl.enabled=true
- xpack.security.http.ssl.key=certs/es01/es01.key
- xpack.security.http.ssl.certificate=certs/es01/es01.crt
- xpack.security.http.ssl.certificate_authorities=certs/ca/ca.crt
- xpack.security.transport.ssl.enabled=true
- xpack.security.transport.ssl.key=certs/es01/es01.key
- xpack.security.transport.ssl.certificate=certs/es01/es01.crt
- xpack.security.transport.ssl.certificate_authorities=certs/ca/ca.crt
- xpack.security.transport.ssl.verification_mode=certificate
- xpack.license.self_generated.type=${LICENSE}
- xpack.ml.use_auto_machine_memory_percent=true
- node_roles=data_content
- xpack.searchable.snapshot.shared_cache.size="1gb"
- cluster.routing.allocation.disk.threshold_enabled=false
- cluster.routing.allocation.disk.watermark.low="2gb"
- cluster.routing.allocation.disk.watermark.high="1gb"
- cluster.routing.allocation.disk.watermark.flood_stage="500mb"
mem_limit: ${MEM_LIMIT}
ulimits:
memlock:
soft: -1
hard: -1
healthcheck:
test:
[
"CMD-SHELL",
"curl -s --cacert config/certs/ca/ca.crt https://localhost:9200 | grep -q 'missing authentication credentials'",
]
interval: 10s
timeout: 10s
retries: 120
es02:
depends_on:
- es01
image: docker.elastic.co/elasticsearch/elasticsearch:${STACK_VERSION}
volumes:
- certs:/usr/share/elasticsearch/config/certs
- esdata02:/usr/share/elasticsearch/data
environment:
- node.name=es02
- cluster.name=${CLUSTER_NAME}
- cluster.initial_master_nodes=es01,es02
- discovery.seed_hosts=es01
- ELASTIC_PASSWORD=${ES_PASS}
- bootstrap.memory_lock=true
- xpack.security.enabled=true
- xpack.security.http.ssl.enabled=true
- xpack.security.http.ssl.key=certs/es02/es02.key
- xpack.security.http.ssl.certificate=certs/es02/es02.crt
- xpack.security.http.ssl.certificate_authorities=certs/ca/ca.crt
- xpack.security.transport.ssl.enabled=true
- xpack.security.transport.ssl.key=certs/es02/es02.key
- xpack.security.transport.ssl.certificate=certs/es02/es02.crt
- xpack.security.transport.ssl.certificate_authorities=certs/ca/ca.crt
- xpack.security.transport.ssl.verification_mode=certificate
- xpack.license.self_generated.type=${LICENSE}
- xpack.ml.use_auto_machine_memory_percent=true
- cluster.routing.allocation.disk.threshold_enabled=false
- cluster.routing.allocation.disk.watermark.low="2gb"
- cluster.routing.allocation.disk.watermark.high="1gb"
- cluster.routing.allocation.disk.watermark.flood_stage="500mb"
mem_limit: ${MEM_LIMIT}
ulimits:
memlock:
soft: -1
hard: -1
healthcheck:
test:
[
"CMD-SHELL",
"curl -s --cacert config/certs/ca/ca.crt https://localhost:9200 | grep -q 'missing authentication credentials'",
]
interval: 10s
timeout: 10s
retries: 120
es03:
depends_on:
- es01
image: docker.elastic.co/elasticsearch/elasticsearch:${STACK_VERSION}
volumes:
- certs:/usr/share/elasticsearch/config/certs
- esdata03:/usr/share/elasticsearch/data
environment:
- node.name=es03
- cluster.name=${CLUSTER_NAME}
- cluster.initial_master_nodes=es01,es03
- discovery.seed_hosts=es01
- ELASTIC_PASSWORD=${ES_PASS}
- bootstrap.memory_lock=true
- xpack.security.enabled=true
- xpack.security.http.ssl.enabled=true
- xpack.security.http.ssl.key=certs/es03/es03.key
- xpack.security.http.ssl.certificate=certs/es03/es03.crt
- xpack.security.http.ssl.certificate_authorities=certs/ca/ca.crt
- xpack.security.transport.ssl.enabled=true
- xpack.security.transport.ssl.key=certs/es03/es03.key
- xpack.security.transport.ssl.certificate=certs/es03/es03.crt
- xpack.security.transport.ssl.certificate_authorities=certs/ca/ca.crt
- xpack.security.transport.ssl.verification_mode=certificate
- xpack.license.self_generated.type=${LICENSE}
- xpack.ml.use_auto_machine_memory_percent=true
- cluster.routing.allocation.disk.threshold_enabled=false
- cluster.routing.allocation.disk.watermark.low="2gb"
- cluster.routing.allocation.disk.watermark.high="1gb"
- cluster.routing.allocation.disk.watermark.flood_stage="500mb"
mem_limit: ${MEM_LIMIT}
ulimits:
memlock:
soft: -1
hard: -1
healthcheck:
test:
[
"CMD-SHELL",
"curl -s --cacert config/certs/ca/ca.crt https://localhost:9200 | grep -q 'missing authentication credentials'",
]
interval: 10s
timeout: 10s
retries: 120
volumes:
certs:
driver: local
esdata01:
driver: local
esdata02:
driver: local
esdata03:
driver: local

96
t/msc-actions.yaml Normal file
View File

@@ -0,0 +1,96 @@
#
# this is a mosscap action file, see: https://codeberg.org/scip/mosscap
vars:
esctl: ../esctl -c cluster.yaml
actions:
wait: |
sleep [[.time]]
docker_wait: |
for i in {1..20}; do
count=$(docker ps | grep healthy | wc -l)
if test $count -eq 3; then
echo "all containers are healthy"
exit
fi
sleep 5s
done
echo "not all containers are healthy"
docker ps
cluster_ls: |
[[.esctl]] cluster ls | grep -E domdoc.*reachable.*yes
cluster_status: |
[[.esctl]] cluster status | grep green
create_ilm: |
[[.esctl]] ilm create --hot-rollover-max-age 7d \
--hot-rollover-max-primary-shard-size 25g \
--warm-min-age 0 \
--delete-min-age 14d [[.policy]]
describe_ilm: |
[[.esctl]] ilm show [[.policy]] -t | grep -E "rollover when storage > 25g"
create_index_template: |
[[.esctl]] index template create [[.index]] -i [[.index]] \
-s number_of_shards:3 \
-s index.sort.field:@timestamp \
-r 1m --ilm-policy [[.policy]] \
user:keyword message:text @timestamp:date
describe_index_template: |
[[.esctl]] index template show [[.index]] | grep -E "index.sort.field.*@timestamp"
create_index: |
[[.esctl]] index create -s 2 -r 2 [[.index]]
describe_index: |
[[.esctl]] index show [[.index]] | grep -E "fields.*@timestamp"
index_ls: |
[[.esctl]] index ls | grep [[.index]]
index_template_rm: |
[[.esctl]] index template rm [[.index]]
index_rm: |
[[.esctl]] index rm [[.index]]
timestamp: |
date --iso-8601=second
date: |
date +%Y-%m-%d
doc_add: |
[[.esctl]] doc add -i [[.index]] '{"user": "[[.user]]", "message":"[[.message]]", "@timestamp":"[[.ts]]"}'
search_any: |
[[.esctl]] search -i [[.index]] -l 1 | jq .source.message
search_date: |
[[.esctl]] search -i [[.index]] -l 1 | jq '.source."@timestamp"' | grep [[.today]]
search_count: |
[[.esctl]] search -i [[.index]] -l 5000 | wc -l | grep 1000
search_content: |
[[.esctl]] search -i [[.index]] [[.content_pattern]] -l 1 | jq .source.message
search_content_and: |
[[.esctl]] search -i [[.index]] [[.content_and_pattern]] -l 1 | jq .source.message
nodes: |
[[.esctl]] node ls | grep es01
describe_node: |
[[.esctl]] node show [[.node]] | grep "Node version"
license: |
[[.esctl]] license show | grep -E "Expires.*never"

14
t/msc-clean.yaml Normal file
View File

@@ -0,0 +1,14 @@
#
# this is a mosscap item file, see: https://codeberg.org/scip/mosscap
items:
- name: docker/domdoc
vars:
index: test
tasks:
- name: delete_index
action: index_rm
- name: delete_template
action: index_template_rm

48
t/msc-cluster.yaml Normal file
View File

@@ -0,0 +1,48 @@
#
# this is a mosscap item file, see: https://codeberg.org/scip/mosscap
statefile: cluster.state
items:
- name: docker/domdoc
vars:
index: test
node: es01
policy: testpolicy
tasks:
- name: ls
action: cluster_ls
- name: license
action: license
- name: status
action: cluster_status
- name: ilm
action: create_ilm
- name: check_ilm
action: describe_ilm
- name: template
action: create_index_template
- name: check_template
action: describe_index_template
- name: index
action: create_index
- name: indexls
action: index_ls
- name: describe
action: describe_index
- name: nodes
action: nodes
- name: check_node
action: describe_node

12
t/msc-docker.yaml Normal file
View File

@@ -0,0 +1,12 @@
#
# this is a mosscap item file, see: https://codeberg.org/scip/mosscap
items:
- name: docker/domdoc
vars:
index: test
time: 5s
tasks:
- name: wait
action: docker_wait

21
t/msc-docs.yaml Normal file
View File

@@ -0,0 +1,21 @@
#
# this is a mosscap generator item file, see: https://codeberg.org/scip/mosscap
vars:
index: test
tasks:
- name: ts
action: timestamp
- name: add
action: doc_add
args:
ts: ts.result
generate:
user: $name
# will become something like:
# "content34 iBAXQ2Va Ko5gMwwu FEH2O99B"
# so we can search for "content3*" and get multiple matches
message: "content$int8 $rand make$int8 $rand"

31
t/msc-search.yaml Normal file
View File

@@ -0,0 +1,31 @@
#
# this is a mosscap item file, see: https://codeberg.org/scip/mosscap
items:
- name: docker/domdoc
vars:
index: test
time: 10s
content_pattern: "content3*"
content_and_pattern: "content3* make"
tasks:
- name: today
action: date
- name: search_any
action: search_any
- name: date
action: search_date
args:
today: today.result
- name: count
action: search_count
- name: content
action: search_content
- name: content_and
action: search_content_and