Compare commits

..

3 Commits

Author SHA1 Message Date
7f1018f0e1 satisfy linter 2026-07-10 15:06:17 +02:00
3486bc0370 add CSV output support 2026-07-10 15:04:55 +02:00
374ff99916 add searchql and help-esql commands 2026-07-10 15:04:41 +02:00
49 changed files with 275 additions and 683 deletions

View File

@@ -110,12 +110,3 @@ profile: buildlocal
./esctl api ls --profile-file cpu.profile ./esctl api ls --profile-file cpu.profile
go tool pprof -text esctl cpu.profile go tool pprof -text esctl cpu.profile
go tool pprof --http localhost:8888 ./esctl cpu.profile go tool pprof --http localhost:8888 ./esctl cpu.profile
docker-up:
make -C t up
docker-waitup:
make -C t up
docker-down:
make -C t down

View File

@@ -29,7 +29,6 @@ Features:
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
recursive searches like: `(cond1 AND cond2) OR (cond3 OR cond4)`. recursive searches like: `(cond1 AND cond2) OR (cond3 OR cond4)`.
- Search using ES|QL language: `esctl searchql`.
- Cross cluster replication (ccr): view, pause, resume, delete - Cross cluster replication (ccr): view, pause, resume, delete
replication. You can also manage follower configuration. replication. You can also manage follower configuration.
- Index management: manage aliases, create, modify, delete indices, - Index management: manage aliases, create, modify, delete indices,
@@ -216,7 +215,7 @@ Pending Tasks 0
Nodes 3 Nodes 3
Red Indices 0 Red Indices 0
Long Running Tasks 2 Long Running Tasks 2
indices 221 Indicies 221
Docs 9854777 Docs 9854777
Total Size 3.4 GB Total Size 3.4 GB
Total Queries 4210411 Total Queries 4210411
@@ -353,30 +352,6 @@ $ esctl search -i foo* -F title=zeitbuchung message=pause | jq
} }
``` ```
You can also search using [ES|QL](https://www.elastic.co/docs/reference/query-languages/esql/esql-getting-started):
```console
$ esctl searchql "from hyperdrive | sort @timestamp | limit 5"
@TIMESTAMP MESSAGE TAG
2026-06-24T08:24:56.000Z arosu loop
2026-06-24T08:24:58.000Z hami loop
2026-06-24T08:24:59.000Z ishininu loop
2026-06-24T08:25:00.000Z uyomoruron loop
2026-06-24T08:25:02.000Z ishimime loop
```
There are several output modes (json, yaml, csv), to get esql output as CSV:
```console
$ esctl searchql "from hyperdrive | sort @timestamp | limit 5" -o csv
@timestamp,message,tag
2026-06-24T08:24:56.000Z,arosu,loop
2026-06-24T08:24:58.000Z,hami,loop
2026-06-24T08:24:59.000Z,ishininu,loop
2026-06-24T08:25:00.000Z,uyomoruron,loop
2026-06-24T08:25:02.000Z,ishimime,loop
```
To check which field mappings are available for an index: To check which field mappings are available for an index:
```console ```console
$ esctl index show foo2 $ esctl index show foo2
@@ -534,7 +509,7 @@ cluster - manage cluster[s]
allocate-empty-primary - allocate an empty primary shard to a node allocate-empty-primary - allocate an empty primary shard to a node
allocate-stale-primary - allocate a stale primary shard to a node allocate-stale-primary - allocate a stale primary shard to a node
datastream - manage data streams datastream - manage data streams
list - list indices list - list indicies
show - show details about an data stream show - show details about an data stream
create - create a new data stream create - create a new data stream
delete - delete a data stream delete - delete a data stream
@@ -554,8 +529,8 @@ ilm - manage index lifecycle
list - list index rollover config list - list index rollover config
show - show rollover forecast over all indices show - show rollover forecast over all indices
explain - explain ilm condition of an index explain - explain ilm condition of an index
index - manage indices index - manage indicies
list - list indices list - list indicies
show - show details about an index show - show details about an index
create - create a new index create - create a new index
update - update an index update - update an index
@@ -564,7 +539,6 @@ index - manage indices
fields - show info about field capabilities fields - show info about field capabilities
ilm - show ilm status ilm - show ilm status
du - show index disk usage du - show index disk usage
copy - copy (reindex) documents from one index to another
alias - manage index aliases alias - manage index aliases
create - create an index alias create - create an index alias
list - list index aliases list - list index aliases
@@ -588,7 +562,6 @@ role - manage roles
show - show details about a role show - show details about a role
diff - show differences between roles and CSV baseline diff - show differences between roles and CSV baseline
search - search within an index search - search within an index
searchql - search using ES/QL language
shard - manage shards shard - manage shards
list - list shards list - list shards
show - show details about a shard show - show details about a shard

View File

@@ -5,3 +5,5 @@
- add datastream support: - add datastream support:
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

View File

@@ -52,7 +52,7 @@ func CcrStatus(conf *cfg.Config) *cli.Command {
Flags: []cli.Flag{ Flags: []cli.Flag{
&cli.StringFlag{ &cli.StringFlag{
Name: "exclude", Name: "exclude",
Usage: "regexp of indices to exclude", Usage: "regexp of indicies to exclude",
Destination: &conf.Exclude, Destination: &conf.Exclude,
Aliases: []string{"e"}, Aliases: []string{"e"},
}, },

View File

@@ -34,7 +34,6 @@ const (
Cilm Cilm
Cnode Cnode
Cclustersettings Cclustersettings
Cindextemplate
) )
func complete(conf *cfg.Config, cmd *cli.Command, what int) { func complete(conf *cfg.Config, cmd *cli.Command, what int) {
@@ -66,8 +65,6 @@ func complete(conf *cfg.Config, cmd *cli.Command, what int) {
list, err = es.NodeNames(conf) list, err = es.NodeNames(conf)
case Cclustersettings: case Cclustersettings:
list, err = es.ClusterSettingsNames(conf) list, err = es.ClusterSettingsNames(conf)
case Cindextemplate:
list, err = es.IndexTemplateNames(conf)
} }
if err != nil { if err != nil {

View File

@@ -46,7 +46,7 @@ func DatastreamList(conf *cfg.Config) *cli.Command {
return &cli.Command{ return &cli.Command{
Name: "list", Name: "list",
Aliases: []string{"ls"}, Aliases: []string{"ls"},
Usage: "list indices", Usage: "list indicies",
Flags: []cli.Flag{ Flags: []cli.Flag{
&cli.IntFlag{ &cli.IntFlag{
@@ -169,7 +169,7 @@ func DatastreamRollover(conf *cfg.Config) *cli.Command {
Usage: "roll over after max age (eg: 7d, 2m, 8h)", Usage: "roll over after max age (eg: 7d, 2m, 8h)",
Destination: &conf.MaxAge, Destination: &conf.MaxAge,
}, },
&cli.Int64Flag{ &cli.IntFlag{
Name: "max-docs", Name: "max-docs",
Usage: "roll over after max docs", Usage: "roll over after max docs",
Destination: &conf.MaxDocs, Destination: &conf.MaxDocs,

View File

@@ -89,7 +89,7 @@ func IlmForecastList(conf *cfg.Config) *cli.Command {
}, },
&cli.BoolFlag{ &cli.BoolFlag{
Name: "hidden", Name: "hidden",
Usage: "include hidden indices", Usage: "include hidden indicies",
Destination: &conf.Hidden, Destination: &conf.Hidden,
Aliases: []string{"H"}, Aliases: []string{"H"},
}, },

View File

@@ -30,7 +30,7 @@ func Index(conf *cfg.Config) *cli.Command {
return &cli.Command{ return &cli.Command{
Name: "index", Name: "index",
Aliases: []string{"i"}, Aliases: []string{"i"},
Usage: "manage indices", Usage: "manage indicies",
Commands: []*cli.Command{ Commands: []*cli.Command{
IndexList(conf), IndexList(conf),
@@ -42,7 +42,6 @@ func Index(conf *cfg.Config) *cli.Command {
IndexFields(conf), IndexFields(conf),
IndexIlm(conf), IndexIlm(conf),
IndexDu(conf), IndexDu(conf),
IndexCopy(conf),
// sub commands // sub commands
IndexAlias(conf), IndexAlias(conf),
@@ -55,7 +54,7 @@ func IndexList(conf *cfg.Config) *cli.Command {
return &cli.Command{ return &cli.Command{
Name: "list", Name: "list",
Aliases: []string{"ls"}, Aliases: []string{"ls"},
Usage: "list indices", Usage: "list indicies",
Flags: []cli.Flag{ Flags: []cli.Flag{
&cli.IntFlag{ &cli.IntFlag{
@@ -66,25 +65,25 @@ func IndexList(conf *cfg.Config) *cli.Command {
}, },
&cli.BoolFlag{ &cli.BoolFlag{
Name: "partials", Name: "partials",
Usage: "include partial indices", Usage: "include partial indicies",
Destination: &conf.Partials, Destination: &conf.Partials,
Aliases: []string{"p"}, Aliases: []string{"p"},
}, },
&cli.BoolFlag{ &cli.BoolFlag{
Name: "hidden", Name: "hidden",
Usage: "include hidden indices", Usage: "include hidden indicies",
Destination: &conf.Hidden, Destination: &conf.Hidden,
Aliases: []string{"H"}, Aliases: []string{"H"},
}, },
&cli.BoolFlag{ &cli.BoolFlag{
Name: "failed", Name: "failed",
Usage: "include only red failed indices", Usage: "include only red failed indicies",
Destination: &conf.Failed, Destination: &conf.Failed,
Aliases: []string{"f"}, Aliases: []string{"f"},
}, },
&cli.StringSliceFlag{ &cli.StringSliceFlag{
Name: "filter", Name: "filter",
Usage: "show only indices matching the filter", Usage: "show only indicies matching the filter",
Destination: &conf.Filter, Destination: &conf.Filter,
Aliases: []string{"F"}, Aliases: []string{"F"},
}, },
@@ -300,82 +299,3 @@ func IndexDu(conf *cfg.Config) *cli.Command {
}, },
} }
} }
func IndexCopy(conf *cfg.Config) *cli.Command {
return &cli.Command{
Name: "copy",
Aliases: []string{"alias", "cp"},
Usage: "copy (reindex) documents from one index to another",
UsageText: "index copy [options] -s <source-index> -t <dest-index>",
Flags: []cli.Flag{
// FIXME: not implemented by typed API
// &cli.BoolFlag{
// Name: "missing", // op_type
// Usage: "copy only missing docs",
// Destination: &conf.Missing,
// Aliases: []string{"m"},
// },
// &cli.BoolFlag{
// Name: "sync", // version_type to external
// Usage: "create missing docs and update outdated docs",
// Destination: &conf.Sync,
// Aliases: []string{"S"},
// },
&cli.BoolFlag{
Name: "force", // conflicts to proceed
Usage: "continue reindexing even when conflicts happen",
Destination: &conf.Force,
Aliases: []string{"f"},
},
&cli.Float64Flag{
Name: "requests-per-second",
Usage: "the maximum number of documents to index per second (-1 turns off throttling)",
Destination: &conf.RequestsPerSecond,
Aliases: []string{"R"},
},
&cli.Int64Flag{
Name: "max-docs",
Usage: "the maximum number of documents to reindex",
Destination: &conf.MaxDocs,
Aliases: []string{"m"},
},
&cli.DurationFlag{
Name: "timeout",
Usage: "timeout for write operations (e.g. 300m or 120s)",
Destination: &conf.Timeout,
Aliases: []string{"T"},
},
&cli.BoolFlag{
Name: "refresh",
Usage: "refresh affected shards to make this operation visible to search",
Destination: &conf.Refresh,
Aliases: []string{"r"},
},
&cli.BoolFlag{
Name: "wait",
Usage: "wait for active shards",
Destination: &conf.Wait,
Aliases: []string{"w"},
},
&cli.StringSliceFlag{
Name: "source",
Usage: "source index (multiple supported)",
Destination: &conf.SourceIndices,
Aliases: []string{"s"},
Required: true,
},
&cli.StringFlag{
Name: "target",
Usage: "target index",
Destination: &conf.Index,
Aliases: []string{"t"},
Required: true,
},
},
Action: func(ctx context.Context, cmd *cli.Command) error {
return es.IndexCopy(conf)
},
}
}

View File

@@ -121,7 +121,7 @@ func IndexAliasRollover(conf *cfg.Config) *cli.Command {
Usage: "roll over after max age (eg: 7d, 2m, 8h)", Usage: "roll over after max age (eg: 7d, 2m, 8h)",
Destination: &conf.MaxAge, Destination: &conf.MaxAge,
}, },
&cli.Int64Flag{ &cli.IntFlag{
Name: "max-docs", Name: "max-docs",
Usage: "roll over after max docs", Usage: "roll over after max docs",
Destination: &conf.MaxDocs, Destination: &conf.MaxDocs,
@@ -159,7 +159,7 @@ func IndexAliasList(conf *cfg.Config) *cli.Command {
Flags: []cli.Flag{ Flags: []cli.Flag{
&cli.StringSliceFlag{ &cli.StringSliceFlag{
Name: "filter", Name: "filter",
Usage: "show only aliases for indices matching the filter", Usage: "show only aliases for indicies matching the filter",
Destination: &conf.Filter, Destination: &conf.Filter,
Aliases: []string{"F"}, Aliases: []string{"F"},
}, },

View File

@@ -49,23 +49,8 @@ func IndexTemplateList(conf *cfg.Config) *cli.Command {
Aliases: []string{"ls"}, Aliases: []string{"ls"},
Usage: "list index templates", Usage: "list index templates",
Flags: []cli.Flag{
&cli.StringSliceFlag{
Name: "filter-index-pattern",
Usage: "show only index templates which use patterns, which match the filter",
Destination: &conf.Filter,
Aliases: []string{"F"},
},
&cli.BoolFlag{
Name: "hidden",
Usage: "show hidden index templates as well",
Destination: &conf.Hidden,
Aliases: []string{"H"},
},
},
Action: func(ctx context.Context, cmd *cli.Command) error { Action: func(ctx context.Context, cmd *cli.Command) error {
return es.IndexTemplateList(conf, cmd.Args().Get(0)) return es.IndexTemplateList(conf)
}, },
} }
} }
@@ -86,7 +71,7 @@ func IndexTemplateShow(conf *cfg.Config) *cli.Command {
}, },
ShellComplete: func(ctx context.Context, cmd *cli.Command) { ShellComplete: func(ctx context.Context, cmd *cli.Command) {
complete(conf, cmd, Cindextemplate) complete(conf, cmd, Cindex)
}, },
} }
} }
@@ -230,7 +215,7 @@ func IndexTemplateDelete(conf *cfg.Config) *cli.Command {
}, },
ShellComplete: func(ctx context.Context, cmd *cli.Command) { ShellComplete: func(ctx context.Context, cmd *cli.Command) {
complete(conf, cmd, Cindextemplate) complete(conf, cmd, Cindex)
}, },
} }
} }

View File

@@ -21,7 +21,6 @@ import (
"fmt" "fmt"
golog "log" golog "log"
"os" "os"
"runtime/debug"
"runtime/pprof" "runtime/pprof"
"strings" "strings"
@@ -119,12 +118,6 @@ func Main() int {
Usage: "enable HTTP debugging", Usage: "enable HTTP debugging",
Destination: &conf.DebugHTTP, Destination: &conf.DebugHTTP,
}, },
&cli.BoolFlag{
Name: "debug-goroutines",
Value: false,
Usage: "enable goroutine debugging",
Destination: &conf.DebugGoRoutines,
},
&cli.BoolFlag{ &cli.BoolFlag{
Name: "align-ints", Name: "align-ints",
Aliases: []string{"I"}, Aliases: []string{"I"},
@@ -260,12 +253,6 @@ func Version(conf *cfg.Config) *cli.Command {
Usage: "show esctl version information", Usage: "show esctl version information",
Action: func(ctx context.Context, cmd *cli.Command) error { Action: func(ctx context.Context, cmd *cli.Command) error {
info, _ := debug.ReadBuildInfo()
if strings.Contains(info.Main.Version, "+dirty") {
cfg.COMMIT += "+dirty"
}
_, err := fmt.Printf(versionFmt, _, err := fmt.Printf(versionFmt,
cfg.Version, cfg.BUILD, cfg.BRANCH, cfg.COMMIT, cfg.GOVERSION, cfg.APIVERSION) cfg.Version, cfg.BUILD, cfg.BRANCH, cfg.COMMIT, cfg.GOVERSION, cfg.APIVERSION)

4
go.mod
View File

@@ -17,9 +17,8 @@ module codeberg.org/scip/esctl
go 1.26 go 1.26
require ( require (
codeberg.org/scip/mapmap v0.0.2
github.com/MichaelMure/go-term-markdown v0.1.4 github.com/MichaelMure/go-term-markdown v0.1.4
github.com/alecthomas/repr v0.5.3 github.com/alecthomas/repr v0.5.2
github.com/charmbracelet/bubbles v1.0.0 github.com/charmbracelet/bubbles v1.0.0
github.com/charmbracelet/bubbletea v1.3.10 github.com/charmbracelet/bubbletea v1.3.10
github.com/charmbracelet/lipgloss v1.1.0 github.com/charmbracelet/lipgloss v1.1.0
@@ -32,6 +31,7 @@ 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/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

8
go.sum
View File

@@ -1,5 +1,3 @@
codeberg.org/scip/mapmap v0.0.2 h1:0i61jOUwFmGVwPukrMzrx0Fi4T9Qru2nlmibuaJimBo=
codeberg.org/scip/mapmap v0.0.2/go.mod h1:/ojYo2P7dMA2FWEu+jHKmsKPeq5yDcCvXeHevqZd5OI=
github.com/MichaelMure/go-term-markdown v0.1.4 h1:Ir3kBXDUtOX7dEv0EaQV8CNPpH+T7AfTh0eniMOtNcs= github.com/MichaelMure/go-term-markdown v0.1.4 h1:Ir3kBXDUtOX7dEv0EaQV8CNPpH+T7AfTh0eniMOtNcs=
github.com/MichaelMure/go-term-markdown v0.1.4/go.mod h1:EhcA3+pKYnlUsxYKBJ5Sn1cTQmmBMjeNlpV8nRb+JxA= github.com/MichaelMure/go-term-markdown v0.1.4/go.mod h1:EhcA3+pKYnlUsxYKBJ5Sn1cTQmmBMjeNlpV8nRb+JxA=
github.com/MichaelMure/go-term-text v0.3.1 h1:Kw9kZanyZWiCHOYu9v/8pWEgDQ6UVN9/ix2Vd2zzWf0= github.com/MichaelMure/go-term-text v0.3.1 h1:Kw9kZanyZWiCHOYu9v/8pWEgDQ6UVN9/ix2Vd2zzWf0=
@@ -12,8 +10,8 @@ github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721 h1:JHZL0hZKJ1VEN
github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721/go.mod h1:QO9JBoKquHd+jz9nshCh40fOfO+JzsoXy8qTHF68zU0= github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721/go.mod h1:QO9JBoKquHd+jz9nshCh40fOfO+JzsoXy8qTHF68zU0=
github.com/alecthomas/kong v0.2.1-0.20190708041108-0548c6b1afae/go.mod h1:+inYUSluD+p4L8KdviBSgzcqEjUQOfC5fQDRFuc36lI= github.com/alecthomas/kong v0.2.1-0.20190708041108-0548c6b1afae/go.mod h1:+inYUSluD+p4L8KdviBSgzcqEjUQOfC5fQDRFuc36lI=
github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897/go.mod h1:xTS7Pm1pD1mvyM075QCDSRqH6qRLXylzS24ZTpRiSzQ= github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897/go.mod h1:xTS7Pm1pD1mvyM075QCDSRqH6qRLXylzS24ZTpRiSzQ=
github.com/alecthomas/repr v0.5.3 h1:Ebk3yZ0kvrHC7TkTHLDJGDq1LxKeu9sQBQREFMcesS8= github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs=
github.com/alecthomas/repr v0.5.3/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/charmbracelet/bubbles v1.0.0 h1:12J8/ak/uCZEMQ6KU7pcfwceyjLlWsDLAxB5fXonfvc= github.com/charmbracelet/bubbles v1.0.0 h1:12J8/ak/uCZEMQ6KU7pcfwceyjLlWsDLAxB5fXonfvc=
@@ -153,6 +151,8 @@ github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/seeruk/go-wordwrap v0.0.0-20191208221741-14ec4aac9550 h1:C3CfUXH/qmWuQFRqnPm3Sx8PFxa+pqACjhV5CaNO8pw=
github.com/seeruk/go-wordwrap v0.0.0-20191208221741-14ec4aac9550/go.mod h1:Sl541M2Em6rRG3V9WObycR7MYFZiERVkd/TJg0Gt0U4=
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=

View File

@@ -194,18 +194,18 @@ func (cluster *Cluster) getDefaultOptions() []elasticsearch.Option {
} }
func (cluster *Cluster) getTransport() elastictransport.Option { func (cluster *Cluster) getTransport() elastictransport.Option {
transport := new(http.Transport{ transport := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}) }
if cluster.DebugHTTP { if cluster.DebugHTTP {
return elastictransport.WithTransport( return elastictransport.WithTransport(
new(DebugTransport{Transport: transport}), &DebugTransport{Transport: transport},
) )
} }
return elastictransport.WithTransport( return elastictransport.WithTransport(
new(CompatibilityTransport{Transport: transport}), &CompatibilityTransport{Transport: transport},
) )
} }

View File

@@ -22,14 +22,13 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"reflect" "reflect"
"time"
"github.com/alecthomas/repr" "github.com/alecthomas/repr"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
) )
const ( const (
Version string = `v0.0.27` Version string = `v0.0.26`
) )
var ( var (
@@ -69,13 +68,6 @@ type Config struct {
Retention string // index template create: -r Retention string // index template create: -r
Rollover bool // index template create: -R Rollover bool // index template create: -R
Missing bool // index copy: -m
Sync bool // index copy: -S
RequestsPerSecond float64 // index copy: -R
Timeout time.Duration // index copy: -T
Refresh bool // index copy: -r
SourceIndices []string // index copy: -s
From, To, MaxItems int // search: flags From, To, MaxItems int // search: flags
Filter []string // search: -F Filter []string // search: -F
Path string // search+doc sh: -p Path string // search+doc sh: -p
@@ -98,7 +90,6 @@ type Config struct {
Force bool // ccr follower renew: -f Force bool // ccr follower renew: -f
DebugHTTP bool // root: --debug-http DebugHTTP bool // root: --debug-http
DebugGoRoutines bool // root: --debug-goroutines
Separator string // role diff: -s Separator string // role diff: -s
NotDeployed bool // role diff: -n NotDeployed bool // role diff: -n
Undefined bool // role diff: -u Undefined bool // role diff: -u
@@ -107,8 +98,7 @@ type Config struct {
// rollover // rollover
MaxAge string MaxAge string
MaxDocs int64 // roll over, plus others MaxDocs, MaxShardSize, MaxShardDocs int // roll over
MaxShardSize, MaxShardDocs int // roll over
DryRun bool // rollover: -n DryRun bool // rollover: -n
Tag string // api ls: -t Tag string // api ls: -t
@@ -123,7 +113,7 @@ type Config struct {
} }
func NewConfig() *Config { func NewConfig() *Config {
return new(Config{Clusters: map[string]*Cluster{}}) return &Config{Clusters: map[string]*Cluster{}}
} }
func getDefaultPath() string { func getDefaultPath() string {
@@ -226,7 +216,7 @@ func (conf *Config) LoadConfig() error {
return fmt.Errorf("failed to read config file: %w", err) return fmt.Errorf("failed to read config file: %w", err)
} }
newconf := new(Config{}) newconf := &Config{}
err = yaml.Unmarshal(data, newconf) err = yaml.Unmarshal(data, newconf)
if err != nil { if err != nil {

View File

@@ -28,7 +28,6 @@ import (
"io" "io"
"log" "log"
"log/slog" "log/slog"
"maps"
"net/http" "net/http"
"os" "os"
"os/exec" "os/exec"
@@ -159,7 +158,7 @@ func ApiRepl(conf *cfg.Config) error {
} }
func pageJsonOutput(conf *cfg.Config, raw []byte) { func pageJsonOutput(conf *cfg.Config, raw []byte) {
tmpconf := new(cfg.Config{HaveJQ: conf.HaveJQ}) tmpconf := &cfg.Config{HaveJQ: conf.HaveJQ}
if conf.Pager != "" { if conf.Pager != "" {
tmpconf.HaveJQ = false tmpconf.HaveJQ = false
@@ -207,16 +206,16 @@ func CallAPI(conf *cfg.Config, verb, path, data string) ([]byte, error) {
verb = strings.ToUpper(verb) verb = strings.ToUpper(verb)
// we're using port-forwards anyway // we're using port-forwards anyway
noVerifyTransport := new(http.Transport{ noVerifyTransport := &http.Transport{
TLSClientConfig: new(tls.Config{InsecureSkipVerify: true}), TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}) }
client := new(http.Client{Transport: noVerifyTransport}) client := &http.Client{Transport: noVerifyTransport}
if conf.DebugHTTP { if conf.DebugHTTP {
client = new(http.Client{ client = &http.Client{
Transport: new(cfg.DebugTransport{ Transport: &cfg.DebugTransport{
Transport: noVerifyTransport})}) Transport: noVerifyTransport}}
} }
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)))
@@ -328,7 +327,7 @@ func ApiList(conf *cfg.Config, pattern string) error {
filter := regexp.MustCompile(pattern) filter := regexp.MustCompile(pattern)
table := printer.NewTable(conf).WithSize(4, 0) table := printer.NewTable(conf, 4, 0)
table.Addheaders("path", "http verb", "tag", "description") table.Addheaders("path", "http verb", "tag", "description")
for path, item := range assets.OpenAPI.Spec().Paths.Paths { for path, item := range assets.OpenAPI.Spec().Paths.Paths {
@@ -367,7 +366,16 @@ func ApiList(conf *cfg.Config, pattern string) error {
func ApiPathNames() []string { func ApiPathNames() []string {
assets.LoadAssetOpenApi() assets.LoadAssetOpenApi()
return slices.Collect(maps.Keys(assets.OpenAPI.Spec().Paths.Paths)) paths := make([]string, len(assets.OpenAPI.Spec().Paths.Paths))
idx := 0
for path := range assets.OpenAPI.Spec().Paths.Paths {
paths[idx] = path
idx++
}
return paths
} }
func ApiShow(conf *cfg.Config, showpath, verb string) error { func ApiShow(conf *cfg.Config, showpath, verb string) error {
@@ -528,7 +536,7 @@ func getApiExample(op *Op) string {
// otherwise showpath+verb have to match precisely. // otherwise showpath+verb have to match precisely.
func matchOperation(showpath, verb string) (*Op, error) { func matchOperation(showpath, verb string) (*Op, error) {
ops := []*Op{} ops := []*Op{}
op := new(Op{}) op := &Op{}
var found bool var found bool

View File

@@ -57,7 +57,7 @@ func CcrStatus(conf *cfg.Config, leader, follower string) error {
res, err := conf.Clusters[alias].ES().Cat.Indices(). res, err := conf.Clusters[alias].ES().Cat.Indices().
Do(context.Background()) Do(context.Background())
if err != nil { if err != nil {
return fmt.Errorf("failed to get indices on %s: %w", alias, esErrorString(err)) return fmt.Errorf("failed to get indicies on %s: %w", alias, esErrorString(err))
} }
indices[alias] = make(map[string]*types.IndicesRecord, len(res)) indices[alias] = make(map[string]*types.IndicesRecord, len(res))
@@ -111,7 +111,7 @@ func CcrRemoteInfo(conf *cfg.Config, index string) error {
mode = "leader" mode = "leader"
} }
table := printer.NewTable(conf).WithSize(2, 5) table := printer.NewTable(conf, 2, 5)
table.Addheaders("ccr remote property", "value") table.Addheaders("ccr remote property", "value")
table.Entries = [][]any{ table.Entries = [][]any{

View File

@@ -164,7 +164,7 @@ func CcrFollowerShow(conf *cfg.Config, index string) error {
follower := res.Indices[0].Shards[0] follower := res.Indices[0].Shards[0]
table := printer.NewTable(conf).WithSize(2, 9) table := printer.NewTable(conf, 2, 9)
table.Addheaders("ccr follower property", "value") table.Addheaders("ccr follower property", "value")
table.Entries = [][]any{ table.Entries = [][]any{

View File

@@ -17,10 +17,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
package es package es
import ( import (
// "encoding/json/jsontext"
// "encoding/json/v2"
"encoding/json" "encoding/json"
"fmt" "fmt"
@@ -53,22 +49,11 @@ func getHealthReport(conf *cfg.Config) (*HealthReport, error) {
return nil, err return nil, err
} }
report := new(HealthReport{}) report := HealthReport{}
// FIXME: use this once jsonv2 is no more experimental it already
// builds and works like intended, but golangci-lint doesn't
// recognize it with: go: unknown GOEXPERIMENT jsonv2
//
// if err := json.UnmarshalDecode(
// jsontext.NewDecoder(
// bytes.NewBuffer(raw)),
// &report); err != nil {
// return nil, fmt.Errorf("failed to unmarshal healthreport response: %w", err)
// }
if err := json.Unmarshal(raw, &report); err != nil { if err := json.Unmarshal(raw, &report); err != nil {
return nil, fmt.Errorf("failed to unmarshal healthreport response: %w", err) return nil, fmt.Errorf("failed to unmarshal healthreport response: %w", err)
} }
return report, nil return &report, nil
} }

View File

@@ -58,7 +58,7 @@ func ClusterList(conf *cfg.Config) error {
wg.Wait() wg.Wait()
table := printer.NewTable(conf).WithSize(5, len(conf.Clusters)) 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
@@ -101,39 +101,23 @@ func getClusterStatus(conf *cfg.Config) (*apiResponse, error) {
es := conf.DefaultCluster.ES() es := conf.DefaultCluster.ES()
responses := make(chan apiResponse, gocount) responses := make(chan apiResponse, gocount)
wg := new(sync.WaitGroup{}) wg := &sync.WaitGroup{}
wg.Go(func() { wg.Add(gocount)
getApiData(conf, es, responses, "health") go getApiData(conf, es, wg, responses, "health")
}) go getApiData(conf, es, wg, responses, "healthreport")
go getApiData(conf, es, wg, responses, "info")
wg.Go(func() { go getApiData(conf, es, wg, responses, "ccr")
getApiData(conf, es, responses, "healthreport") go getApiData(conf, es, wg, responses, "indices")
}) go getApiData(conf, es, wg, responses, "tasks")
wg.Go(func() {
getApiData(conf, es, responses, "info")
})
wg.Go(func() {
getApiData(conf, es, responses, "ccr")
})
wg.Go(func() {
getApiData(conf, es, responses, "indices")
})
wg.Go(func() {
getApiData(conf, es, responses, "tasks")
})
if conf.Verbose { if conf.Verbose {
getApiData(conf, es, responses, "stats") go getApiData(conf, es, wg, responses, "stats")
} }
wg.Wait() wg.Wait()
all := new(apiResponse{}) all := apiResponse{}
var err error var err error
@@ -160,7 +144,7 @@ func getClusterStatus(conf *cfg.Config) (*apiResponse, error) {
} }
} }
return all, err return &all, err
} }
func ClusterStatus(conf *cfg.Config) error { func ClusterStatus(conf *cfg.Config) error {
@@ -212,7 +196,7 @@ func ClusterStatus(conf *cfg.Config) error {
} }
} }
table := printer.NewTable(conf).WithSize(2, 7) table := printer.NewTable(conf, 2, 7)
table.Addheaders(conf.DefaultCluster.Name, "status") table.Addheaders(conf.DefaultCluster.Name, "status")
table.Entries = [][]any{ table.Entries = [][]any{
@@ -288,7 +272,7 @@ func gatherClusterStats(clusterstats *clusterstats.Response, table *printer.Tabl
} }
table.Entries = append(table.Entries, [][]any{ table.Entries = append(table.Entries, [][]any{
{"indices", clusterstats.Indices.Count}, {"Indicies", clusterstats.Indices.Count},
{"Docs", clusterstats.Indices.Docs.Count}, {"Docs", clusterstats.Indices.Docs.Count},
{"Total Size", printer.Bytes(clusterstats.Indices.Docs.TotalSizeInBytes)}, {"Total Size", printer.Bytes(clusterstats.Indices.Docs.TotalSizeInBytes)},
{"Total Queries", "%d", querycount}, {"Total Queries", "%d", querycount},

View File

@@ -29,12 +29,12 @@ func ClusterRerouteMove(conf *cfg.Config, index string) error {
move := conf.DefaultCluster.ES().Cluster.Reroute() move := conf.DefaultCluster.ES().Cluster.Reroute()
commands := esdsl.NewCommand() commands := esdsl.NewCommand()
moveCommand := new(types.CommandMoveAction{ moveCommand := &types.CommandMoveAction{
Shard: conf.Shards, Shard: conf.Shards,
FromNode: conf.FromNode, FromNode: conf.FromNode,
ToNode: conf.ToNode, ToNode: conf.ToNode,
Index: index, Index: index,
}) }
commands.CommandCaster().Move = moveCommand commands.CommandCaster().Move = moveCommand
@@ -52,11 +52,11 @@ func ClusterRerouteAllocateReplica(conf *cfg.Config, index string) error {
move := conf.DefaultCluster.ES().Cluster.Reroute() move := conf.DefaultCluster.ES().Cluster.Reroute()
commands := esdsl.NewCommand() commands := esdsl.NewCommand()
allocCommand := new(types.CommandAllocateReplicaAction{ allocCommand := &types.CommandAllocateReplicaAction{
Shard: conf.Shards, Shard: conf.Shards,
Node: conf.ToNode, Node: conf.ToNode,
Index: index, Index: index,
}) }
commands.CommandCaster().AllocateReplica = allocCommand commands.CommandCaster().AllocateReplica = allocCommand
@@ -74,12 +74,12 @@ func ClusterRerouteCancel(conf *cfg.Config, index string) error {
move := conf.DefaultCluster.ES().Cluster.Reroute() move := conf.DefaultCluster.ES().Cluster.Reroute()
commands := esdsl.NewCommand() commands := esdsl.NewCommand()
cancelCommand := new(types.CommandCancelAction{ cancelCommand := &types.CommandCancelAction{
Shard: conf.Shards, Shard: conf.Shards,
Node: conf.ToNode, Node: conf.ToNode,
Index: index, Index: index,
AllowPrimary: &conf.AllowPrimary, AllowPrimary: &conf.AllowPrimary,
}) }
commands.CommandCaster().Cancel = cancelCommand commands.CommandCaster().Cancel = cancelCommand
@@ -97,12 +97,12 @@ func ClusterRerouteAllocatePrimary(conf *cfg.Config, index string, stale bool) e
move := conf.DefaultCluster.ES().Cluster.Reroute() move := conf.DefaultCluster.ES().Cluster.Reroute()
commands := esdsl.NewCommand() commands := esdsl.NewCommand()
allocCommand := new(types.CommandAllocatePrimaryAction{ allocCommand := &types.CommandAllocatePrimaryAction{
Shard: conf.Shards, Shard: conf.Shards,
Node: conf.ToNode, Node: conf.ToNode,
Index: index, Index: index,
AcceptDataLoss: conf.AcceptDataLoss, AcceptDataLoss: conf.AcceptDataLoss,
}) }
if stale { if stale {
commands.CommandCaster().AllocateStalePrimary = allocCommand commands.CommandCaster().AllocateStalePrimary = allocCommand

View File

@@ -38,7 +38,7 @@ func ClusterSettingsList(conf *cfg.Config) error {
return fmt.Errorf("failed to get cluster settings: %w", esErrorString(err)) return fmt.Errorf("failed to get cluster settings: %w", esErrorString(err))
} }
table := printer.NewTable(conf).WithSize(2, 0) table := printer.NewTable(conf, 2, 0)
table.Addheaders("setting", "value") table.Addheaders("setting", "value")
entries := [][]any{} entries := [][]any{}

View File

@@ -71,7 +71,7 @@ func checkClusterStatus(conf *cfg.Config, leader, follower string) bool {
status[cluster] = clusterHealth status[cluster] = clusterHealth
} }
table := printer.NewTable(conf).WithSize(3, 6) table := printer.NewTable(conf, 3, 6)
table.Addheaders("setting", "leader:"+leader, "follower:"+follower) table.Addheaders("setting", "leader:"+leader, "follower:"+follower)
@@ -89,7 +89,7 @@ func checkClusterStatus(conf *cfg.Config, leader, follower string) bool {
status[leader].ActivePrimaryShards, status[leader].ActivePrimaryShards,
status[follower].ActivePrimaryShards, status[follower].ActivePrimaryShards,
}, },
{"indices", {"Indicies",
len(status[leader].Indices), len(status[leader].Indices),
len(status[follower].Indices), len(status[follower].Indices),
}, },
@@ -151,7 +151,7 @@ func findIlmErrors(conf *cfg.Config, leader, follower string) bool {
if len(failed[cluster]) > 0 { if len(failed[cluster]) > 0 {
idx := 0 idx := 0
table := printer.NewTable(conf).WithSize(2, len(failed[cluster])) table := printer.NewTable(conf, 2, len(failed[cluster]))
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] {
@@ -172,7 +172,7 @@ func findIlmErrors(conf *cfg.Config, leader, follower string) bool {
return false return false
} }
// find unsynchronized indices only present on leader // find unsynchronized indicies only present on leader
func findIndicesOnlyOnLeader(conf *cfg.Config, indices ClusterIndices, leader, follower string) bool { func findIndicesOnlyOnLeader(conf *cfg.Config, indices ClusterIndices, leader, follower string) bool {
exclude := regexp.MustCompile(DefaultExclude) exclude := regexp.MustCompile(DefaultExclude)
if conf.Exclude != "" { if conf.Exclude != "" {
@@ -225,7 +225,7 @@ func findIndicesOnlyOnLeader(conf *cfg.Config, indices ClusterIndices, leader, f
} }
idx := 0 idx := 0
table := printer.NewTable(conf).WithSize(3, len(indexOnlyOnLeader)) table := printer.NewTable(conf, 3, len(indexOnlyOnLeader))
table.Addheaders("index only on leader", "size", "docscount") table.Addheaders("index only on leader", "size", "docscount")
for name, index := range indexOnlyOnLeader { for name, index := range indexOnlyOnLeader {
@@ -275,7 +275,7 @@ func findOrphanedIndices(conf *cfg.Config, indices ClusterIndices, leader, follo
} }
idx := 0 idx := 0
table := printer.NewTable(conf).WithSize(3, len(orphaned)) table := printer.NewTable(conf, 3, len(orphaned))
table.Addheaders("orphaned index on follower", "size", "docscount") table.Addheaders("orphaned index on follower", "size", "docscount")
for name, index := range orphaned { for name, index := range orphaned {
@@ -311,7 +311,7 @@ func findFailedFollowerIndices(conf *cfg.Config, indices ClusterIndices, followe
} }
idx := 0 idx := 0
table := printer.NewTable(conf).WithSize(3, len(red)) table := printer.NewTable(conf, 3, len(red))
table.Addheaders("red index on follower", "size", "docscount") table.Addheaders("red index on follower", "size", "docscount")
for name, index := range red { for name, index := range red {

View File

@@ -62,7 +62,7 @@ func DatastreamList(conf *cfg.Config) error {
} }
} }
table := printer.NewTable(conf).WithSize(7, size) table := printer.NewTable(conf, 7, size)
table.Addheaders("name", "ilm policy", "hidden", "system", "replicated", "generation", "timestamp field") table.Addheaders("name", "ilm policy", "hidden", "system", "replicated", "generation", "timestamp field")
for idx, datastream := range list { for idx, datastream := range list {
@@ -146,7 +146,7 @@ func DatastreamShow(conf *cfg.Config, dsname string) error {
return fmt.Errorf("failed to get data stream stats: %w", esErrorString(err)) return fmt.Errorf("failed to get data stream stats: %w", esErrorString(err))
} }
table := printer.NewTable(conf).WithSize(11, 2) table := printer.NewTable(conf, 11, 2)
table.Addheaders("data stream property", "value") table.Addheaders("data stream property", "value")
datastream := res.DataStreams[0] datastream := res.DataStreams[0]
@@ -176,7 +176,7 @@ func DatastreamShow(conf *cfg.Config, dsname string) error {
return err return err
} }
table = printer.NewTable(conf).WithSize(5, len(datastream.Indices)) table = printer.NewTable(conf, 5, len(datastream.Indices))
table.Addheaders("backing index name", "uuid", "prefer ilm", "ilm policy", "managed by") table.Addheaders("backing index name", "uuid", "prefer ilm", "ilm policy", "managed by")
for idx, index := range datastream.Indices { for idx, index := range datastream.Indices {
@@ -233,7 +233,7 @@ func DatastreamRollover(conf *cfg.Config, ds string) error {
return fmt.Errorf("failed to rollover data stream: %w", esErrorString(err)) return fmt.Errorf("failed to rollover data stream: %w", esErrorString(err))
} }
table := printer.NewTable(conf).WithSize(2, 5) table := printer.NewTable(conf, 2, 5)
table.Addheaders("rollover response", "value") table.Addheaders("rollover response", "value")
table.Entries = [][]any{ table.Entries = [][]any{
{"acknowledged", res.Acknowledged}, {"acknowledged", res.Acknowledged},

View File

@@ -101,7 +101,7 @@ func DocDelete(conf *cfg.Config, queries []string) error {
return nil return nil
} }
req := new(deletebyquery.Request{}) req := &deletebyquery.Request{}
if len(queries) == 0 && conf.All { if len(queries) == 0 && conf.All {
req.Query = esdsl.NewMatchAllQuery().QueryCaster() req.Query = esdsl.NewMatchAllQuery().QueryCaster()

View File

@@ -22,8 +22,6 @@ import (
"errors" "errors"
"fmt" "fmt"
"log/slog" "log/slog"
"maps"
"slices"
"strings" "strings"
"codeberg.org/scip/esctl/pkg/cfg" "codeberg.org/scip/esctl/pkg/cfg"
@@ -64,7 +62,13 @@ func IlmNames(conf *cfg.Config) ([]string, error) {
return nil, fmt.Errorf("failed to get ilm policies: %w", esErrorString(err)) return nil, fmt.Errorf("failed to get ilm policies: %w", esErrorString(err))
} }
names := slices.Collect(maps.Keys(res)) names := make([]string, len(res))
idx := 0
for name := range res {
names[idx] = name
idx++
}
return names, nil return names, nil
} }
@@ -85,7 +89,7 @@ func IlmList(conf *cfg.Config, pattern string) error {
repr.Println(res) repr.Println(res)
} }
table := printer.NewTable(conf).WithSize(5, 0) table := printer.NewTable(conf, 5, 0)
table.Addheaders("ilm policy", "hot", "warm", "frozen", "delete") table.Addheaders("ilm policy", "hot", "warm", "frozen", "delete")
for name, ilm := range res { for name, ilm := range res {
@@ -123,7 +127,7 @@ func IlmShow(conf *cfg.Config, policy string) error {
return IlmShowTree(conf, ilm.Policy) return IlmShowTree(conf, ilm.Policy)
} }
table := printer.NewTable(conf).WithSize(2, 5) table := printer.NewTable(conf, 2, 5)
table.Addheaders("ilm policy setting", "value") table.Addheaders("ilm policy setting", "value")
table.Entries = [][]any{ table.Entries = [][]any{
@@ -142,7 +146,7 @@ func IlmShow(conf *cfg.Config, policy string) error {
func IlmShowTree(conf *cfg.Config, ilm types.IlmPolicy) error { func IlmShowTree(conf *cfg.Config, ilm types.IlmPolicy) error {
indent := "" indent := ""
table := printer.NewTable(conf).WithSize(4, 0) table := printer.NewTable(conf, 4, 0)
table.Addheaders("phase", "min age", "min size", "snapshot repo") table.Addheaders("phase", "min age", "min size", "snapshot repo")
for _, phase := range IlmPhaseOrder { for _, phase := range IlmPhaseOrder {
@@ -280,17 +284,15 @@ func IlmExplain(conf *cfg.Config, index string) error {
ilm := explain.(*types.LifecycleExplainManaged) ilm := explain.(*types.LifecycleExplainManaged)
table := printer.NewTable(conf).WithSize(2, 0) table := printer.NewTable(conf, 2, 0)
table.Addheaders("ilm status field", "value") table.Addheaders("ilm status field", "value")
info := "" info := ""
if len(ilm.StepInfo["reason"]) > 0 {
err = json.Unmarshal(ilm.StepInfo["reason"], &info) err = json.Unmarshal(ilm.StepInfo["reason"], &info)
if err != nil { if err != nil {
return fmt.Errorf("failed to unmarshal step info: %w", err) return fmt.Errorf("failed to unmarshal step info: %w", err)
} }
}
table.Entries = [][]any{ table.Entries = [][]any{
{"index", ilm.Index}, {"index", ilm.Index},
@@ -301,11 +303,8 @@ func IlmExplain(conf *cfg.Config, index string) error {
{"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 retry count", ilm.FailedStepRetryCount},
if ilm.FailedStep != nil {
table.AddRow("failed step", *ilm.FailedStep)
table.AddRow("failed step retry count", ilm.FailedStepRetryCount)
} }
if err := table.Print(); err != nil { if err := table.Print(); err != nil {
@@ -352,7 +351,7 @@ func IlmCreate(conf *cfg.Config, policyname string) error {
var actions types.IlmActionsVariant = esdsl.NewIlmActions() var actions types.IlmActionsVariant = esdsl.NewIlmActions()
rollover := new(types.RolloverAction{}) rollover := &types.RolloverAction{}
haveroll := false haveroll := false
if policy != nil { if policy != nil {
@@ -514,8 +513,8 @@ func IlmCreate(conf *cfg.Config, policyname string) error {
phases.PhasesCaster().Delete = policy.Phases.Delete phases.PhasesCaster().Delete = policy.Phases.Delete
} }
put := new(putlifecycle.Request{}) put := &putlifecycle.Request{}
newpolicy := new(types.IlmPolicy{}) newpolicy := &types.IlmPolicy{}
newpolicy.IlmPolicyCaster().Phases = *phases.PhasesCaster() newpolicy.IlmPolicyCaster().Phases = *phases.PhasesCaster()
put.Policy = newpolicy put.Policy = newpolicy

View File

@@ -105,7 +105,7 @@ func IlmForecastList(conf *cfg.Config, filter string) error {
headers = append(headers, "ilm policy") headers = append(headers, "ilm policy")
} }
table := printer.NewTable(conf).WithSize(len(headers), 0) table := printer.NewTable(conf, len(headers), 0)
table.Addheaders(headers...) table.Addheaders(headers...)
for _, phase := range phaseData { for _, phase := range phaseData {
@@ -185,19 +185,12 @@ func virtualAge(phase *PhaseData) time.Duration {
// Retrieve all index, ilm-explain and ilm-policies in parallel // Retrieve all index, ilm-explain and ilm-policies in parallel
func getIlmPhaseData(conf *cfg.Config) ([]PhaseData, error) { func getIlmPhaseData(conf *cfg.Config) ([]PhaseData, error) {
responses := make(chan apiResponse, 3) responses := make(chan apiResponse, 3)
wg := new(sync.WaitGroup{}) wg := &sync.WaitGroup{}
wg.Add(3)
wg.Go(func() { go getApiData(conf, conf.DefaultCluster.ES(), wg, responses, "indicesbytes")
getApiData(conf, conf.DefaultCluster.ES(), responses, "indicesbytes") go getApiData(conf, conf.DefaultCluster.ES(), wg, responses, "explain")
}) go getApiData(conf, conf.DefaultCluster.ES(), wg, responses, "policies")
wg.Go(func() {
getApiData(conf, conf.DefaultCluster.ES(), responses, "explain")
})
wg.Go(func() {
getApiData(conf, conf.DefaultCluster.ES(), responses, "policies")
})
wg.Wait() wg.Wait()
@@ -338,7 +331,7 @@ func findNextPhase(policy types.IlmPolicy, currentPhase string) *NextPhase {
// phase list to determine which comes next // phase list to determine which comes next
phases, start := registerPhases(policy, currentPhase) phases, start := registerPhases(policy, currentPhase)
nextPhase := new(NextPhase{}) nextPhase := &NextPhase{}
// finally determine which phase comes next // finally determine which phase comes next
// exception: hot, where we look for rollover rules // exception: hot, where we look for rollover rules

View File

@@ -30,11 +30,9 @@ 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"
"codeberg.org/scip/mapmap"
"github.com/charmbracelet/lipgloss" "github.com/charmbracelet/lipgloss"
"github.com/elastic/go-elasticsearch/v9/typedapi/cat/indices" "github.com/elastic/go-elasticsearch/v9/typedapi/cat/indices"
"github.com/elastic/go-elasticsearch/v9/typedapi/esdsl" "github.com/elastic/go-elasticsearch/v9/typedapi/esdsl"
"github.com/elastic/go-elasticsearch/v9/typedapi/types"
"github.com/elastic/go-elasticsearch/v9/typedapi/types/enums/healthstatus" "github.com/elastic/go-elasticsearch/v9/typedapi/types/enums/healthstatus"
) )
@@ -43,7 +41,7 @@ func IndexNames(conf *cfg.Config) ([]string, error) {
res, err := conf.DefaultCluster.ES().Cat.Indices(). res, err := conf.DefaultCluster.ES().Cat.Indices().
Do(context.Background()) Do(context.Background())
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to get indices: %w", esErrorString(err)) return nil, fmt.Errorf("failed to get indicies: %w", esErrorString(err))
} }
indices := make([]string, len(res)) indices := make([]string, len(res))
@@ -55,29 +53,41 @@ func IndexNames(conf *cfg.Config) ([]string, error) {
} }
func filterIndices(conf *cfg.Config, list indices.Response) indices.Response { func filterIndices(conf *cfg.Config, list indices.Response) indices.Response {
var filter *regexp.Regexp // apply partials filter first
selectedlist := indices.Response{}
if len(conf.Filter) > 0 { for _, index := range list {
filter = regexp.MustCompile(conf.Filter[0])
}
return mapmap.NewSlicer(list).MapSliceValuesImmutable(func(index types.IndicesRecord) bool {
if !conf.Partials && strings.HasPrefix(*index.Index, "partial-") { if !conf.Partials && strings.HasPrefix(*index.Index, "partial-") {
return false continue
} }
if !conf.Hidden && strings.HasPrefix(*index.Index, ".") { if !conf.Hidden && strings.HasPrefix(*index.Index, ".") {
return false continue
} }
if len(conf.Filter) > 0 { if strings.HasPrefix(*index.Index, ".ds-") {
if !filter.MatchString(*index.Index) { // ignore data stream backing indicies
return false continue
}
selectedlist = append(selectedlist, index)
}
if len(conf.Filter) == 0 {
return selectedlist
}
// we support just one filter here, for now
filter := *regexp.MustCompile(conf.Filter[0])
newlist := indices.Response{}
for _, index := range selectedlist {
if filter.MatchString(*index.Index) {
newlist = append(newlist, index)
} }
} }
return true return newlist
})
} }
func IndexList(conf *cfg.Config) error { func IndexList(conf *cfg.Config) error {
@@ -89,10 +99,10 @@ func IndexList(conf *cfg.Config) error {
res, err := cat.Do(context.Background()) res, err := cat.Do(context.Background())
if err != nil { if err != nil {
return fmt.Errorf("failed to get indices: %w", esErrorString(err)) return fmt.Errorf("failed to get indicies: %w", esErrorString(err))
} }
slog.Debug("ES result", "indices", res) slog.Debug("ES result", "indicies", res)
list := filterIndices(conf, res) list := filterIndices(conf, res)
@@ -104,7 +114,7 @@ func IndexList(conf *cfg.Config) error {
} }
} }
table := printer.NewTable(conf).WithSize(3, size) table := printer.NewTable(conf, 3, size)
table.Addheaders("name", "size", "docscount") table.Addheaders("name", "size", "docscount")
for idx, index := range list { for idx, index := range list {
@@ -140,7 +150,7 @@ func IndexShow(conf *cfg.Config, indexpattern string) error {
idx++ idx++
} }
table := printer.NewTable(conf).WithSize(2, 7) table := printer.NewTable(conf, 2, 7)
table.Addheaders("index property", "value") table.Addheaders("index property", "value")
ts, err := strconv.ParseInt(index.Settings.Index.CreationDate.(string), 10, 64) ts, err := strconv.ParseInt(index.Settings.Index.CreationDate.(string), 10, 64)
@@ -263,7 +273,7 @@ func IndexFields(conf *cfg.Config, index string) error {
return fmt.Errorf("failed to retrieve field capabilties: %w", esErrorString(err)) return fmt.Errorf("failed to retrieve field capabilties: %w", esErrorString(err))
} }
table := printer.NewTable(conf).WithSize(5, 0) table := printer.NewTable(conf, 5, 0)
table.Addheaders("field", "type", "searchable", "aggretable", "metadata") table.Addheaders("field", "type", "searchable", "aggretable", "metadata")
idx := 0 idx := 0
@@ -336,7 +346,7 @@ func IndexDiskusage(conf *cfg.Config, index string) error {
return errors.New("no disk usage reported for index") return errors.New("no disk usage reported for index")
} }
table := printer.NewTable(conf). table := printer.NewTableEmpty(conf).
WithHeaders("field", "bloom filter", "norms", "points", "term vectors", "knn vectors", "total") WithHeaders("field", "bloom filter", "norms", "points", "term vectors", "knn vectors", "total")
for name, field := range diskusage.Fields { for name, field := range diskusage.Fields {

View File

@@ -72,7 +72,7 @@ func IndexAliasList(conf *cfg.Config) error {
} }
} }
table := printer.NewTable(conf).WithSize(2, len(aliaslist)) table := printer.NewTable(conf, 2, len(aliaslist))
table.Addheaders("index", "alias") table.Addheaders("index", "alias")
idx := 0 idx := 0
@@ -113,7 +113,7 @@ func IndexAliasRollover(conf *cfg.Config, alias string) error {
return fmt.Errorf("failed to rollover index alias: %w", esErrorString(err)) return fmt.Errorf("failed to rollover index alias: %w", esErrorString(err))
} }
table := printer.NewTable(conf).WithSize(2, 5) table := printer.NewTable(conf, 2, 5)
table.Addheaders("rollover response", "value") table.Addheaders("rollover response", "value")
table.Entries = [][]any{ table.Entries = [][]any{
{"acknowledged", res.Acknowledged}, {"acknowledged", res.Acknowledged},

View File

@@ -1,83 +0,0 @@
/*
Copyright © 2026 Thomas von Dein
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package es
import (
"context"
"fmt"
"time"
"codeberg.org/scip/esctl/pkg/cfg"
"codeberg.org/scip/esctl/pkg/printer"
"github.com/elastic/go-elasticsearch/v9/typedapi/esdsl"
"github.com/elastic/go-elasticsearch/v9/typedapi/types/enums/conflicts"
)
func IndexCopy(conf *cfg.Config) error {
copy := conf.DefaultCluster.ES().Reindex()
if conf.Force {
copy.Conflicts(conflicts.Proceed)
}
if conf.RequestsPerSecond != 0 {
copy.RequestsPerSecond(fmt.Sprintf("%.2f", conf.RequestsPerSecond))
}
if conf.MaxDocs > 0 {
copy.MaxDocs(conf.MaxDocs)
}
if conf.Timeout > 0 {
copy.Timeout(formatDuration(conf.Timeout))
}
if conf.Wait {
copy.WaitForActiveShards("all")
}
if conf.Refresh {
copy.Refresh(true)
}
copy.Source(esdsl.NewReindexSource().Index(conf.SourceIndices...))
copy.Dest(esdsl.NewReindexDestination().Index(conf.Index))
res, err := copy.Do(context.Background())
if err != nil {
return fmt.Errorf("failed to copy indices: %w", esErrorString(err))
}
table := printer.NewTable(conf).WithSize(2, 0).
WithHeaders("Reindex metrtic", "value")
table.Entries = [][]any{
{"Source indices", conf.SourceIndices},
{"Target index", conf.Index},
{"Batches", *res.Batches},
{"Documents total", *res.Total},
{"Documents created", *res.Created},
{"Documents deleted", *res.Deleted},
{"Documents updated", *res.Updated},
{"Requests/s", *res.RequestsPerSecond},
{"Timed out", *res.TimedOut},
{"Time elapsed", time.Duration(*res.Took) * time.Millisecond},
{"Version conflicts", *res.VersionConflicts},
}
return table.Print()
}

View File

@@ -28,29 +28,12 @@ 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"
"codeberg.org/scip/mapmap"
"github.com/elastic/go-elasticsearch/v9/typedapi/esdsl" "github.com/elastic/go-elasticsearch/v9/typedapi/esdsl"
"github.com/elastic/go-elasticsearch/v9/typedapi/types" "github.com/elastic/go-elasticsearch/v9/typedapi/types"
) )
// used for completion // used for completion
func IndexTemplateNames(conf *cfg.Config) ([]string, error) { func IndexTemplateList(conf *cfg.Config) error {
res, err := conf.DefaultCluster.ES().Indices.GetIndexTemplate().
Do(context.Background())
if err != nil {
return nil, fmt.Errorf("failed to get index templates: %w", esErrorString(err))
}
names := make([]string, len(res.IndexTemplates))
for idx, tpl := range res.IndexTemplates {
names[idx] = tpl.Name
}
return names, nil
}
func IndexTemplateList(conf *cfg.Config, filter string) error {
res, err := conf.DefaultCluster.ES().Indices.GetIndexTemplate(). res, err := conf.DefaultCluster.ES().Indices.GetIndexTemplate().
Do(context.Background()) Do(context.Background())
if err != nil { if err != nil {
@@ -59,20 +42,16 @@ func IndexTemplateList(conf *cfg.Config, filter string) error {
slog.Debug("res", "index templates", res) slog.Debug("res", "index templates", res)
table := printer.NewTable(conf).WithSize(5, 0) table := printer.NewTable(conf, 5, len(res.IndexTemplates))
table.Addheaders("name", "description", "index patterns", "priority") table.Addheaders("name", "description", "priority")
tplList := filterIndexTemplates(conf, filter, res.IndexTemplates) for idx, tpl := range res.IndexTemplates {
desc, err := json.Marshal(tpl.IndexTemplate.Meta_["description"])
for _, tpl := range tplList { if err != nil {
desc := strings.TrimPrefix(strings.TrimSuffix(string(tpl.IndexTemplate.Meta_["description"]), `"`), `"`) return fmt.Errorf("failed to unmarshal meta json data: %w", err)
var prio int64
if tpl.IndexTemplate.Priority != nil {
prio = *tpl.IndexTemplate.Priority
} }
table.AddRow(tpl.Name, desc, tpl.IndexTemplate.IndexPatterns, prio) table.Entries[idx] = []any{tpl.Name, desc, tpl.IndexTemplate.Priority}
} }
table.Sort() table.Sort()
@@ -80,30 +59,6 @@ func IndexTemplateList(conf *cfg.Config, filter string) error {
return table.Print() return table.Print()
} }
// Filter index templates by name, index pattern or hidden flag, using
// mapmap.Slicer
func filterIndexTemplates(conf *cfg.Config, nameFilter string,
templates []types.IndexTemplateItem) []types.IndexTemplateItem {
return mapmap.NewSlicer(templates).MapSliceValuesImmutable(func(tpl types.IndexTemplateItem) bool {
if !conf.Hidden && strings.HasPrefix(tpl.Name, ".") {
return false
}
if nameFilter != "" && !strings.Contains(tpl.Name, nameFilter) {
return false
}
if len(conf.Filter) > 0 {
if !mapmap.NewSlicer(tpl.IndexTemplate.IndexPatterns).
FindSliceInSlice(conf.Filter, strings.Contains) {
return false
}
}
return true
})
}
func IndexTemplateShow(conf *cfg.Config, tplname string) error { func IndexTemplateShow(conf *cfg.Config, tplname string) error {
res, err := conf.DefaultCluster.ES().Indices.GetIndexTemplate(). res, err := conf.DefaultCluster.ES().Indices.GetIndexTemplate().
Name(tplname). Name(tplname).
@@ -120,7 +75,7 @@ func IndexTemplateShow(conf *cfg.Config, tplname string) error {
tpl := res.IndexTemplates[0] tpl := res.IndexTemplates[0]
table := printer.NewTable(conf).WithSize(2, 6) table := printer.NewTable(conf, 2, 6)
table.Addheaders("index template property", "value") table.Addheaders("index template property", "value")
desc, err := json.Marshal(tpl.IndexTemplate.Meta_["description"]) desc, err := json.Marshal(tpl.IndexTemplate.Meta_["description"])
@@ -148,7 +103,7 @@ func IndexTemplateShow(conf *cfg.Config, tplname string) error {
return err return err
} }
table = printer.NewTable(conf).WithSize(2, 0) table = printer.NewTable(conf, 2, 0)
table.Addheaders("index setting property", "value") table.Addheaders("index setting property", "value")
err = getIndexTemplateSettings(conf, tplname, table) err = getIndexTemplateSettings(conf, tplname, table)
@@ -162,7 +117,7 @@ func IndexTemplateShow(conf *cfg.Config, tplname string) error {
return err return err
} }
table = printer.NewTable(conf).WithSize(2, 0) table = printer.NewTable(conf, 2, 0)
table.Addheaders("index field mapping", "type") table.Addheaders("index field mapping", "type")
if tpl.IndexTemplate.Template.Mappings != nil { if tpl.IndexTemplate.Template.Mappings != nil {
@@ -441,7 +396,7 @@ func rolloverAliasIndexTemplate(conf *cfg.Config, name string) error {
return nil return nil
} }
table := printer.NewTable(conf).WithSize(4, 0) table := printer.NewTable(conf, 4, 0)
table.Addheaders("rollover alias", "status", "ack", "new index") table.Addheaders("rollover alias", "status", "ack", "new index")
// apply rollover to all matching aliases, if any // apply rollover to all matching aliases, if any

View File

@@ -35,7 +35,7 @@ func LicenseShow(conf *cfg.Config) error {
slog.Debug("license show", "license", res) slog.Debug("license show", "license", res)
table := printer.NewTable(conf).WithHeaders("license setting", "value") table := printer.NewTableEmpty(conf).WithHeaders("license setting", "value")
lic := res.License lic := res.License

View File

@@ -37,7 +37,7 @@ func NodeList(conf *cfg.Config) error {
slog.Debug("ES result", "nodes", nodes) slog.Debug("ES result", "nodes", nodes)
table := printer.NewTable(conf).WithHeaders( table := printer.NewTableEmpty(conf).WithHeaders(
"name", "ip", "load1m", "load5m", "load15m", "ram %", "heap %") "name", "ip", "load1m", "load5m", "load15m", "ram %", "heap %")
for _, node := range nodes { for _, node := range nodes {
@@ -104,7 +104,7 @@ func NodeShow(conf *cfg.Config, nodename string) error {
for id, info := range res.Nodes { for id, info := range res.Nodes {
stat := stats.Nodes[id] stat := stats.Nodes[id]
table := printer.NewTable(conf).WithSize(2, 0) table := printer.NewTable(conf, 2, 0)
table.Addheaders(nodename+" property", "value") table.Addheaders(nodename+" property", "value")
roles := make([]string, len(info.Roles)) roles := make([]string, len(info.Roles))
@@ -197,7 +197,7 @@ func NodeClients(conf *cfg.Config, nodename string) error {
slog.Debug("ES result", "stat", stats) slog.Debug("ES result", "stat", stats)
table := printer.NewTable(conf).WithSize(5, 0) table := printer.NewTable(conf, 5, 0)
table.Addheaders("agent", "id", "when", "from host", "url") table.Addheaders("agent", "id", "when", "from host", "url")
for _, stat := range stats.Nodes { for _, stat := range stats.Nodes {
@@ -250,7 +250,7 @@ func NodeUsage(conf *cfg.Config, nodeid string) error {
slog.Debug("ES result", "usage", stats) slog.Debug("ES result", "usage", stats)
table := printer.NewTable(conf).WithHeaders( table := printer.NewTableEmpty(conf).WithHeaders(
"node", "node",
"bulk", "bulk",
"doc get", "doc get",

View File

@@ -19,6 +19,7 @@ package es
import ( import (
"context" "context"
"fmt" "fmt"
"sync"
"codeberg.org/scip/esctl/pkg/cfg" "codeberg.org/scip/esctl/pkg/cfg"
"github.com/elastic/go-elasticsearch/v9" "github.com/elastic/go-elasticsearch/v9"
@@ -60,7 +61,14 @@ type apiResponse struct {
which int which int
} }
func getApiData(conf *cfg.Config, es *elasticsearch.TypedClient, reschan chan apiResponse, which string) { func getApiData(
conf *cfg.Config,
es *elasticsearch.TypedClient,
wg *sync.WaitGroup,
reschan chan apiResponse,
which string) {
defer wg.Done()
apiRes := apiResponse{} apiRes := apiResponse{}
var arerr error var arerr error

View File

@@ -20,8 +20,6 @@ import (
"context" "context"
"fmt" "fmt"
"log/slog" "log/slog"
"maps"
"slices"
"codeberg.org/scip/esctl/pkg/cfg" "codeberg.org/scip/esctl/pkg/cfg"
"codeberg.org/scip/esctl/pkg/printer" "codeberg.org/scip/esctl/pkg/printer"
@@ -35,7 +33,15 @@ func RoleNames(conf *cfg.Config) ([]string, error) {
return nil, fmt.Errorf("failed to get roles: %w", esErrorString(err)) return nil, fmt.Errorf("failed to get roles: %w", esErrorString(err))
} }
return slices.Collect(maps.Keys(res)), nil roles := make([]string, len(res))
idx := 0
for name := range res {
roles[idx] = name
idx++
}
return roles, nil
} }
func RoleList(conf *cfg.Config) error { func RoleList(conf *cfg.Config) error {
@@ -47,7 +53,7 @@ func RoleList(conf *cfg.Config) error {
slog.Debug("ES result", "roles", res) slog.Debug("ES result", "roles", res)
table := printer.NewTable(conf).WithSize(3, len(res)) table := printer.NewTable(conf, 3, len(res))
table.Addheaders("role", "index roles", "cluster roles") table.Addheaders("role", "index roles", "cluster roles")
idx := 0 idx := 0
@@ -126,7 +132,7 @@ func roleRemoteClusters(conf *cfg.Config, role types.Role) error {
return nil return nil
} }
table := printer.NewTable(conf).WithSize(2, len(role.RemoteCluster)) table := printer.NewTable(conf, 2, len(role.RemoteCluster))
table.Addheaders("remote cluster", "privilege") table.Addheaders("remote cluster", "privilege")
idx := 0 idx := 0
@@ -152,7 +158,7 @@ func roleClusters(conf *cfg.Config, role types.Role) error {
return nil return nil
} }
table := printer.NewTable(conf).WithSize(1, len(role.Cluster)) table := printer.NewTable(conf, 1, len(role.Cluster))
table.Addheaders("cluster rights") table.Addheaders("cluster rights")
idx := 0 idx := 0
@@ -171,7 +177,7 @@ func roleRemoteIndices(conf *cfg.Config, role types.Role) error {
return nil return nil
} }
table := printer.NewTable(conf).WithSize(3, len(role.RemoteIndices)) table := printer.NewTable(conf, 3, len(role.RemoteIndices))
table.Addheaders("remote index names", "index permissions", "allow restricted") table.Addheaders("remote index names", "index permissions", "allow restricted")
idx := 0 idx := 0
@@ -197,7 +203,7 @@ func roleIndices(conf *cfg.Config, role types.Role) error {
return nil return nil
} }
table := printer.NewTable(conf).WithSize(3, len(role.Indices)) table := printer.NewTable(conf, 3, len(role.Indices))
table.Addheaders("index names", "index permissions", "allow restricted") table.Addheaders("index names", "index permissions", "allow restricted")
idx := 0 idx := 0
@@ -223,7 +229,7 @@ func roleApplications(conf *cfg.Config, role types.Role) error {
return nil return nil
} }
table := printer.NewTable(conf).WithSize(3, len(role.Applications)) table := printer.NewTable(conf, 3, len(role.Applications))
table.Addheaders("application", "privileges", "resources") table.Addheaders("application", "privileges", "resources")
idx := 0 idx := 0

View File

@@ -127,7 +127,7 @@ func getCsvRecord(conf *cfg.Config, csvfile, rolename string) (*Record, error) {
}() }()
scanner := bufio.NewScanner(fd) scanner := bufio.NewScanner(fd)
record := new(Record{role: rolename}) record := Record{role: rolename}
for scanner.Scan() { for scanner.Scan() {
line := strings.TrimSpace(scanner.Text()) line := strings.TrimSpace(scanner.Text())
@@ -150,7 +150,7 @@ func getCsvRecord(conf *cfg.Config, csvfile, rolename string) (*Record, error) {
} }
} }
return record, nil return &record, nil
} }
func diffRoles(conf *cfg.Config, records map[string]Record, res getrole.Response) []Register { func diffRoles(conf *cfg.Config, records map[string]Record, res getrole.Response) []Register {
@@ -216,7 +216,7 @@ func RoleDiff(conf *cfg.Config, csvfile, role string) error {
rows := diffRoles(conf, records, res) rows := diffRoles(conf, records, res)
table := printer.NewTable(conf).WithSize(3, len(rows)) table := printer.NewTable(conf, 3, len(rows))
table.Addheaders("role", "is deployed", "is defined") table.Addheaders("role", "is deployed", "is defined")
for idx, row := range rows { for idx, row := range rows {

View File

@@ -33,7 +33,7 @@ func RolloverConditions(conf *cfg.Config) types.RolloverConditionsVariant {
} }
if conf.MaxDocs > 0 { if conf.MaxDocs > 0 {
cond.MaxDocs(conf.MaxDocs) cond.MaxDocs(int64(conf.MaxDocs))
} }
if conf.MaxShardSize > 0 { if conf.MaxShardSize > 0 {

View File

@@ -56,7 +56,7 @@ func Search(conf *cfg.Config, queries []string) error {
return err return err
} }
req := new(search.Request{Query: queryCaster}) req := &search.Request{Query: queryCaster}
searchEs.Request(req) searchEs.Request(req)
@@ -128,7 +128,7 @@ func validateSearch(conf *cfg.Config, queries []string) error {
return err return err
} }
req := new(validatequery.Request{Query: queryCaster}) req := &validatequery.Request{Query: queryCaster}
validate.Request(req) validate.Request(req)

View File

@@ -80,7 +80,7 @@ func NewFilter(query string) (*filter, error) {
return nil, errors.New("search queries must be in the form field<sep>pattern where <sep> must be one of: = or !=") return nil, errors.New("search queries must be in the form field<sep>pattern where <sep> must be one of: = or !=")
} }
flt := new(filter{term: part[0], filter: part[1], criteria: criteria}) flt := &filter{term: part[0], filter: part[1], criteria: criteria}
if strings.Contains(part[0], ",") { if strings.Contains(part[0], ",") {
// a MultiMatchQuery, match across multiple fields at once // a MultiMatchQuery, match across multiple fields at once

View File

@@ -64,7 +64,7 @@ func SearchQL(conf *cfg.Config, querystring string) error {
return nil return nil
} }
table := printer.NewTable(conf).WithSize(len(qlResult.Columns), len(qlResult.Values)) table := printer.NewTable(conf, len(qlResult.Columns), len(qlResult.Values))
headers := make([]string, len(qlResult.Columns)) headers := make([]string, len(qlResult.Columns))
for idx, col := range qlResult.Columns { for idx, col := range qlResult.Columns {

View File

@@ -100,7 +100,7 @@ func printShards(conf *cfg.Config, shardlist shards.Response) error {
headers = append(headers, "node", "ip") headers = append(headers, "node", "ip")
} }
table := printer.NewTable(conf).WithSize(len(headers), len(shardlist)) table := printer.NewTable(conf, len(headers), len(shardlist))
table.Addheaders(headers...) table.Addheaders(headers...)
for idx, shard := range shardlist { for idx, shard := range shardlist {
@@ -163,7 +163,7 @@ func ShardAllocation(conf *cfg.Config, index string) error {
slog.Debug("ES result", "explain", res) slog.Debug("ES result", "explain", res)
table := printer.NewTable(conf).WithSize(2, 10) table := printer.NewTable(conf, 2, 10)
table.Addheaders("shard allocation setting", "value") table.Addheaders("shard allocation setting", "value")
table.Entries = [][]any{ table.Entries = [][]any{

View File

@@ -42,17 +42,17 @@ type Snapshot struct {
} }
func SnapshotList(conf *cfg.Config) error { func SnapshotList(conf *cfg.Config) error {
// get partial indices // get partial indicies
ires, err := conf.DefaultCluster.ES().Cat.Indices().Do(context.Background()) ires, err := conf.DefaultCluster.ES().Cat.Indices().Do(context.Background())
if err != nil { if err != nil {
return fmt.Errorf("failed to get indices: %w", esErrorString(err)) return fmt.Errorf("failed to get indicies: %w", esErrorString(err))
} }
indices := map[string]int{} indicies := map[string]int{}
for _, index := range ires { for _, index := range ires {
name := strings.ReplaceAll(*index.Index, "partial-", "") name := strings.ReplaceAll(*index.Index, "partial-", "")
indices[name] = 1 indicies[name] = 1
} }
// get snapshots // get snapshots
@@ -61,20 +61,20 @@ func SnapshotList(conf *cfg.Config) error {
return fmt.Errorf("failed to get snapshots: %w", esErrorString(err)) return fmt.Errorf("failed to get snapshots: %w", esErrorString(err))
} }
slog.Debug("ES result", "indices", sres) slog.Debug("ES result", "indicies", sres)
snapshots := []*Snapshot{} // original snapshot names snapshots := []*Snapshot{} // original snapshot names
for _, snapshot := range sres { for _, snapshot := range sres {
snap := new(Snapshot{ snap := &Snapshot{
Name: *snapshot.Id, Name: *snapshot.Id,
Status: *snapshot.Status, Status: *snapshot.Status,
Start: fmt.Sprintf("%s", snapshot.StartTime), Start: fmt.Sprintf("%s", snapshot.StartTime),
Forindex: indexFromSnapshot(*snapshot.Id), Forindex: indexFromSnapshot(*snapshot.Id),
Orphaned: "no", Orphaned: "no",
}) }
_, exists := indices[snap.Forindex] _, exists := indicies[snap.Forindex]
if !exists { if !exists {
snap.Orphaned = "orphaned" snap.Orphaned = "orphaned"
} }
@@ -84,7 +84,7 @@ func SnapshotList(conf *cfg.Config) error {
} }
} }
table := printer.NewTable(conf).WithSize(5, len(snapshots)) table := printer.NewTable(conf, 5, len(snapshots))
table.Addheaders("name", "index", "start", "orphaned", "status") table.Addheaders("name", "index", "start", "orphaned", "status")
for idx, snap := range snapshots { for idx, snap := range snapshots {
@@ -118,7 +118,7 @@ func SnapshotShow(conf *cfg.Config, snapshot string) error {
return errors.New("no snapshot retrieved") return errors.New("no snapshot retrieved")
} }
table := printer.NewTable(conf).WithSize(2, 17) table := printer.NewTable(conf, 2, 17)
table.Addheaders("snapshot property", "value") table.Addheaders("snapshot property", "value")
snap := res.Snapshots[0] snap := res.Snapshots[0]

View File

@@ -36,7 +36,7 @@ func TaskList(conf *cfg.Config) error {
slog.Debug("res", "tasks", res) slog.Debug("res", "tasks", res)
table := printer.NewTable(conf).WithSize(6, len(res)) table := printer.NewTable(conf, 8, len(res))
table.Addheaders("task id", "action", "start time", "run time", "node", "type") table.Addheaders("task id", "action", "start time", "run time", "node", "type")
for idx, task := range res { for idx, task := range res {

View File

@@ -29,13 +29,13 @@ import (
const LevelNotice = slog.Level(2) const LevelNotice = slog.Level(2)
func Init(conf *cfg.Config) { func Init(conf *cfg.Config) {
logLevel := new(slog.LevelVar{}) logLevel := &slog.LevelVar{}
opts := new(yadu.Options{ opts := &yadu.Options{
Level: logLevel, Level: logLevel,
AddSource: true, AddSource: true,
NoColor: !isatty.IsTerminal(os.Stdout.Fd()), NoColor: !isatty.IsTerminal(os.Stdout.Fd()),
}) }
buildInfo, _ := debug.ReadBuildInfo() buildInfo, _ := debug.ReadBuildInfo()

View File

@@ -27,7 +27,7 @@ func (b *ByteSize) String() string {
} }
func Bytes(size int64) *ByteSize { func Bytes(size int64) *ByteSize {
return new(ByteSize{size: uint64(size)}) return &ByteSize{size: uint64(size)}
} }
func ByteString(size int64) string { func ByteString(size int64) string {

View File

@@ -18,11 +18,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
package printer package printer
import ( import (
"os"
"codeberg.org/scip/esctl/pkg/cfg" "codeberg.org/scip/esctl/pkg/cfg"
"github.com/fatih/color" "github.com/fatih/color"
"github.com/mattn/go-isatty"
) )
var ( var (
@@ -34,10 +31,6 @@ var (
) )
func Colorize(conf *cfg.Config, col, what string) string { func Colorize(conf *cfg.Config, col, what string) string {
if !isatty.IsTerminal(os.Stdout.Fd()) {
return what
}
switch conf.Output { switch conf.Output {
case "json", "yaml": case "json", "yaml":
return what return what
@@ -56,11 +49,3 @@ func Colorize(conf *cfg.Config, col, what string) string {
return what return what
} }
func Bold(what string) string {
if !isatty.IsTerminal(os.Stdout.Fd()) {
return what
}
return bold(what)
}

View File

@@ -1,55 +0,0 @@
/*
Copyright © 2026 Thomas von Dein
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package printer
import (
"fmt"
"runtime/metrics"
"strconv"
)
func printGoRoutineMetrics() {
fmt.Println("\nGoroutine metrics:")
printMetric("/sched/goroutines-created:goroutines", "Created")
printMetric("/sched/goroutines:goroutines", "Live")
printMetric("/sched/goroutines/not-in-go:goroutines", "Syscall/CGO")
printMetric("/sched/goroutines/runnable:goroutines", "Runnable")
printMetric("/sched/goroutines/running:goroutines", "Running")
printMetric("/sched/goroutines/waiting:goroutines", "Waiting")
fmt.Println("Thread metrics:")
printMetric("/sched/gomaxprocs:threads", "Max")
printMetric("/sched/threads/total:threads", "Live")
}
func printMetric(name string, descr string) {
sample := []metrics.Sample{{Name: name}}
metrics.Read(sample)
var val string
switch sample[0].Value.Kind() {
case metrics.KindFloat64, metrics.KindFloat64Histogram:
val = fmt.Sprintf("%.2f", sample[0].Value.Float64())
case metrics.KindUint64:
val = strconv.FormatUint(sample[0].Value.Uint64(), 10)
case metrics.KindBad:
val = "n/a"
}
fmt.Printf(" %s: %v\n", descr, val)
}

View File

@@ -25,6 +25,7 @@ import (
"strings" "strings"
"codeberg.org/scip/esctl/pkg/cfg" "codeberg.org/scip/esctl/pkg/cfg"
"github.com/seeruk/go-wordwrap"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
) )
@@ -39,62 +40,51 @@ type Table struct {
lenHeaders []int lenHeaders []int
alignInts bool alignInts bool
maxwidth int maxwidth int
debugGoRoutines bool
} }
func NewTable(conf *cfg.Config) *Table { func NewTable(conf *cfg.Config, columns, rows int) *Table {
return new(Table{ table := Table{Mode: conf.Output, maxwidth: cfg.GetTermWidth()}
Mode: conf.Output,
maxwidth: cfg.GetTermWidth(),
debugGoRoutines: conf.DebugGoRoutines,
Headers: []string{},
RawHeaders: []string{},
lenHeaders: []int{},
alignInts: conf.AlignInts,
})
}
func (table *Table) WithSize(columns, rows int) *Table {
if columns > 0 {
table.Headers = make([]string, columns) table.Headers = make([]string, columns)
table.RawHeaders = make([]string, columns) table.RawHeaders = make([]string, columns)
table.lenHeaders = make([]int, columns)
}
if rows > 0 {
table.Entries = make([][]any, rows) table.Entries = make([][]any, rows)
} table.lenHeaders = make([]int, columns)
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 { func (table *Table) WithHeaders(headers ...string) *Table {
count := len(headers) count := len(headers)
table.WithSize(count, 0).Addheaders(headers...) table.Entries = [][]any{}
table.lenHeaders = make([]int, count)
table.Headers = make([]string, count)
table.RawHeaders = make([]string, count)
table.Addheaders(headers...)
return table return table
} }
func (table *Table) Print() error { func (table *Table) Print() error {
var err error
switch table.Mode { switch table.Mode {
case "json": case "json":
err = table.PrintJSON() return table.PrintJSON()
case "yaml": case "yaml":
err = table.PrintYAML() return table.PrintYAML()
case "csv": case "csv":
err = table.PrintCSV() return table.PrintCSV()
default: default:
err = table.PrintTSV() return table.PrintTSV()
} }
if table.debugGoRoutines {
printGoRoutineMetrics()
}
return err
} }
var ( var (
@@ -149,17 +139,23 @@ func (table *Table) PrintTSV() error {
for _, entries := range table.rows { for _, entries := range table.rows {
currentWidth := 0 currentWidth := 0
columns := len(entries)
for idx, entry := range entries { for idx, entry := range entries {
length := visibleLen(entry) length := visibleLen(entry)
if length+currentWidth > table.maxwidth && if length+currentWidth > table.maxwidth && table.maxwidth-currentWidth > 1 {
table.maxwidth-currentWidth > 1 && // text is too wide to be put into one line, wrap it
idx == columns-1 { wrapper := wordwrap.Wrapper(table.maxwidth-currentWidth, false)
// text is too wide to be put into one line, and wrapped := wrapper(entry)
// it's the last cell, so wrap it
entry = wrap(table.maxwidth-currentWidth, currentWidth+2, entry) // and indent it
for idx, line := range strings.Split(wrapped, "\n") {
if idx == 0 {
entry = line
} else {
entry += "\n " + strings.Repeat(" ", currentWidth) + line
}
}
} }
currentWidth += table.lenHeaders[idx] currentWidth += table.lenHeaders[idx]
@@ -187,48 +183,6 @@ func (table *Table) PrintTSV() error {
return nil return nil
} }
// Wrap a text into multiple lines, first line is not indented, all
// further lines will be indented. Used within Print() to print large
// cell text.
func wrap(width, indent int, text string) string {
if len(text) <= width {
return text
}
wrapped := ""
line := ""
for word := range strings.FieldsSeq(text) {
if len(line)+len(word)+1 <= width {
// appending word to current line doesn't exceed width
if line != "" {
line += " "
}
line += word
} else {
// it exceeds it, so we need to wrap
if wrapped == "" {
// beginning of output, no indenting here
wrapped = line + "\n"
} else {
// we're in the middle of the text, so add the indent
wrapped += strings.Repeat(" ", indent) + line + "\n"
}
// remember the current word for the next round
line = word
}
}
if line != "" {
// last line, no newline needed here
wrapped += strings.Repeat(" ", indent) + line
}
return wrapped
}
func (table *Table) PrintCSV() error { func (table *Table) PrintCSV() error {
table.preprocessRows() table.preprocessRows()
@@ -270,7 +224,7 @@ func (table *Table) Addheaders(headers ...string) {
case "json", "yaml": case "json", "yaml":
table.Headers[idx] = strings.ReplaceAll(strings.ToLower(header), " ", "_") table.Headers[idx] = strings.ReplaceAll(strings.ToLower(header), " ", "_")
default: default:
table.Headers[idx] = Bold(strings.ReplaceAll(strings.ToUpper(header), " ", "-")) table.Headers[idx] = bold(strings.ReplaceAll(strings.ToUpper(header), " ", "-"))
} }
table.RawHeaders[idx] = header table.RawHeaders[idx] = header

View File

@@ -1,4 +1,4 @@
.PHONY: test clean cluster docs search up down delete runenv .PHONY: test clean cluster docs search up down delete
# docker stuff # docker stuff
up: up:
@@ -20,8 +20,6 @@ delete:
clean-docker: down delete clean-docker: down delete
runenv: up waitup cluster docs wait
# mosscap esctl stuff # mosscap esctl stuff
test: up waitup cluster docs wait search test: up waitup cluster docs wait search