mirror of
https://codeberg.org/scip/esctl.git
synced 2026-08-24 21:44:19 +02:00
Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e16f2bfd0b | ||
|
|
6d31d18e0c | ||
| 355e3051b4 | |||
|
|
a81d6b94e9 | ||
|
|
9b5ba0775b | ||
|
|
802217158c | ||
| 5d345aaa9a | |||
| 2f029304f3 | |||
| 49bc2c7acb | |||
|
|
f1ff361c12 | ||
| 072e949147 | |||
|
|
ce4a60f8e2 | ||
|
|
97004e0957 | ||
|
|
d1f3082e5a | ||
|
|
d0d1d69791 | ||
|
|
ec1ce56b2d | ||
| 925b21823a | |||
| 9b9b394539 | |||
|
|
b9eb3e3e2f | ||
|
|
f30c837d53 | ||
| c4143093fd | |||
| 7e9a9f82a7 | |||
| 0f48a17536 | |||
|
|
01e0fd024b | ||
|
|
5ecba5abe6 | ||
|
|
18af1b6073 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -30,3 +30,5 @@ esctl
|
|||||||
*.sh
|
*.sh
|
||||||
*.json
|
*.json
|
||||||
*.log
|
*.log
|
||||||
|
|
||||||
|
cpu.profile
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ matrix:
|
|||||||
platform:
|
platform:
|
||||||
- linux/amd64
|
- linux/amd64
|
||||||
goversion:
|
goversion:
|
||||||
- 1.25
|
- 1.25.8
|
||||||
|
|
||||||
labels:
|
labels:
|
||||||
platform: ${platform}
|
platform: ${platform}
|
||||||
|
|||||||
23
Makefile
23
Makefile
@@ -28,12 +28,22 @@ COMMIT = $(shell git rev-parse --short=8 HEAD)
|
|||||||
BUILD = $(shell date +%Y.%m.%d.%H%M%S)
|
BUILD = $(shell date +%Y.%m.%d.%H%M%S)
|
||||||
GOVERSION = $(shell grep -E "^go" go.mod | cut -d' ' -f2)
|
GOVERSION = $(shell grep -E "^go" go.mod | cut -d' ' -f2)
|
||||||
APIVERSION = $(shell grep github.com/elastic/go-elasticsearch go.mod | cut -d' ' -f2)
|
APIVERSION = $(shell grep github.com/elastic/go-elasticsearch go.mod | cut -d' ' -f2)
|
||||||
|
APISPEC = "https://www.elastic.co/docs/api/doc/elasticsearch.json"
|
||||||
|
|
||||||
all: buildlocal
|
all: buildlocal
|
||||||
|
|
||||||
|
|
||||||
buildlocal:
|
# pkg/es/openspec.go:
|
||||||
|
# echo "package es" > pkg/es/openspec.go
|
||||||
|
# echo >> pkg/es/openspec.go
|
||||||
|
# echo "const Openspec = \`" >> pkg/es/openspec.go
|
||||||
|
# curl -o - $(APISPEC) | sed "s/\`/'/g" >> pkg/es/openspec.go
|
||||||
|
# echo "\`" >> pkg/es/openspec.go
|
||||||
|
|
||||||
|
assets/openapi.json:
|
||||||
|
curl -o assets/openapi.json $(APISPEC)
|
||||||
|
|
||||||
|
buildlocal: assets/openapi.json
|
||||||
CGO_LDFLAGS='-static' go build -tags osusergo,netgo -ldflags "-extldflags=-static" \
|
CGO_LDFLAGS='-static' go build -tags osusergo,netgo -ldflags "-extldflags=-static" \
|
||||||
-ldflags "-X codeberg.org/scip/esctl/pkg/cfg.BRANCH=$(BRANCH) \
|
-ldflags "-X codeberg.org/scip/esctl/pkg/cfg.BRANCH=$(BRANCH) \
|
||||||
-X codeberg.org/scip/esctl/pkg/cfg.COMMIT=$(COMMIT) \
|
-X codeberg.org/scip/esctl/pkg/cfg.COMMIT=$(COMMIT) \
|
||||||
@@ -50,7 +60,7 @@ install: buildlocal
|
|||||||
install -o $(UID) -g $(GID) -m 444 $(tool).1 $(PREFIX)/man/man1/
|
install -o $(UID) -g $(GID) -m 444 $(tool).1 $(PREFIX)/man/man1/
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
rm -rf $(tool) coverage.out testdata t/out
|
rm -rf $(tool) coverage.out testdata t/out pkg/es/openspec.go
|
||||||
|
|
||||||
test: clean
|
test: clean
|
||||||
mkdir -p t/out
|
mkdir -p t/out
|
||||||
@@ -96,5 +106,8 @@ show-versions: buildlocal
|
|||||||
@echo "### go version used for building:"
|
@echo "### go version used for building:"
|
||||||
@grep -m 1 go go.mod
|
@grep -m 1 go go.mod
|
||||||
|
|
||||||
# lint:
|
.PHONY: profile
|
||||||
# golangci-lint run -p bugs -p unused
|
profile: buildlocal
|
||||||
|
./esctl api ls --profile-file cpu.profile
|
||||||
|
go tool pprof -text esctl cpu.profile
|
||||||
|
go tool pprof --http localhost:8888 ./esctl cpu.profile
|
||||||
|
|||||||
100
README.md
100
README.md
@@ -4,11 +4,57 @@
|
|||||||
|
|
||||||
Elasticsearch CLI
|
Elasticsearch CLI
|
||||||
|
|
||||||
|
## Introduction
|
||||||
|
|
||||||
|
This is a handy cli tool which interfaces to an elasticsearch cluster
|
||||||
|
(or two of them if you're using cross cluster replication). It is a
|
||||||
|
work-in-progress project yet, things might change occasionally. Expect
|
||||||
|
a stable release once we reach major version 1.0.0.
|
||||||
|
|
||||||
|
Features:
|
||||||
|
|
||||||
|
- Configuration of cluster credentials using environment vars or
|
||||||
|
config file. Multiple clusters can be configured. `esctl cluster ls`
|
||||||
|
shows which one is reachable.
|
||||||
|
- Shell completion support (bash, zsh and fish). Put this into your
|
||||||
|
rc: `source <(esctl completion bash)`.
|
||||||
|
- Cluster settings can be viewed and modified.
|
||||||
|
- Search: you can search indices using full text or by fields, select
|
||||||
|
logical condition (OR, AND), use PIT, limit datetime (ES date math
|
||||||
|
can be used), etc. It is however not yet possible to create
|
||||||
|
recursive searches like: `(cond1 AND cond2) OR (cond3 OR cond4)`.
|
||||||
|
- Cross cluster replication (ccr): view, pause, resume, delete
|
||||||
|
replication. You can also manage follower configuration.
|
||||||
|
- Index management: manage aliases, create, modify, delete indices,
|
||||||
|
display field mappings etc.
|
||||||
|
- Node management: only list nodes yet.
|
||||||
|
- Shard management: only list shards yet.
|
||||||
|
- Snapshot management: only list snapshots yet.
|
||||||
|
- Role management: only list roles yet. There's also a `role diff`
|
||||||
|
subcommand, which is for internal use. It can be used to verify if
|
||||||
|
role defs in a CSV match the deployed roles.
|
||||||
|
- Repl: this is an interactive REPL (read eval print loop) towards the
|
||||||
|
elasticsearch API. You can run API calls on the current selected
|
||||||
|
cluster w/o the hassle to specify the whole url, credentials etc. It
|
||||||
|
has line editing and history support. If `jq` is installed output
|
||||||
|
JSON will be syntax highlighted.
|
||||||
|
- 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:
|
||||||
|
`esctl index create foo` and then insert docs into it for search
|
||||||
|
experiments:
|
||||||
|
```console
|
||||||
|
esctl doc add -i foo '{"title":"curry in a hurry", "message":"australian thai"}'
|
||||||
|
```
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
Command tree:
|
Command tree:
|
||||||
|
|
||||||
```console
|
```console
|
||||||
|
api
|
||||||
|
list
|
||||||
|
repl
|
||||||
|
show
|
||||||
ccr
|
ccr
|
||||||
follower
|
follower
|
||||||
add
|
add
|
||||||
@@ -28,30 +74,51 @@ Command tree:
|
|||||||
list
|
list
|
||||||
set
|
set
|
||||||
status
|
status
|
||||||
|
datastream
|
||||||
|
create
|
||||||
|
delete
|
||||||
|
list
|
||||||
|
rollover
|
||||||
|
show
|
||||||
debug
|
debug
|
||||||
doc
|
doc
|
||||||
add
|
add
|
||||||
delete
|
delete
|
||||||
show
|
show
|
||||||
help
|
help
|
||||||
|
help-jsonpath
|
||||||
|
ilm
|
||||||
|
create
|
||||||
|
list
|
||||||
|
retry
|
||||||
|
show
|
||||||
|
status
|
||||||
index
|
index
|
||||||
alias
|
alias
|
||||||
create
|
create
|
||||||
delete
|
delete
|
||||||
list
|
list
|
||||||
|
rollover
|
||||||
allocation
|
allocation
|
||||||
close
|
close
|
||||||
create
|
create
|
||||||
delete
|
delete
|
||||||
fields
|
fields
|
||||||
|
ilm
|
||||||
|
list
|
||||||
|
modify
|
||||||
|
show
|
||||||
|
template
|
||||||
|
create
|
||||||
|
delete
|
||||||
list
|
list
|
||||||
modify
|
modify
|
||||||
show
|
show
|
||||||
node
|
node
|
||||||
list
|
list
|
||||||
show
|
show
|
||||||
repl
|
|
||||||
role
|
role
|
||||||
|
diff
|
||||||
list
|
list
|
||||||
show
|
show
|
||||||
search
|
search
|
||||||
@@ -61,6 +128,9 @@ Command tree:
|
|||||||
snapshot
|
snapshot
|
||||||
list
|
list
|
||||||
show
|
show
|
||||||
|
task
|
||||||
|
cancel
|
||||||
|
list
|
||||||
version
|
version
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -91,10 +161,6 @@ can omit `-c ...`.
|
|||||||
If you want to work on a specific cluster, specify its name with the
|
If you want to work on a specific cluster, specify its name with the
|
||||||
global `-C` option.
|
global `-C` option.
|
||||||
|
|
||||||
## Introduction
|
|
||||||
|
|
||||||
FIXME
|
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
The tool does not have any dependencies. Just download the binary for
|
The tool does not have any dependencies. Just download the binary for
|
||||||
@@ -145,6 +211,30 @@ make
|
|||||||
sudo make install
|
sudo make install
|
||||||
```
|
```
|
||||||
|
|
||||||
|
# Development
|
||||||
|
|
||||||
|
## To test completion
|
||||||
|
|
||||||
|
Add the flag `--generate-shell-completion` to any command, e.g.:
|
||||||
|
|
||||||
|
```console
|
||||||
|
./esctl role show --generate-shell-completion
|
||||||
|
machine_learning_admin
|
||||||
|
rollup_admin
|
||||||
|
editor
|
||||||
|
reporting_user
|
||||||
|
snapshot_user
|
||||||
|
fcn_admin
|
||||||
|
machine_learning_user
|
||||||
|
kibana_system
|
||||||
|
beats_admin
|
||||||
|
kibana_user
|
||||||
|
fcns_space
|
||||||
|
transport_client
|
||||||
|
transform_user
|
||||||
|
[..]
|
||||||
|
```
|
||||||
|
|
||||||
# Report bugs
|
# Report bugs
|
||||||
|
|
||||||
[Please open an issue](https://codeberg.org/scip/esctl/issues). Thanks!
|
[Please open an issue](https://codeberg.org/scip/esctl/issues). Thanks!
|
||||||
|
|||||||
26
TODO.md
26
TODO.md
@@ -1,33 +1,13 @@
|
|||||||
- [Go client docs](https://www.elastic.co/docs/reference/elasticsearch/clients/go/typed-api)
|
- [Go client docs](https://www.elastic.co/docs/reference/elasticsearch/clients/go/typed-api)
|
||||||
- [ES API docs](https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-indices-get)
|
- [ES API docs](https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-indices-get)
|
||||||
|
|
||||||
- Fix index names custom completion
|
|
||||||
https://github.com/urfave/cli/issues/2332
|
|
||||||
https://github.com/urfave/cli/issues/2333
|
|
||||||
|
|
||||||
- index show: add more details, see screenshots
|
- index show: add more details, see screenshots
|
||||||
|
|
||||||
- add shard explain, aka:
|
- add shard explain, aka:
|
||||||
get /_cluster/allocation/explain {"index":"yourindex", "primary": true, "shard":0}
|
get /_cluster/allocation/explain {"index":"yourindex", "primary": true, "shard":0}
|
||||||
|
|
||||||
|
|
||||||
- add validate:
|
- add datastream support:
|
||||||
|
https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-indices-get-data-stream
|
||||||
|
|
||||||
> GET /mock/_validate/query?rewrite=true {"from":0,"query":{"bool":{"must":[{"match_all":{}}]}},"size":20,"sort":[{"name":{"order":"desc"}}]}
|
also exclude data stream backing indices from index ls
|
||||||
{
|
|
||||||
"valid": false
|
|
||||||
}
|
|
||||||
|
|
||||||
- add explain to search (maybe option -e)
|
|
||||||
|
|
||||||
> GET /mock/_explain/1780043878 {"query":{"bool":{"must":[{"match_all":{}}]}}}
|
|
||||||
{
|
|
||||||
"_index": "mock",
|
|
||||||
"_id": "1780043878",
|
|
||||||
"matched": true,
|
|
||||||
"explanation": {
|
|
||||||
"value": 1.0,
|
|
||||||
"description": "*:*",
|
|
||||||
"details": []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
43
assets/assets.go
Normal file
43
assets/assets.go
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
/*
|
||||||
|
Copyright © 2026 Thomas von Dein
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package assets
|
||||||
|
|
||||||
|
import (
|
||||||
|
"embed"
|
||||||
|
|
||||||
|
"github.com/go-openapi/loads"
|
||||||
|
"github.com/go-openapi/swag/loading"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed *.json
|
||||||
|
var AssetFS embed.FS
|
||||||
|
|
||||||
|
var OpenAPI *loads.Document
|
||||||
|
|
||||||
|
func LoadAssetOpenApi() {
|
||||||
|
doc, err := loads.Spec(
|
||||||
|
"openapi.json",
|
||||||
|
loads.WithLoadingOptions(
|
||||||
|
loading.WithFS(AssetFS),
|
||||||
|
))
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
OpenAPI = doc
|
||||||
|
}
|
||||||
160985
assets/openapi.json
Normal file
160985
assets/openapi.json
Normal file
File diff suppressed because one or more lines are too long
90
cmd/api.go
Normal file
90
cmd/api.go
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
/*
|
||||||
|
Copyright © 2026 Thomas von Dein
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"codeberg.org/scip/esctl/pkg/cfg"
|
||||||
|
"codeberg.org/scip/esctl/pkg/es"
|
||||||
|
|
||||||
|
"github.com/urfave/cli/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Api(conf *cfg.Config) *cli.Command {
|
||||||
|
return &cli.Command{
|
||||||
|
Name: "api",
|
||||||
|
Usage: "manage apiuments",
|
||||||
|
|
||||||
|
Commands: []*cli.Command{
|
||||||
|
ApiList(conf),
|
||||||
|
ApiShow(conf),
|
||||||
|
ApiRepl(conf),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ApiList(conf *cfg.Config) *cli.Command {
|
||||||
|
return &cli.Command{
|
||||||
|
Name: "list",
|
||||||
|
Aliases: []string{"ls"},
|
||||||
|
Usage: "list index of API calls",
|
||||||
|
UsageText: "list [<pattern>]",
|
||||||
|
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "tag",
|
||||||
|
Usage: "list only operations matching <tag>",
|
||||||
|
Destination: &conf.Tag,
|
||||||
|
Aliases: []string{"t"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||||
|
return es.ApiList(conf, cmd.Args().Get(0))
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ApiShow(conf *cfg.Config) *cli.Command {
|
||||||
|
return &cli.Command{
|
||||||
|
Name: "show",
|
||||||
|
Aliases: []string{"sh"},
|
||||||
|
Usage: "show an API doc",
|
||||||
|
UsageText: "show <pattern> [<verb>]",
|
||||||
|
|
||||||
|
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||||
|
return es.ApiShow(conf, cmd.Args().Get(0), cmd.Args().Get(1))
|
||||||
|
},
|
||||||
|
|
||||||
|
ShellComplete: func(ctx context.Context, cmd *cli.Command) {
|
||||||
|
complete(cmd, Capi)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ApiRepl(conf *cfg.Config) *cli.Command {
|
||||||
|
return &cli.Command{
|
||||||
|
Name: "repl",
|
||||||
|
Aliases: []string{"shell"},
|
||||||
|
Usage: "interactive API repl",
|
||||||
|
|
||||||
|
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||||
|
return es.ApiRepl(conf)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -47,6 +47,7 @@ func CcrStatus(conf *cfg.Config) *cli.Command {
|
|||||||
Name: "status",
|
Name: "status",
|
||||||
Aliases: []string{"st"},
|
Aliases: []string{"st"},
|
||||||
Usage: "cross cluster replication status (yaml config with 2 clusters required)",
|
Usage: "cross cluster replication status (yaml config with 2 clusters required)",
|
||||||
|
UsageText: "status <leader> <follower>",
|
||||||
|
|
||||||
Flags: []cli.Flag{
|
Flags: []cli.Flag{
|
||||||
&cli.StringFlag{
|
&cli.StringFlag{
|
||||||
@@ -57,6 +58,10 @@ func CcrStatus(conf *cfg.Config) *cli.Command {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
ShellComplete: func(ctx context.Context, cmd *cli.Command) {
|
||||||
|
complete(cmd, Ccluster)
|
||||||
|
},
|
||||||
|
|
||||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||||
leader := cmd.Args().Get(0)
|
leader := cmd.Args().Get(0)
|
||||||
follower := cmd.Args().Get(1)
|
follower := cmd.Args().Get(1)
|
||||||
@@ -109,6 +114,10 @@ func CcrRemoteInfo(conf *cfg.Config) *cli.Command {
|
|||||||
Usage: "show ccr remote info",
|
Usage: "show ccr remote info",
|
||||||
UsageText: "info [options] [<index>]",
|
UsageText: "info [options] [<index>]",
|
||||||
|
|
||||||
|
ShellComplete: func(ctx context.Context, cmd *cli.Command) {
|
||||||
|
complete(cmd, Cindex)
|
||||||
|
},
|
||||||
|
|
||||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||||
return es.CcrRemoteInfo(conf, cmd.Args().Get(0))
|
return es.CcrRemoteInfo(conf, cmd.Args().Get(0))
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -59,6 +59,10 @@ func CcrFollowerRenew(conf *cfg.Config) *cli.Command {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
ShellComplete: func(ctx context.Context, cmd *cli.Command) {
|
||||||
|
complete(cmd, Cindex)
|
||||||
|
},
|
||||||
|
|
||||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||||
args := cmd.Args()
|
args := cmd.Args()
|
||||||
|
|
||||||
@@ -77,6 +81,10 @@ func CcrFollowerResume(conf *cfg.Config) *cli.Command {
|
|||||||
Usage: "resume ccr index to follow",
|
Usage: "resume ccr index to follow",
|
||||||
UsageText: "resume [options] <index>",
|
UsageText: "resume [options] <index>",
|
||||||
|
|
||||||
|
ShellComplete: func(ctx context.Context, cmd *cli.Command) {
|
||||||
|
complete(cmd, Cindex)
|
||||||
|
},
|
||||||
|
|
||||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||||
args := cmd.Args()
|
args := cmd.Args()
|
||||||
|
|
||||||
@@ -95,6 +103,10 @@ func CcrFollowerPause(conf *cfg.Config) *cli.Command {
|
|||||||
Usage: "pause ccr index to follow",
|
Usage: "pause ccr index to follow",
|
||||||
UsageText: "pause [options] <index>",
|
UsageText: "pause [options] <index>",
|
||||||
|
|
||||||
|
ShellComplete: func(ctx context.Context, cmd *cli.Command) {
|
||||||
|
complete(cmd, Cindex)
|
||||||
|
},
|
||||||
|
|
||||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||||
args := cmd.Args()
|
args := cmd.Args()
|
||||||
|
|
||||||
@@ -113,6 +125,10 @@ func CcrFollowerUnfollow(conf *cfg.Config) *cli.Command {
|
|||||||
Usage: "unfollow ccr follower index",
|
Usage: "unfollow ccr follower index",
|
||||||
UsageText: "unfollow [options] <index>",
|
UsageText: "unfollow [options] <index>",
|
||||||
|
|
||||||
|
ShellComplete: func(ctx context.Context, cmd *cli.Command) {
|
||||||
|
complete(cmd, Cindex)
|
||||||
|
},
|
||||||
|
|
||||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||||
args := cmd.Args()
|
args := cmd.Args()
|
||||||
|
|
||||||
@@ -141,6 +157,10 @@ func CcrFollowerAdd(conf *cfg.Config) *cli.Command {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
ShellComplete: func(ctx context.Context, cmd *cli.Command) {
|
||||||
|
complete(cmd, Cindex)
|
||||||
|
},
|
||||||
|
|
||||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||||
args := cmd.Args()
|
args := cmd.Args()
|
||||||
|
|
||||||
@@ -160,6 +180,10 @@ func CcrFollowerDelete(conf *cfg.Config) *cli.Command {
|
|||||||
Usage: "delete ccr follower index",
|
Usage: "delete ccr follower index",
|
||||||
UsageText: "delete <index>",
|
UsageText: "delete <index>",
|
||||||
|
|
||||||
|
ShellComplete: func(ctx context.Context, cmd *cli.Command) {
|
||||||
|
complete(cmd, Cindex)
|
||||||
|
},
|
||||||
|
|
||||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||||
args := cmd.Args()
|
args := cmd.Args()
|
||||||
|
|
||||||
@@ -180,6 +204,10 @@ func CcrFollowerShow(conf *cfg.Config) *cli.Command {
|
|||||||
Usage: "show ccr follower index details",
|
Usage: "show ccr follower index details",
|
||||||
UsageText: "show <index>",
|
UsageText: "show <index>",
|
||||||
|
|
||||||
|
ShellComplete: func(ctx context.Context, cmd *cli.Command) {
|
||||||
|
complete(cmd, Cindex)
|
||||||
|
},
|
||||||
|
|
||||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||||
args := cmd.Args()
|
args := cmd.Args()
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
SETTINGS = `https://www.elastic.co/docs/reference/elasticsearch/configuration-reference`
|
SETTINGS = ``
|
||||||
)
|
)
|
||||||
|
|
||||||
func ClusterSettings(conf *cfg.Config) *cli.Command {
|
func ClusterSettings(conf *cfg.Config) *cli.Command {
|
||||||
@@ -85,7 +85,9 @@ func ClusterSettingsSet(conf *cfg.Config) *cli.Command {
|
|||||||
Name: "set",
|
Name: "set",
|
||||||
Usage: "set|update cluster settings",
|
Usage: "set|update cluster settings",
|
||||||
Aliases: []string{"set", "update"},
|
Aliases: []string{"set", "update"},
|
||||||
UsageText: "set [options] setting:value [setting:value ...]\n\nReference: " + SETTINGS,
|
UsageText: "set [options] setting:value [setting:value ...]",
|
||||||
|
CustomHelpTemplate: addReference(
|
||||||
|
"https://www.elastic.co/docs/reference/elasticsearch/configuration-reference"),
|
||||||
|
|
||||||
Flags: []cli.Flag{
|
Flags: []cli.Flag{
|
||||||
&cli.BoolFlag{
|
&cli.BoolFlag{
|
||||||
|
|||||||
76
cmd/completion.go
Normal file
76
cmd/completion.go
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
/*
|
||||||
|
Copyright © 2026 Thomas von Dein
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"codeberg.org/scip/esctl/pkg/cfg"
|
||||||
|
"codeberg.org/scip/esctl/pkg/es"
|
||||||
|
|
||||||
|
"github.com/urfave/cli/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
Cindex = iota
|
||||||
|
Cdatastream
|
||||||
|
Crole
|
||||||
|
Ccluster
|
||||||
|
Capi
|
||||||
|
Cilm
|
||||||
|
)
|
||||||
|
|
||||||
|
func complete(cmd *cli.Command, what int) {
|
||||||
|
if cmd.NArg() > 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME: config should load from root.Before(), see https://github.com/urfave/cli/issues/2348
|
||||||
|
// workaround: load it directly here
|
||||||
|
conf := cfg.NewConfig()
|
||||||
|
if err := conf.Init(); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var list []string
|
||||||
|
var err error
|
||||||
|
|
||||||
|
switch what {
|
||||||
|
case Cindex:
|
||||||
|
list, err = es.IndexNames(conf)
|
||||||
|
case Crole:
|
||||||
|
list, err = es.RoleNames(conf)
|
||||||
|
case Ccluster:
|
||||||
|
for cluster := range conf.Clusters {
|
||||||
|
list = append(list, cluster)
|
||||||
|
}
|
||||||
|
case Cdatastream:
|
||||||
|
list, err = es.DatastreamNames(conf)
|
||||||
|
case Capi:
|
||||||
|
list = es.ApiPathNames()
|
||||||
|
case Cilm:
|
||||||
|
list, err = es.IlmNames(conf)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, item := range list {
|
||||||
|
fmt.Println(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
220
cmd/datastream.go
Normal file
220
cmd/datastream.go
Normal file
@@ -0,0 +1,220 @@
|
|||||||
|
/*
|
||||||
|
Copyright © 2026 Thomas von Dein
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"codeberg.org/scip/esctl/pkg/cfg"
|
||||||
|
"codeberg.org/scip/esctl/pkg/es"
|
||||||
|
|
||||||
|
"github.com/urfave/cli/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Datastream(conf *cfg.Config) *cli.Command {
|
||||||
|
return &cli.Command{
|
||||||
|
Name: "datastream",
|
||||||
|
Aliases: []string{"ds"},
|
||||||
|
Usage: "manage data streams",
|
||||||
|
|
||||||
|
Commands: []*cli.Command{
|
||||||
|
DatastreamList(conf),
|
||||||
|
DatastreamShow(conf),
|
||||||
|
DatastreamCreate(conf),
|
||||||
|
DatastreamDelete(conf),
|
||||||
|
DatastreamRollover(conf),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func DatastreamList(conf *cfg.Config) *cli.Command {
|
||||||
|
return &cli.Command{
|
||||||
|
Name: "list",
|
||||||
|
Aliases: []string{"ls"},
|
||||||
|
Usage: "list indicies",
|
||||||
|
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
&cli.IntFlag{
|
||||||
|
Name: "max",
|
||||||
|
Usage: "max number of items to process",
|
||||||
|
Destination: &conf.MaxItems,
|
||||||
|
Aliases: []string{"m"},
|
||||||
|
},
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: "hidden",
|
||||||
|
Usage: "include hidden data streams",
|
||||||
|
Destination: &conf.Hidden,
|
||||||
|
Aliases: []string{"H"},
|
||||||
|
},
|
||||||
|
&cli.StringSliceFlag{
|
||||||
|
Name: "filter",
|
||||||
|
Usage: "show only data streams matching the filter",
|
||||||
|
Destination: &conf.Filter,
|
||||||
|
Aliases: []string{"F"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||||
|
return es.DatastreamList(conf)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func DatastreamShow(conf *cfg.Config) *cli.Command {
|
||||||
|
return &cli.Command{
|
||||||
|
Name: "show",
|
||||||
|
Aliases: []string{"sh"},
|
||||||
|
Usage: "show details about an data stream",
|
||||||
|
|
||||||
|
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||||
|
index := cmd.Args().Get(0)
|
||||||
|
if index == "" {
|
||||||
|
return errors.New("no data stream specified")
|
||||||
|
}
|
||||||
|
|
||||||
|
return es.DatastreamShow(conf, cmd.Args().Get(0))
|
||||||
|
},
|
||||||
|
|
||||||
|
ShellComplete: func(ctx context.Context, cmd *cli.Command) {
|
||||||
|
complete(cmd, Cdatastream)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func DatastreamCreate(conf *cfg.Config) *cli.Command {
|
||||||
|
return &cli.Command{
|
||||||
|
Name: "create",
|
||||||
|
Aliases: []string{"+"},
|
||||||
|
Usage: "create a new data stream",
|
||||||
|
UsageText: "create [options] <name>",
|
||||||
|
|
||||||
|
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||||
|
index := cmd.Args().Get(0)
|
||||||
|
if index == "" {
|
||||||
|
return errors.New("no data stream specified")
|
||||||
|
}
|
||||||
|
|
||||||
|
return es.DatastreamCreate(conf, cmd.Args().Get(0))
|
||||||
|
},
|
||||||
|
|
||||||
|
ShellComplete: func(ctx context.Context, cmd *cli.Command) {
|
||||||
|
complete(cmd, Cdatastream)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func DatastreamDelete(conf *cfg.Config) *cli.Command {
|
||||||
|
return &cli.Command{
|
||||||
|
Name: "delete",
|
||||||
|
Aliases: []string{"rm"},
|
||||||
|
Usage: "delete a data stream",
|
||||||
|
UsageText: "delete [options] <name>",
|
||||||
|
|
||||||
|
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||||
|
index := cmd.Args().Get(0)
|
||||||
|
if index == "" {
|
||||||
|
return errors.New("no data stream specified")
|
||||||
|
}
|
||||||
|
|
||||||
|
return es.DatastreamDelete(conf, cmd.Args().Get(0))
|
||||||
|
},
|
||||||
|
|
||||||
|
ShellComplete: func(ctx context.Context, cmd *cli.Command) {
|
||||||
|
complete(cmd, Cdatastream)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func DatastreamRollover(conf *cfg.Config) *cli.Command {
|
||||||
|
return &cli.Command{
|
||||||
|
Name: "rollover",
|
||||||
|
Aliases: []string{"roll"},
|
||||||
|
Usage: "roll over a data stream",
|
||||||
|
UsageText: "rollover <data stream>",
|
||||||
|
|
||||||
|
ShellComplete: func(ctx context.Context, cmd *cli.Command) {
|
||||||
|
complete(cmd, Cindex)
|
||||||
|
},
|
||||||
|
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: "wait",
|
||||||
|
Usage: "wait for active shards",
|
||||||
|
Destination: &conf.Wait,
|
||||||
|
Aliases: []string{"w"},
|
||||||
|
},
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: "dry-run",
|
||||||
|
Usage: "checks conditions but does not perform a rollover",
|
||||||
|
Destination: &conf.Wait,
|
||||||
|
Aliases: []string{"n"},
|
||||||
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "max-age",
|
||||||
|
Usage: "roll over after max age (eg: 7d, 2m, 8h)",
|
||||||
|
Destination: &conf.MaxAge,
|
||||||
|
},
|
||||||
|
&cli.IntFlag{
|
||||||
|
Name: "max-docs",
|
||||||
|
Usage: "roll over after max docs",
|
||||||
|
Destination: &conf.MaxDocs,
|
||||||
|
},
|
||||||
|
&cli.IntFlag{
|
||||||
|
Name: "max-primary-shard-size",
|
||||||
|
Usage: "roll over when primary shard size reaches size",
|
||||||
|
Destination: &conf.MaxShardSize,
|
||||||
|
},
|
||||||
|
&cli.IntFlag{
|
||||||
|
Name: "max-primary-shard-docs",
|
||||||
|
Usage: "roll over after max docs in primary shard reached",
|
||||||
|
Destination: &conf.MaxShardDocs,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||||
|
alias := cmd.Args().Get(0)
|
||||||
|
|
||||||
|
if alias == "" {
|
||||||
|
return errors.New("no data stream specified")
|
||||||
|
}
|
||||||
|
|
||||||
|
return es.DatastreamRollover(conf, alias)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func DatastreamIlm(conf *cfg.Config) *cli.Command {
|
||||||
|
return &cli.Command{
|
||||||
|
Name: "ilm",
|
||||||
|
Usage: "show ilm status",
|
||||||
|
UsageText: "ds ilm <name>",
|
||||||
|
|
||||||
|
ShellComplete: func(ctx context.Context, cmd *cli.Command) {
|
||||||
|
complete(cmd, Cdatastream)
|
||||||
|
},
|
||||||
|
|
||||||
|
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||||
|
ds := cmd.Args().Get(0)
|
||||||
|
if ds == "" {
|
||||||
|
return errors.New("no ds name specified")
|
||||||
|
}
|
||||||
|
|
||||||
|
return es.IlmExplain(conf, ds)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
14
cmd/doc.go
14
cmd/doc.go
@@ -89,19 +89,9 @@ func DocShow(conf *cfg.Config) *cli.Command {
|
|||||||
Destination: &conf.Path,
|
Destination: &conf.Path,
|
||||||
Aliases: []string{"p"},
|
Aliases: []string{"p"},
|
||||||
},
|
},
|
||||||
&cli.BoolFlag{
|
|
||||||
Name: "help-jsonpath",
|
|
||||||
Usage: "show jsonPath help",
|
|
||||||
Destination: &conf.Subhelp,
|
|
||||||
Aliases: []string{"H"},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
|
||||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||||
if conf.Subhelp {
|
|
||||||
return showJsonPathHelp()
|
|
||||||
}
|
|
||||||
|
|
||||||
args := cmd.Args()
|
args := cmd.Args()
|
||||||
|
|
||||||
if args.Len() != 1 {
|
if args.Len() != 1 {
|
||||||
@@ -161,10 +151,6 @@ func DocDelete(conf *cfg.Config) *cli.Command {
|
|||||||
},
|
},
|
||||||
|
|
||||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||||
if conf.Subhelp {
|
|
||||||
return showJsonPathHelp()
|
|
||||||
}
|
|
||||||
|
|
||||||
args := cmd.Args()
|
args := cmd.Args()
|
||||||
|
|
||||||
if args.Len() == 0 && !conf.All {
|
if args.Len() == 0 && !conf.All {
|
||||||
|
|||||||
277
cmd/ilm.go
Normal file
277
cmd/ilm.go
Normal file
@@ -0,0 +1,277 @@
|
|||||||
|
/*
|
||||||
|
Copyright © 2026 Thomas von Dein
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"codeberg.org/scip/esctl/pkg/cfg"
|
||||||
|
"codeberg.org/scip/esctl/pkg/es"
|
||||||
|
|
||||||
|
"github.com/urfave/cli/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Ilm(conf *cfg.Config) *cli.Command {
|
||||||
|
return &cli.Command{
|
||||||
|
Name: "ilm",
|
||||||
|
Usage: "manage index lifecycle",
|
||||||
|
|
||||||
|
Commands: []*cli.Command{
|
||||||
|
IlmRetry(conf),
|
||||||
|
IlmStatus(conf),
|
||||||
|
IlmList(conf),
|
||||||
|
IlmShow(conf),
|
||||||
|
IlmCreate(conf),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func IlmRetry(conf *cfg.Config) *cli.Command {
|
||||||
|
return &cli.Command{
|
||||||
|
Name: "retry",
|
||||||
|
Usage: "retry applying an ILM profile to an index",
|
||||||
|
UsageText: "retry <index>",
|
||||||
|
|
||||||
|
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||||
|
return es.IlmRetry(conf, cmd.Args().Get(0))
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func IlmStatus(conf *cfg.Config) *cli.Command {
|
||||||
|
return &cli.Command{
|
||||||
|
Name: "status",
|
||||||
|
Usage: "get the current index lifecycle management status",
|
||||||
|
|
||||||
|
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||||
|
return es.IlmStatus(conf)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func IlmList(conf *cfg.Config) *cli.Command {
|
||||||
|
return &cli.Command{
|
||||||
|
Name: "list",
|
||||||
|
Aliases: []string{"ls"},
|
||||||
|
Usage: "list index lifecycle policies",
|
||||||
|
UsageText: "list [<policy-pattern>]",
|
||||||
|
|
||||||
|
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||||
|
return es.IlmList(conf, cmd.Args().Get(0))
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func IlmShow(conf *cfg.Config) *cli.Command {
|
||||||
|
return &cli.Command{
|
||||||
|
Name: "show",
|
||||||
|
Aliases: []string{"sh"},
|
||||||
|
Usage: "show details about an index lifecycle policy",
|
||||||
|
UsageText: "show <policy>",
|
||||||
|
|
||||||
|
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||||
|
policy := cmd.Args().Get(0)
|
||||||
|
if policy == "" {
|
||||||
|
return errors.New("no policy specified")
|
||||||
|
}
|
||||||
|
|
||||||
|
return es.IlmShow(conf, cmd.Args().Get(0))
|
||||||
|
},
|
||||||
|
|
||||||
|
ShellComplete: func(ctx context.Context, cmd *cli.Command) {
|
||||||
|
complete(cmd, Cilm)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
*
|
||||||
|
{
|
||||||
|
"policy": {
|
||||||
|
"phases": {
|
||||||
|
"hot": {
|
||||||
|
"min_age": "0ms",
|
||||||
|
"actions": {
|
||||||
|
"rollover": {
|
||||||
|
"max_primary_shard_size": "50gb",
|
||||||
|
"max_age": "1d",
|
||||||
|
"max_docs": 100000000
|
||||||
|
},
|
||||||
|
"set_priority": {
|
||||||
|
"priority": 100
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"warm": {
|
||||||
|
"min_age": "7d",
|
||||||
|
"actions": {
|
||||||
|
"migrate": {},
|
||||||
|
"shrink": {
|
||||||
|
"number_of_shards": 1
|
||||||
|
},
|
||||||
|
"forcemerge": {
|
||||||
|
"max_num_segments": 1
|
||||||
|
},
|
||||||
|
"set_priority": {
|
||||||
|
"priority": 50
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cold": {
|
||||||
|
"min_age": "30d",
|
||||||
|
"actions": {
|
||||||
|
"migrate": {},
|
||||||
|
"searchable_snapshot": {
|
||||||
|
"snapshot_repository": "s3-logs-archive",
|
||||||
|
"force_merge_index": true
|
||||||
|
},
|
||||||
|
"set_priority": {
|
||||||
|
"priority": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"delete": {
|
||||||
|
"min_age": "90d",
|
||||||
|
"actions": {
|
||||||
|
"delete": {
|
||||||
|
"delete_searchable_snapshot": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
func IlmCreate(conf *cfg.Config) *cli.Command {
|
||||||
|
return &cli.Command{
|
||||||
|
Name: "create",
|
||||||
|
Aliases: []string{"+"},
|
||||||
|
Usage: "create a index lifecycle policy",
|
||||||
|
UsageText: "create [options] <policy>",
|
||||||
|
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "description",
|
||||||
|
Usage: "policy description",
|
||||||
|
Destination: &conf.Ilm.Description,
|
||||||
|
Aliases: []string{"D"},
|
||||||
|
},
|
||||||
|
|
||||||
|
// hot
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "hot-min-age",
|
||||||
|
Usage: "hot phase: min age",
|
||||||
|
Destination: &conf.Ilm.HotMinAge,
|
||||||
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "hot-rollover-max-age",
|
||||||
|
Usage: "hot phase: rollover after",
|
||||||
|
Destination: &conf.Ilm.HotRolloverMaxAge,
|
||||||
|
},
|
||||||
|
&cli.Int64Flag{
|
||||||
|
Name: "hot-rollover-max-primary-shard-size",
|
||||||
|
Usage: "hot phase: max primary shard size",
|
||||||
|
Destination: &conf.Ilm.HotRolloverMaxPrimaryShardSize,
|
||||||
|
},
|
||||||
|
&cli.Int64Flag{
|
||||||
|
Name: "hot-rollover-max-docs",
|
||||||
|
Usage: "hot phase: max docs",
|
||||||
|
Destination: &conf.Ilm.HotRolloverMaxDocs,
|
||||||
|
},
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: "hot-readonly",
|
||||||
|
Usage: "hot phase: make index ro before rollover",
|
||||||
|
Destination: &conf.Ilm.HotReadonly,
|
||||||
|
},
|
||||||
|
|
||||||
|
// warm
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "warm-min-age",
|
||||||
|
Usage: "warm phase: min age",
|
||||||
|
Destination: &conf.Ilm.WarmMinAge,
|
||||||
|
},
|
||||||
|
&cli.IntFlag{
|
||||||
|
Name: "warm-force-merge-segments",
|
||||||
|
Usage: "warm phase: force merge segments per shard",
|
||||||
|
Destination: &conf.Ilm.WarmForceMerge,
|
||||||
|
},
|
||||||
|
&cli.IntFlag{
|
||||||
|
Name: "warm-shrink-shards",
|
||||||
|
Usage: "warm phase: shrink on number of shards",
|
||||||
|
Destination: &conf.Ilm.WarmShrinkShards,
|
||||||
|
},
|
||||||
|
&cli.IntFlag{
|
||||||
|
Name: "warm-priority",
|
||||||
|
Usage: "warm phase: priority",
|
||||||
|
Destination: &conf.Ilm.WarmPriority,
|
||||||
|
},
|
||||||
|
|
||||||
|
// cold
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "cold-min-age",
|
||||||
|
Usage: "cold phase: min age",
|
||||||
|
Destination: &conf.Ilm.ColdMinAge,
|
||||||
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "cold-searchable-snapshot-repo",
|
||||||
|
Usage: "cold phase: searchable snapshot repo",
|
||||||
|
Destination: &conf.Ilm.ColdSearchableSnapshotRepo,
|
||||||
|
},
|
||||||
|
&cli.IntFlag{
|
||||||
|
Name: "cold-force-merge-segments",
|
||||||
|
Usage: "cold phase: force merge segments per shard",
|
||||||
|
Destination: &conf.Ilm.ColdForceMerge,
|
||||||
|
},
|
||||||
|
&cli.IntFlag{
|
||||||
|
Name: "cold-priority",
|
||||||
|
Usage: "cold phase: priority",
|
||||||
|
Destination: &conf.Ilm.ColdPriority,
|
||||||
|
},
|
||||||
|
|
||||||
|
// frozen
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "frozen-min-age",
|
||||||
|
Usage: "frozen phase: min age",
|
||||||
|
Destination: &conf.Ilm.FrozenMinAge,
|
||||||
|
},
|
||||||
|
|
||||||
|
// delete
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "delete-min-age",
|
||||||
|
Usage: "delete phase: min age",
|
||||||
|
Destination: &conf.Ilm.DeleteMinAge,
|
||||||
|
},
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: "delete-searchable-snapshots",
|
||||||
|
Usage: "delete phase: delete searchable snapshots",
|
||||||
|
Destination: &conf.Ilm.DeleteSearchableSnapshots,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||||
|
policy := cmd.Args().Get(0)
|
||||||
|
if policy == "" {
|
||||||
|
return errors.New("no policy name specified")
|
||||||
|
}
|
||||||
|
|
||||||
|
return es.IlmCreate(conf, cmd.Args().Get(0))
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
84
cmd/index.go
84
cmd/index.go
@@ -41,8 +41,12 @@ func Index(conf *cfg.Config) *cli.Command {
|
|||||||
IndexClose(conf),
|
IndexClose(conf),
|
||||||
IndexAllocation(conf),
|
IndexAllocation(conf),
|
||||||
IndexModify(conf),
|
IndexModify(conf),
|
||||||
IndexAlias(conf),
|
|
||||||
IndexFields(conf),
|
IndexFields(conf),
|
||||||
|
IndexIlm(conf),
|
||||||
|
|
||||||
|
// sub commands
|
||||||
|
IndexAlias(conf),
|
||||||
|
IndexTemplate(conf),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -133,22 +137,8 @@ func IndexShow(conf *cfg.Config) *cli.Command {
|
|||||||
return es.IndexShow(conf, cmd.Args().Get(0))
|
return es.IndexShow(conf, cmd.Args().Get(0))
|
||||||
},
|
},
|
||||||
|
|
||||||
// FIXME: doesn't work at all
|
|
||||||
// FIXME: also it would ONLY work if the user uses env vars, -C would not be
|
|
||||||
// there when the completion output is being generated
|
|
||||||
ShellComplete: func(ctx context.Context, cmd *cli.Command) {
|
ShellComplete: func(ctx context.Context, cmd *cli.Command) {
|
||||||
if cmd.NArg() > 0 {
|
complete(cmd, Cindex)
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
indices, err := es.IndexNames(conf)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, index := range indices {
|
|
||||||
fmt.Println(index)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -179,6 +169,7 @@ Valid field mapping types: integer, text, date, keyword`,
|
|||||||
Usage: "number of replicas to create",
|
Usage: "number of replicas to create",
|
||||||
Destination: &conf.Replicas,
|
Destination: &conf.Replicas,
|
||||||
Aliases: []string{"r"},
|
Aliases: []string{"r"},
|
||||||
|
Value: 1,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -200,6 +191,11 @@ func IndexDelete(conf *cfg.Config) *cli.Command {
|
|||||||
Name: "delete",
|
Name: "delete",
|
||||||
Aliases: []string{"rm"},
|
Aliases: []string{"rm"},
|
||||||
Usage: "delete an index",
|
Usage: "delete an index",
|
||||||
|
UsageText: "delete <index>",
|
||||||
|
|
||||||
|
ShellComplete: func(ctx context.Context, cmd *cli.Command) {
|
||||||
|
complete(cmd, Cindex)
|
||||||
|
},
|
||||||
|
|
||||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||||
index := cmd.Args().Get(0)
|
index := cmd.Args().Get(0)
|
||||||
@@ -216,6 +212,11 @@ func IndexClose(conf *cfg.Config) *cli.Command {
|
|||||||
return &cli.Command{
|
return &cli.Command{
|
||||||
Name: "close",
|
Name: "close",
|
||||||
Usage: "close an index",
|
Usage: "close an index",
|
||||||
|
UsageText: "close <index>",
|
||||||
|
|
||||||
|
ShellComplete: func(ctx context.Context, cmd *cli.Command) {
|
||||||
|
complete(cmd, Cindex)
|
||||||
|
},
|
||||||
|
|
||||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||||
index := cmd.Args().Get(0)
|
index := cmd.Args().Get(0)
|
||||||
@@ -234,12 +235,17 @@ func IndexModify(conf *cfg.Config) *cli.Command {
|
|||||||
Usage: "modify an index",
|
Usage: "modify an index",
|
||||||
UsageText: "modify <index[,index,...]|_all>",
|
UsageText: "modify <index[,index,...]|_all>",
|
||||||
|
|
||||||
|
ShellComplete: func(ctx context.Context, cmd *cli.Command) {
|
||||||
|
complete(cmd, Cindex)
|
||||||
|
},
|
||||||
|
|
||||||
Flags: []cli.Flag{
|
Flags: []cli.Flag{
|
||||||
&cli.IntFlag{
|
&cli.IntFlag{
|
||||||
Name: "replicas",
|
Name: "replicas",
|
||||||
Usage: "number of replicas",
|
Usage: "number of replicas",
|
||||||
Destination: &conf.Replicas,
|
Destination: &conf.Replicas,
|
||||||
Aliases: []string{"r"},
|
Aliases: []string{"r"},
|
||||||
|
Value: 1,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -260,6 +266,31 @@ func IndexFields(conf *cfg.Config) *cli.Command {
|
|||||||
Usage: "show info about field capabilities",
|
Usage: "show info about field capabilities",
|
||||||
UsageText: "index fields <index>",
|
UsageText: "index fields <index>",
|
||||||
|
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: "aggretable",
|
||||||
|
Usage: "include only aggretable fields",
|
||||||
|
Destination: &conf.Aggretable,
|
||||||
|
Aliases: []string{"a"},
|
||||||
|
},
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: "searchable",
|
||||||
|
Usage: "include only searchable fields",
|
||||||
|
Destination: &conf.Searchable,
|
||||||
|
Aliases: []string{"s"},
|
||||||
|
},
|
||||||
|
&cli.StringSliceFlag{
|
||||||
|
Name: "type",
|
||||||
|
Usage: "show only fields of this type",
|
||||||
|
Destination: &conf.Filter,
|
||||||
|
Aliases: []string{"t"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
ShellComplete: func(ctx context.Context, cmd *cli.Command) {
|
||||||
|
complete(cmd, Cindex)
|
||||||
|
},
|
||||||
|
|
||||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||||
index := cmd.Args().Get(0)
|
index := cmd.Args().Get(0)
|
||||||
if index == "" {
|
if index == "" {
|
||||||
@@ -270,3 +301,24 @@ func IndexFields(conf *cfg.Config) *cli.Command {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func IndexIlm(conf *cfg.Config) *cli.Command {
|
||||||
|
return &cli.Command{
|
||||||
|
Name: "ilm",
|
||||||
|
Usage: "show ilm status",
|
||||||
|
UsageText: "index ilm <index>",
|
||||||
|
|
||||||
|
ShellComplete: func(ctx context.Context, cmd *cli.Command) {
|
||||||
|
complete(cmd, Cindex)
|
||||||
|
},
|
||||||
|
|
||||||
|
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||||
|
index := cmd.Args().Get(0)
|
||||||
|
if index == "" {
|
||||||
|
return errors.New("no index specified")
|
||||||
|
}
|
||||||
|
|
||||||
|
return es.IlmExplain(conf, index)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -36,7 +36,8 @@ func IndexAlias(conf *cfg.Config) *cli.Command {
|
|||||||
IndexAliasCreate(conf),
|
IndexAliasCreate(conf),
|
||||||
IndexAliasList(conf),
|
IndexAliasList(conf),
|
||||||
IndexAliasDelete(conf),
|
IndexAliasDelete(conf),
|
||||||
//IndexAliasShow(conf),
|
IndexAliasRollover(conf),
|
||||||
|
// FIXME: implement IndexAliasAdd
|
||||||
//IndexAliasAdd(conf), // see https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-indices-update-aliases
|
//IndexAliasAdd(conf), // see https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-indices-update-aliases
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -49,6 +50,10 @@ func IndexAliasCreate(conf *cfg.Config) *cli.Command {
|
|||||||
Usage: "create an index alias",
|
Usage: "create an index alias",
|
||||||
UsageText: "create <index> <alias>",
|
UsageText: "create <index> <alias>",
|
||||||
|
|
||||||
|
ShellComplete: func(ctx context.Context, cmd *cli.Command) {
|
||||||
|
complete(cmd, Cindex)
|
||||||
|
},
|
||||||
|
|
||||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||||
index := cmd.Args().Get(0)
|
index := cmd.Args().Get(0)
|
||||||
alias := cmd.Args().Get(1)
|
alias := cmd.Args().Get(1)
|
||||||
@@ -69,6 +74,10 @@ func IndexAliasDelete(conf *cfg.Config) *cli.Command {
|
|||||||
Usage: "delete an index alias",
|
Usage: "delete an index alias",
|
||||||
UsageText: "delete <index> <alias>",
|
UsageText: "delete <index> <alias>",
|
||||||
|
|
||||||
|
ShellComplete: func(ctx context.Context, cmd *cli.Command) {
|
||||||
|
complete(cmd, Cindex)
|
||||||
|
},
|
||||||
|
|
||||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||||
index := cmd.Args().Get(0)
|
index := cmd.Args().Get(0)
|
||||||
alias := cmd.Args().Get(1)
|
alias := cmd.Args().Get(1)
|
||||||
@@ -82,6 +91,64 @@ func IndexAliasDelete(conf *cfg.Config) *cli.Command {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func IndexAliasRollover(conf *cfg.Config) *cli.Command {
|
||||||
|
return &cli.Command{
|
||||||
|
Name: "rollover",
|
||||||
|
Aliases: []string{"roll"},
|
||||||
|
Usage: "roll over an index alias",
|
||||||
|
UsageText: "rollover <alias>",
|
||||||
|
|
||||||
|
ShellComplete: func(ctx context.Context, cmd *cli.Command) {
|
||||||
|
complete(cmd, Cindex)
|
||||||
|
},
|
||||||
|
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: "wait",
|
||||||
|
Usage: "wait for active shards",
|
||||||
|
Destination: &conf.Wait,
|
||||||
|
Aliases: []string{"w"},
|
||||||
|
},
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: "dry-run",
|
||||||
|
Usage: "checks conditions but does not perform a rollover",
|
||||||
|
Destination: &conf.Wait,
|
||||||
|
Aliases: []string{"n"},
|
||||||
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "max-age",
|
||||||
|
Usage: "roll over after max age (eg: 7d, 2m, 8h)",
|
||||||
|
Destination: &conf.MaxAge,
|
||||||
|
},
|
||||||
|
&cli.IntFlag{
|
||||||
|
Name: "max-docs",
|
||||||
|
Usage: "roll over after max docs",
|
||||||
|
Destination: &conf.MaxDocs,
|
||||||
|
},
|
||||||
|
&cli.IntFlag{
|
||||||
|
Name: "max-primary-shard-size",
|
||||||
|
Usage: "roll over when primary shard size reaches size",
|
||||||
|
Destination: &conf.MaxShardSize,
|
||||||
|
},
|
||||||
|
&cli.IntFlag{
|
||||||
|
Name: "max-primary-shard-docs",
|
||||||
|
Usage: "roll over after max docs in primary shard reached",
|
||||||
|
Destination: &conf.MaxShardDocs,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||||
|
alias := cmd.Args().Get(0)
|
||||||
|
|
||||||
|
if alias == "" {
|
||||||
|
return errors.New("no alias specified")
|
||||||
|
}
|
||||||
|
|
||||||
|
return es.IndexAliasRollover(conf, alias)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func IndexAliasList(conf *cfg.Config) *cli.Command {
|
func IndexAliasList(conf *cfg.Config) *cli.Command {
|
||||||
return &cli.Command{
|
return &cli.Command{
|
||||||
Name: "list",
|
Name: "list",
|
||||||
|
|||||||
216
cmd/index_template.go
Normal file
216
cmd/index_template.go
Normal file
@@ -0,0 +1,216 @@
|
|||||||
|
/*
|
||||||
|
Copyright © 2026 Thomas von Dein
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"slices"
|
||||||
|
|
||||||
|
"codeberg.org/scip/esctl/pkg/cfg"
|
||||||
|
"codeberg.org/scip/esctl/pkg/es"
|
||||||
|
|
||||||
|
"github.com/urfave/cli/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
func IndexTemplate(conf *cfg.Config) *cli.Command {
|
||||||
|
return &cli.Command{
|
||||||
|
Name: "template",
|
||||||
|
Aliases: []string{"tpl"},
|
||||||
|
Usage: "manage index templates",
|
||||||
|
|
||||||
|
Commands: []*cli.Command{
|
||||||
|
IndexTemplateList(conf),
|
||||||
|
IndexTemplateShow(conf),
|
||||||
|
IndexTemplateCreate(conf, false),
|
||||||
|
IndexTemplateCreate(conf, true),
|
||||||
|
IndexTemplateDelete(conf),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func IndexTemplateList(conf *cfg.Config) *cli.Command {
|
||||||
|
return &cli.Command{
|
||||||
|
Name: "list",
|
||||||
|
Aliases: []string{"ls"},
|
||||||
|
Usage: "list index templates",
|
||||||
|
|
||||||
|
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||||
|
return es.IndexTemplateList(conf)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func IndexTemplateShow(conf *cfg.Config) *cli.Command {
|
||||||
|
return &cli.Command{
|
||||||
|
Name: "show",
|
||||||
|
Aliases: []string{"sh"},
|
||||||
|
Usage: "show details about an index template",
|
||||||
|
|
||||||
|
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||||
|
index := cmd.Args().Get(0)
|
||||||
|
if index == "" {
|
||||||
|
return errors.New("no index template specified")
|
||||||
|
}
|
||||||
|
|
||||||
|
return es.IndexTemplateShow(conf, cmd.Args().Get(0))
|
||||||
|
},
|
||||||
|
|
||||||
|
ShellComplete: func(ctx context.Context, cmd *cli.Command) {
|
||||||
|
complete(cmd, Cindex)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func IndexTemplateCreate(conf *cfg.Config, modify bool) *cli.Command {
|
||||||
|
name := "create"
|
||||||
|
alias := "+"
|
||||||
|
required := true
|
||||||
|
|
||||||
|
if modify {
|
||||||
|
name = "modify"
|
||||||
|
alias = "mod"
|
||||||
|
required = false
|
||||||
|
}
|
||||||
|
|
||||||
|
return &cli.Command{
|
||||||
|
Name: name,
|
||||||
|
Aliases: []string{alias},
|
||||||
|
Usage: name + " a new index template",
|
||||||
|
UsageText: name + ` <name> <fieldmapping:type>...`,
|
||||||
|
CustomHelpTemplate: addReference(
|
||||||
|
`Valid field mapping types: integer, text, date, keyword
|
||||||
|
|
||||||
|
Doc for index settings:
|
||||||
|
https://www.elastic.co/docs/reference/elasticsearch/index-settings
|
||||||
|
`),
|
||||||
|
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: "data-stream",
|
||||||
|
Usage: "template creates a data stream",
|
||||||
|
Destination: &conf.Stream,
|
||||||
|
Aliases: []string{"S"},
|
||||||
|
},
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: "allow-auto-create",
|
||||||
|
Usage: "automatically create indices",
|
||||||
|
Destination: &conf.AutoCreate,
|
||||||
|
Aliases: []string{"a"},
|
||||||
|
},
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: "rollover",
|
||||||
|
Usage: "automatically rollover associated aliases",
|
||||||
|
Destination: &conf.Rollover,
|
||||||
|
Aliases: []string{"R"},
|
||||||
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "mode",
|
||||||
|
Usage: "index mode, one of: standard, timeseries, logsdb or lookup",
|
||||||
|
Destination: &conf.Mode,
|
||||||
|
Aliases: []string{"m"},
|
||||||
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "retention",
|
||||||
|
Usage: "data retention, e.g. -1 (infinitely), 30d, 3m or 1y (data stream only)",
|
||||||
|
Destination: &conf.Retention,
|
||||||
|
Aliases: []string{"r"},
|
||||||
|
Required: required,
|
||||||
|
},
|
||||||
|
&cli.IntFlag{
|
||||||
|
Name: "priority",
|
||||||
|
Usage: "index template priority",
|
||||||
|
Destination: &conf.Priority,
|
||||||
|
Aliases: []string{"p"},
|
||||||
|
},
|
||||||
|
&cli.StringSliceFlag{
|
||||||
|
Name: "index-pattern",
|
||||||
|
Usage: "index matching pattern",
|
||||||
|
Destination: &conf.Patterns,
|
||||||
|
Aliases: []string{"i"},
|
||||||
|
Required: required,
|
||||||
|
},
|
||||||
|
&cli.StringSliceFlag{
|
||||||
|
Name: "settings",
|
||||||
|
Usage: "individual template settings, format: name:value, multiple possible",
|
||||||
|
Destination: &conf.Settings,
|
||||||
|
Aliases: []string{"s"},
|
||||||
|
},
|
||||||
|
&cli.StringSliceFlag{
|
||||||
|
Name: "component",
|
||||||
|
Usage: "include component template, multiple possible",
|
||||||
|
Destination: &conf.Components,
|
||||||
|
Aliases: []string{"c"},
|
||||||
|
},
|
||||||
|
&cli.StringSliceFlag{
|
||||||
|
Name: "meta",
|
||||||
|
Usage: "custom meta field, format: name:value, multiple possible",
|
||||||
|
Destination: &conf.Meta,
|
||||||
|
Aliases: []string{"M"},
|
||||||
|
},
|
||||||
|
&cli.StringSliceFlag{
|
||||||
|
Name: "aliases",
|
||||||
|
Usage: "set up aliases to associate with indices",
|
||||||
|
Destination: &conf.Aliases,
|
||||||
|
Aliases: []string{"A"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||||
|
args := cmd.Args()
|
||||||
|
if args.Len() == 0 {
|
||||||
|
return fmt.Errorf("no name specified")
|
||||||
|
}
|
||||||
|
|
||||||
|
if conf.Mode != "" {
|
||||||
|
if !slices.Contains([]string{"standard", "timeseries", "logsdb", "lookup"}, conf.Mode) {
|
||||||
|
return errors.New("mode must be one of: standard, timeseries, logsdb or lookup")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mappings := args.Slice()[1:]
|
||||||
|
|
||||||
|
if modify {
|
||||||
|
return es.IndexTemplateModify(conf, args.Get(0), mappings)
|
||||||
|
}
|
||||||
|
|
||||||
|
return es.IndexTemplateCreate(conf, args.Get(0), mappings)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func IndexTemplateDelete(conf *cfg.Config) *cli.Command {
|
||||||
|
return &cli.Command{
|
||||||
|
Name: "delete",
|
||||||
|
Aliases: []string{"rm"},
|
||||||
|
Usage: "delete an index template",
|
||||||
|
|
||||||
|
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||||
|
index := cmd.Args().Get(0)
|
||||||
|
if index == "" {
|
||||||
|
return errors.New("no index template specified")
|
||||||
|
}
|
||||||
|
|
||||||
|
return es.IndexTemplateDelete(conf, cmd.Args().Get(0))
|
||||||
|
},
|
||||||
|
|
||||||
|
ShellComplete: func(ctx context.Context, cmd *cli.Command) {
|
||||||
|
complete(cmd, Cindex)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -58,6 +58,7 @@ func NodeShow(conf *cfg.Config) *cli.Command {
|
|||||||
UsageText: "show [options] <node>",
|
UsageText: "show [options] <node>",
|
||||||
|
|
||||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||||
|
// FIXME: implement es.NodeShow()
|
||||||
// return es.NodeShow(conf, cmd.Args().Get(0))
|
// return es.NodeShow(conf, cmd.Args().Get(0))
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
|
|||||||
21
cmd/repl.go
21
cmd/repl.go
@@ -15,24 +15,3 @@ You should have received a copy of the GNU General Public License
|
|||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"codeberg.org/scip/esctl/pkg/cfg"
|
|
||||||
"codeberg.org/scip/esctl/pkg/es"
|
|
||||||
|
|
||||||
"github.com/urfave/cli/v3"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Repl(conf *cfg.Config) *cli.Command {
|
|
||||||
return &cli.Command{
|
|
||||||
Name: "repl",
|
|
||||||
Aliases: []string{"shell"},
|
|
||||||
Usage: "interactive API repl",
|
|
||||||
|
|
||||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
|
||||||
return es.Repl(conf)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
63
cmd/roles.go
63
cmd/roles.go
@@ -34,6 +34,7 @@ func Roles(conf *cfg.Config) *cli.Command {
|
|||||||
Commands: []*cli.Command{
|
Commands: []*cli.Command{
|
||||||
RoleList(conf),
|
RoleList(conf),
|
||||||
RoleShow(conf),
|
RoleShow(conf),
|
||||||
|
RoleDiff(conf),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -66,6 +67,10 @@ func RoleShow(conf *cfg.Config) *cli.Command {
|
|||||||
Usage: "show details about a role",
|
Usage: "show details about a role",
|
||||||
UsageText: "show [options] <role>",
|
UsageText: "show [options] <role>",
|
||||||
|
|
||||||
|
ShellComplete: func(ctx context.Context, cmd *cli.Command) {
|
||||||
|
complete(cmd, Crole)
|
||||||
|
},
|
||||||
|
|
||||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||||
index := cmd.Args().Get(0)
|
index := cmd.Args().Get(0)
|
||||||
if index == "" {
|
if index == "" {
|
||||||
@@ -76,3 +81,61 @@ func RoleShow(conf *cfg.Config) *cli.Command {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func RoleDiff(conf *cfg.Config) *cli.Command {
|
||||||
|
return &cli.Command{
|
||||||
|
Name: "diff",
|
||||||
|
Usage: "show differences between roles and CSV baseline",
|
||||||
|
UsageText: `diff [options] <file.csv> [<role>]
|
||||||
|
CSV format:
|
||||||
|
index_name;role;index_privilege;cluster_privilege;ad_group;space;retention;kibana_privilege;field_privilege`,
|
||||||
|
|
||||||
|
MutuallyExclusiveFlags: []cli.MutuallyExclusiveFlags{{
|
||||||
|
Flags: [][]cli.Flag{
|
||||||
|
{
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: "not-deployed",
|
||||||
|
Usage: "include only not deployed but defined roles",
|
||||||
|
Destination: &conf.NotDeployed,
|
||||||
|
Aliases: []string{"n"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: "undefined",
|
||||||
|
Usage: "include only deployed but undefined roles",
|
||||||
|
Destination: &conf.Undefined,
|
||||||
|
Aliases: []string{"u"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: "diff",
|
||||||
|
Usage: "include only differing roles",
|
||||||
|
Destination: &conf.Diff,
|
||||||
|
Aliases: []string{"D"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "separator",
|
||||||
|
Usage: "CSV field separator",
|
||||||
|
Destination: &conf.Separator,
|
||||||
|
Aliases: []string{"s"},
|
||||||
|
Value: ",",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||||
|
csvfile := cmd.Args().Get(0)
|
||||||
|
if csvfile == "" {
|
||||||
|
return errors.New("no CSV file specified")
|
||||||
|
}
|
||||||
|
|
||||||
|
return es.RoleDiff(conf, cmd.Args().Get(0), cmd.Args().Get(1))
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
87
cmd/root.go
87
cmd/root.go
@@ -19,7 +19,9 @@ package cmd
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
golog "log"
|
||||||
"os"
|
"os"
|
||||||
|
"runtime/pprof"
|
||||||
|
|
||||||
"codeberg.org/scip/esctl/pkg/cfg"
|
"codeberg.org/scip/esctl/pkg/cfg"
|
||||||
"codeberg.org/scip/esctl/pkg/es"
|
"codeberg.org/scip/esctl/pkg/es"
|
||||||
@@ -83,6 +85,12 @@ func Main() int {
|
|||||||
Usage: "output mode (tsv, markdown, json, yaml) default: tsv",
|
Usage: "output mode (tsv, markdown, json, yaml) default: tsv",
|
||||||
Destination: &conf.Output,
|
Destination: &conf.Output,
|
||||||
},
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "profile-file",
|
||||||
|
Usage: "golang profiler output file",
|
||||||
|
Destination: &conf.ProfileFile,
|
||||||
|
Hidden: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
Commands: []*cli.Command{
|
Commands: []*cli.Command{
|
||||||
@@ -90,14 +98,18 @@ func Main() int {
|
|||||||
Cluster(conf),
|
Cluster(conf),
|
||||||
Ccr(conf),
|
Ccr(conf),
|
||||||
Index(conf),
|
Index(conf),
|
||||||
|
Ilm(conf),
|
||||||
|
Datastream(conf),
|
||||||
Shard(conf),
|
Shard(conf),
|
||||||
Snapshot(conf),
|
Snapshot(conf),
|
||||||
Node(conf),
|
Node(conf),
|
||||||
Doc(conf),
|
Doc(conf),
|
||||||
Repl(conf),
|
|
||||||
Version(conf),
|
Version(conf),
|
||||||
Debug(conf),
|
Debug(conf),
|
||||||
Roles(conf),
|
Roles(conf),
|
||||||
|
Task(conf),
|
||||||
|
HelpJsonPath(conf),
|
||||||
|
Api(conf),
|
||||||
},
|
},
|
||||||
|
|
||||||
Before: func(ctx context.Context, cmd *cli.Command) (context.Context, error) {
|
Before: func(ctx context.Context, cmd *cli.Command) (context.Context, error) {
|
||||||
@@ -112,6 +124,26 @@ func Main() int {
|
|||||||
|
|
||||||
log.Init(conf)
|
log.Init(conf)
|
||||||
|
|
||||||
|
if conf.ProfileFile != "" {
|
||||||
|
// enable cpu profiling. Do NOT use q to stop the game but
|
||||||
|
// close the window to get a profile
|
||||||
|
fd, err := os.Create(conf.ProfileFile)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if err := fd.Close(); err != nil {
|
||||||
|
golog.Fatal(err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
if err := pprof.StartCPUProfile(fd); err != nil {
|
||||||
|
golog.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer pprof.StopCPUProfile()
|
||||||
|
}
|
||||||
|
|
||||||
return nil, nil
|
return nil, nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -119,6 +151,59 @@ func Main() int {
|
|||||||
return Finish(cmd.Run(context.Background(), os.Args))
|
return Finish(cmd.Run(context.Background(), os.Args))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func HelpJsonPath(conf *cfg.Config) *cli.Command {
|
||||||
|
msg := `jsonPath usage:
|
||||||
|
|
||||||
|
name.last >> "Anderson"
|
||||||
|
age >> 37
|
||||||
|
children >> ["Sara","Alex","Jack"]
|
||||||
|
children.# >> 3
|
||||||
|
children.1 >> "Alex"
|
||||||
|
child*.2 >> "Jack"
|
||||||
|
c?ildren.0 >> "Sara"
|
||||||
|
fav\.movie >> "Deer Hunter"
|
||||||
|
friends.#.first >> ["Dale","Roger","Jane"]
|
||||||
|
friends.1.last >> "Craig"
|
||||||
|
|
||||||
|
You can also query an array for the first match by using #(...), or
|
||||||
|
find all matches with #(...)#. Queries support the ==, !=, <, <=, >,
|
||||||
|
>= comparison operators and the simple pattern matching % (like) and
|
||||||
|
!% (not like) operators. Eg:
|
||||||
|
|
||||||
|
friends.#(last=="Murphy").first >> "Dale"
|
||||||
|
friends.#(last=="Murphy")#.first >> ["Dale","Jane"]
|
||||||
|
friends.#(age>45)#.last >> ["Craig","Murphy"]
|
||||||
|
friends.#(first%"D*").last >> "Murphy"
|
||||||
|
friends.#(first!%"D*").last >> "Craig"
|
||||||
|
friends.#(nets.#(=="fb"))#.first >> ["Dale","Roger"]
|
||||||
|
|
||||||
|
To extract more than one field, use:
|
||||||
|
|
||||||
|
{"ns":source.namespace_name,"time":source.@timestamp} >>
|
||||||
|
|
||||||
|
{
|
||||||
|
"ns": "3f80316965c64405-275a75782e984bcc8d82",
|
||||||
|
"time": "2026-06-10T04:23:38.914080361+00:00"
|
||||||
|
}
|
||||||
|
{
|
||||||
|
"ns": "3f80316965c64405-f0e1dcef668e45bb9afd",
|
||||||
|
"time": "2026-06-10T09:39:00.554892542+00:00"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Documentation: https://github.com/tidwall/gjson/blob/master/SYNTAX.md`
|
||||||
|
|
||||||
|
return &cli.Command{
|
||||||
|
Name: "help-jsonpath",
|
||||||
|
Usage: "show jsonpath help",
|
||||||
|
|
||||||
|
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||||
|
_, err := fmt.Println(msg)
|
||||||
|
return err
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func Version(conf *cfg.Config) *cli.Command {
|
func Version(conf *cfg.Config) *cli.Command {
|
||||||
return &cli.Command{
|
return &cli.Command{
|
||||||
Name: "version",
|
Name: "version",
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ package cmd
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"codeberg.org/scip/esctl/pkg/cfg"
|
"codeberg.org/scip/esctl/pkg/cfg"
|
||||||
"codeberg.org/scip/esctl/pkg/es"
|
"codeberg.org/scip/esctl/pkg/es"
|
||||||
@@ -26,7 +25,7 @@ import (
|
|||||||
"github.com/urfave/cli/v3"
|
"github.com/urfave/cli/v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
const SearchUsage = `<sep> might be one of:
|
const SearchUsage = `field <sep> might be one of:
|
||||||
=: Must match
|
=: Must match
|
||||||
!=: Must not match
|
!=: Must not match
|
||||||
|
|
||||||
@@ -44,15 +43,15 @@ For datetime range format refer to:
|
|||||||
https://www.elastic.co/docs/reference/elasticsearch/rest-apis/common-options#date-math
|
https://www.elastic.co/docs/reference/elasticsearch/rest-apis/common-options#date-math
|
||||||
|
|
||||||
For timestamp formats refer to:
|
For timestamp formats refer to:
|
||||||
https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/mapping-date-format
|
https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/mapping-date-format`
|
||||||
`
|
|
||||||
|
|
||||||
func Search(conf *cfg.Config) *cli.Command {
|
func Search(conf *cfg.Config) *cli.Command {
|
||||||
return &cli.Command{
|
return &cli.Command{
|
||||||
Name: "search",
|
Name: "search",
|
||||||
Aliases: []string{"/"},
|
Aliases: []string{"/"},
|
||||||
Usage: "search within an index",
|
Usage: "search within an index",
|
||||||
UsageText: "search [options] [<[field<sep>]pattern> ...]\n" + SearchUsage,
|
UsageText: "search [options] [<[field<sep>]pattern> ...]\n",
|
||||||
|
CustomHelpTemplate: addReference(SearchUsage),
|
||||||
|
|
||||||
Flags: []cli.Flag{
|
Flags: []cli.Flag{
|
||||||
&cli.StringFlag{
|
&cli.StringFlag{
|
||||||
@@ -113,12 +112,6 @@ func Search(conf *cfg.Config) *cli.Command {
|
|||||||
Destination: &conf.Ascending,
|
Destination: &conf.Ascending,
|
||||||
Aliases: []string{"a"},
|
Aliases: []string{"a"},
|
||||||
},
|
},
|
||||||
&cli.BoolFlag{
|
|
||||||
Name: "help-jsonpath",
|
|
||||||
Usage: "show jsonPath help",
|
|
||||||
Destination: &conf.Subhelp,
|
|
||||||
Aliases: []string{"H"},
|
|
||||||
},
|
|
||||||
&cli.BoolFlag{
|
&cli.BoolFlag{
|
||||||
Name: "tail",
|
Name: "tail",
|
||||||
Usage: "follow search live, like tail -f",
|
Usage: "follow search live, like tail -f",
|
||||||
@@ -131,13 +124,21 @@ func Search(conf *cfg.Config) *cli.Command {
|
|||||||
Destination: &conf.Or,
|
Destination: &conf.Or,
|
||||||
Aliases: []string{"O"},
|
Aliases: []string{"O"},
|
||||||
},
|
},
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: "validate",
|
||||||
|
Usage: "validate search query",
|
||||||
|
Destination: &conf.Validate,
|
||||||
|
Aliases: []string{"v"},
|
||||||
|
},
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: "explain",
|
||||||
|
Usage: "explain search query",
|
||||||
|
Destination: &conf.Explain,
|
||||||
|
Aliases: []string{"e"},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||||
if conf.Subhelp {
|
|
||||||
return showJsonPathHelp()
|
|
||||||
}
|
|
||||||
|
|
||||||
args := cmd.Args()
|
args := cmd.Args()
|
||||||
|
|
||||||
if conf.To == -1 {
|
if conf.To == -1 {
|
||||||
@@ -148,34 +149,3 @@ func Search(conf *cfg.Config) *cli.Command {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func showJsonPathHelp() error {
|
|
||||||
_, err := fmt.Println(`jsonPath usage:
|
|
||||||
|
|
||||||
name.last >> "Anderson"
|
|
||||||
age >> 37
|
|
||||||
children >> ["Sara","Alex","Jack"]
|
|
||||||
children.# >> 3
|
|
||||||
children.1 >> "Alex"
|
|
||||||
child*.2 >> "Jack"
|
|
||||||
c?ildren.0 >> "Sara"
|
|
||||||
fav\.movie >> "Deer Hunter"
|
|
||||||
friends.#.first >> ["Dale","Roger","Jane"]
|
|
||||||
friends.1.last >> "Craig"
|
|
||||||
|
|
||||||
You can also query an array for the first match by using #(...), or
|
|
||||||
find all matches with #(...)#. Queries support the ==, !=, <, <=, >,
|
|
||||||
>= comparison operators and the simple pattern matching % (like) and
|
|
||||||
!% (not like) operators. Eg:
|
|
||||||
|
|
||||||
friends.#(last=="Murphy").first >> "Dale"
|
|
||||||
friends.#(last=="Murphy")#.first >> ["Dale","Jane"]
|
|
||||||
friends.#(age>45)#.last >> ["Craig","Murphy"]
|
|
||||||
friends.#(first%"D*").last >> "Murphy"
|
|
||||||
friends.#(first!%"D*").last >> "Craig"
|
|
||||||
friends.#(nets.#(=="fb"))#.first >> ["Dale","Roger"]
|
|
||||||
|
|
||||||
Documentation: https://github.com/tidwall/gjson/blob/master/SYNTAX.md`)
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|||||||
69
cmd/tasks.go
Normal file
69
cmd/tasks.go
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
/*
|
||||||
|
Copyright © 2026 Thomas von Dein
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"codeberg.org/scip/esctl/pkg/cfg"
|
||||||
|
"codeberg.org/scip/esctl/pkg/es"
|
||||||
|
|
||||||
|
"github.com/urfave/cli/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Task(conf *cfg.Config) *cli.Command {
|
||||||
|
return &cli.Command{
|
||||||
|
Name: "task",
|
||||||
|
Usage: "manage tasks",
|
||||||
|
|
||||||
|
Commands: []*cli.Command{
|
||||||
|
TaskList(conf),
|
||||||
|
TaskCancel(conf),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TaskList(conf *cfg.Config) *cli.Command {
|
||||||
|
return &cli.Command{
|
||||||
|
Name: "list",
|
||||||
|
Aliases: []string{"ls"},
|
||||||
|
Usage: "list tasks",
|
||||||
|
|
||||||
|
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||||
|
return es.TaskList(conf)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TaskCancel(conf *cfg.Config) *cli.Command {
|
||||||
|
return &cli.Command{
|
||||||
|
Name: "cancel",
|
||||||
|
Aliases: []string{"rm"},
|
||||||
|
Usage: "cancel running task",
|
||||||
|
UsageText: "cancel <taskId>",
|
||||||
|
|
||||||
|
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||||
|
taskId := cmd.Args().Get(0)
|
||||||
|
if taskId == "" {
|
||||||
|
return errors.New("no taskId specified")
|
||||||
|
}
|
||||||
|
|
||||||
|
return es.TaskCancel(conf, cmd.Args().Get(0))
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
35
cmd/utilities.go
Normal file
35
cmd/utilities.go
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
/*
|
||||||
|
Copyright © 2026 Thomas von Dein
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/urfave/cli/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
func addReference(ref string) string {
|
||||||
|
indentedRef := []string{}
|
||||||
|
|
||||||
|
for _, line := range strings.Split(ref, "\n") {
|
||||||
|
indentedRef = append(indentedRef, " "+line)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s\nREFERENCE:\n%s\n",
|
||||||
|
cli.SubcommandHelpTemplate,
|
||||||
|
strings.Join(indentedRef, "\n"))
|
||||||
|
}
|
||||||
61
go.mod
61
go.mod
@@ -14,42 +14,89 @@
|
|||||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
module codeberg.org/scip/esctl
|
module codeberg.org/scip/esctl
|
||||||
|
|
||||||
go 1.25.0
|
go 1.25.8
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
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/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
|
||||||
github.com/elastic/elastic-transport-go/v8 v8.11.0
|
github.com/elastic/elastic-transport-go/v8 v8.11.0
|
||||||
github.com/elastic/go-elasticsearch/v9 v9.3.4
|
github.com/elastic/go-elasticsearch/v9 v9.3.4
|
||||||
github.com/fatih/color v1.19.0
|
github.com/fatih/color v1.19.0
|
||||||
|
github.com/go-openapi/loads v0.24.0
|
||||||
|
github.com/go-openapi/spec v0.22.5
|
||||||
|
github.com/go-openapi/swag/loading v0.26.1
|
||||||
github.com/mattn/go-isatty v0.0.22
|
github.com/mattn/go-isatty v0.0.22
|
||||||
github.com/olekukonko/tablewriter v1.1.4
|
github.com/olekukonko/tablewriter v1.1.4
|
||||||
|
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.9.1-0.20260524212652-be8b79d0c8de
|
github.com/urfave/cli/v3 v3.9.1-0.20260524212652-be8b79d0c8de
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/MichaelMure/go-term-text v0.3.1 // indirect
|
||||||
|
github.com/alecthomas/chroma v0.7.1 // indirect
|
||||||
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
github.com/clipperhouse/displaywidth v0.10.0 // indirect
|
github.com/charmbracelet/bubbles v1.0.0 // indirect
|
||||||
github.com/clipperhouse/uax29/v2 v2.6.0 // indirect
|
github.com/charmbracelet/bubbletea v1.3.10 // indirect
|
||||||
|
github.com/charmbracelet/colorprofile v0.4.1 // indirect
|
||||||
|
github.com/charmbracelet/x/ansi v0.11.6 // indirect
|
||||||
|
github.com/charmbracelet/x/cellbuf v0.0.15 // indirect
|
||||||
|
github.com/charmbracelet/x/term v0.2.2 // indirect
|
||||||
|
github.com/clipperhouse/displaywidth v0.11.0 // indirect
|
||||||
|
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
|
||||||
|
github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964 // indirect
|
||||||
|
github.com/disintegration/imaging v1.6.2 // indirect
|
||||||
|
github.com/dlclark/regexp2 v1.11.0 // indirect
|
||||||
|
github.com/eliukblau/pixterm/pkg/ansimage v0.0.0-20191210081756-9fb6cf8c2f75 // indirect
|
||||||
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
||||||
github.com/go-logr/logr v1.4.3 // indirect
|
github.com/go-logr/logr v1.4.3 // indirect
|
||||||
github.com/go-logr/stdr v1.2.2 // indirect
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
|
github.com/go-openapi/analysis v0.25.2 // indirect
|
||||||
|
github.com/go-openapi/errors v0.22.8 // indirect
|
||||||
|
github.com/go-openapi/jsonpointer v0.23.1 // indirect
|
||||||
|
github.com/go-openapi/jsonreference v0.21.6 // indirect
|
||||||
|
github.com/go-openapi/strfmt v0.26.3 // indirect
|
||||||
|
github.com/go-openapi/swag/conv v0.26.1 // indirect
|
||||||
|
github.com/go-openapi/swag/jsonname v0.26.1 // indirect
|
||||||
|
github.com/go-openapi/swag/jsonutils v0.26.1 // indirect
|
||||||
|
github.com/go-openapi/swag/mangling v0.26.1 // indirect
|
||||||
|
github.com/go-openapi/swag/stringutils v0.26.1 // indirect
|
||||||
|
github.com/go-openapi/swag/typeutils v0.26.1 // indirect
|
||||||
|
github.com/go-openapi/swag/yamlutils v0.26.1 // indirect
|
||||||
|
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
|
||||||
github.com/goccy/go-json v0.10.6 // indirect
|
github.com/goccy/go-json v0.10.6 // indirect
|
||||||
github.com/lmittmann/tint v1.1.3 // indirect
|
github.com/gomarkdown/markdown v0.0.0-20191123064959-2c17d62f5098 // indirect
|
||||||
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
|
github.com/kyokomi/emoji/v2 v2.2.8 // indirect
|
||||||
|
github.com/lucasb-eyer/go-colorful v1.4.0 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||||
|
github.com/mattn/go-localereader v0.0.1 // indirect
|
||||||
github.com/mattn/go-runewidth v0.0.23 // indirect
|
github.com/mattn/go-runewidth v0.0.23 // indirect
|
||||||
|
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
|
||||||
|
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||||
|
github.com/muesli/termenv v0.16.0 // indirect
|
||||||
|
github.com/oklog/ulid/v2 v2.1.1 // indirect
|
||||||
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 // indirect
|
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 // indirect
|
||||||
github.com/olekukonko/errors v1.2.0 // indirect
|
github.com/olekukonko/errors v1.2.0 // indirect
|
||||||
github.com/olekukonko/ll v0.1.8 // indirect
|
github.com/olekukonko/ll v0.1.8 // indirect
|
||||||
github.com/tidwall/gjson v1.19.0 // indirect
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
github.com/tidwall/match v1.1.1 // indirect
|
github.com/tidwall/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
|
||||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||||
go.opentelemetry.io/otel v1.35.0 // indirect
|
go.opentelemetry.io/otel v1.35.0 // indirect
|
||||||
go.opentelemetry.io/otel/metric v1.35.0 // indirect
|
go.opentelemetry.io/otel/metric v1.35.0 // indirect
|
||||||
go.opentelemetry.io/otel/trace v1.35.0 // indirect
|
go.opentelemetry.io/otel/trace v1.35.0 // indirect
|
||||||
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f // indirect
|
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||||
golang.org/x/sys v0.42.0 // indirect
|
golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect
|
||||||
|
golang.org/x/image v0.0.0-20191206065243-da761ea9ff43 // indirect
|
||||||
|
golang.org/x/net v0.55.0 // indirect
|
||||||
|
golang.org/x/sys v0.46.0 // indirect
|
||||||
|
golang.org/x/term v0.44.0 // indirect
|
||||||
|
golang.org/x/text v0.37.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
213
go.sum
213
go.sum
@@ -1,29 +1,74 @@
|
|||||||
|
github.com/MichaelMure/go-term-markdown v0.1.4 h1:Ir3kBXDUtOX7dEv0EaQV8CNPpH+T7AfTh0eniMOtNcs=
|
||||||
|
github.com/MichaelMure/go-term-markdown v0.1.4/go.mod h1:EhcA3+pKYnlUsxYKBJ5Sn1cTQmmBMjeNlpV8nRb+JxA=
|
||||||
|
github.com/MichaelMure/go-term-text v0.3.1 h1:Kw9kZanyZWiCHOYu9v/8pWEgDQ6UVN9/ix2Vd2zzWf0=
|
||||||
|
github.com/MichaelMure/go-term-text v0.3.1/go.mod h1:QgVjAEDUnRMlzpS6ky5CGblux7ebeiLnuy9dAaFZu8o=
|
||||||
|
github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38 h1:smF2tmSOzy2Mm+0dGI2AIUHY+w0BUc+4tn40djz7+6U=
|
||||||
|
github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38/go.mod h1:r7bzyVFMNntcxPZXK3/+KdruV1H5KSlyVY0gc+NgInI=
|
||||||
|
github.com/alecthomas/chroma v0.7.1 h1:G1i02OhUbRi2nJxcNkwJaY/J1gHXj9tt72qN6ZouLFQ=
|
||||||
|
github.com/alecthomas/chroma v0.7.1/go.mod h1:gHw09mkX1Qp80JlYbmN9L3+4R5o6DJJ3GRShh+AICNc=
|
||||||
|
github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721 h1:JHZL0hZKJ1VENNfmXvHbgYlbUOvpzYzvy2aZU5gXVeo=
|
||||||
|
github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721/go.mod h1:QO9JBoKquHd+jz9nshCh40fOfO+JzsoXy8qTHF68zU0=
|
||||||
|
github.com/alecthomas/kong v0.2.1-0.20190708041108-0548c6b1afae/go.mod h1:+inYUSluD+p4L8KdviBSgzcqEjUQOfC5fQDRFuc36lI=
|
||||||
|
github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897/go.mod h1:xTS7Pm1pD1mvyM075QCDSRqH6qRLXylzS24ZTpRiSzQ=
|
||||||
github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs=
|
github.com/alecthomas/repr v0.5.2 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/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
|
github.com/charmbracelet/bubbles v1.0.0 h1:12J8/ak/uCZEMQ6KU7pcfwceyjLlWsDLAxB5fXonfvc=
|
||||||
|
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/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/go.mod h1:U1d9Dljmdf9DLegaJ0nGZNJvoXAhayhmidOdcBwAvKk=
|
||||||
|
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
|
||||||
|
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
|
||||||
|
github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE=
|
||||||
|
github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q=
|
||||||
|
github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8=
|
||||||
|
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/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/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
|
||||||
|
github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM=
|
||||||
github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ=
|
github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ=
|
||||||
github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI=
|
github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI=
|
||||||
github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk=
|
github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk=
|
||||||
|
github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04=
|
||||||
github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=
|
github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=
|
||||||
github.com/clipperhouse/displaywidth v0.10.0 h1:GhBG8WuerxjFQQYeuZAeVTuyxuX+UraiZGD4HJQ3Y8g=
|
github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8=
|
||||||
github.com/clipperhouse/displaywidth v0.10.0/go.mod h1:XqJajYsaiEwkxOj4bowCTMcT1SgvHo9flfF3jQasdbs=
|
github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0=
|
||||||
github.com/clipperhouse/uax29/v2 v2.6.0 h1:z0cDbUV+aPASdFb2/ndFnS9ts/WNXgTNNGFoKXuhpos=
|
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
|
||||||
github.com/clipperhouse/uax29/v2 v2.6.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
|
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
|
||||||
|
github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964 h1:y5HC9v93H5EPKqaS1UYVg1uYah5Xf51mBfIoWehClUQ=
|
||||||
|
github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964/go.mod h1:Xd9hchkHSWYkEqJwUGisez3G1QY8Ryz0sdWrLPMGjLk=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
|
||||||
|
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
|
||||||
|
github.com/dlclark/regexp2 v1.1.6/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
|
||||||
|
github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
|
||||||
|
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||||
github.com/elastic/elastic-transport-go/v8 v8.9.0 h1:KeT/2P54F0xS0S8Y3Pf+tFDg4HmBgReQMB+BMz8dDAs=
|
|
||||||
github.com/elastic/elastic-transport-go/v8 v8.9.0/go.mod h1:ssMTvNS2hwf7CaiGsRRsx4gQHFZ/jS/DkLcISxekWzc=
|
|
||||||
github.com/elastic/elastic-transport-go/v8 v8.11.0 h1:taYmqC2M6+fZt/+W+ENYh/W5L9+KrlJGOSbEJs8egWc=
|
github.com/elastic/elastic-transport-go/v8 v8.11.0 h1:taYmqC2M6+fZt/+W+ENYh/W5L9+KrlJGOSbEJs8egWc=
|
||||||
github.com/elastic/elastic-transport-go/v8 v8.11.0/go.mod h1:DZQ0szCNywc9F+C9l/Kkd4n69SvJVj0I3yK1Of7s3l8=
|
github.com/elastic/elastic-transport-go/v8 v8.11.0/go.mod h1:DZQ0szCNywc9F+C9l/Kkd4n69SvJVj0I3yK1Of7s3l8=
|
||||||
github.com/elastic/go-elasticsearch/v9 v9.3.2 h1:nvtvfN/Gsp/rzUPz/9yILwDAsYJ3s5L0VmhR16zPKCA=
|
|
||||||
github.com/elastic/go-elasticsearch/v9 v9.3.2/go.mod h1:ubKUMJCJbX5V/gW5MIn2NQZyaEZ61ubXwJmD5UMNrM8=
|
|
||||||
github.com/elastic/go-elasticsearch/v9 v9.3.4 h1:vnqXl6jnlA+ZwlfRaKF9BR9woZC1KSB17gtyvgf8zVU=
|
github.com/elastic/go-elasticsearch/v9 v9.3.4 h1:vnqXl6jnlA+ZwlfRaKF9BR9woZC1KSB17gtyvgf8zVU=
|
||||||
github.com/elastic/go-elasticsearch/v9 v9.3.4/go.mod h1:ubKUMJCJbX5V/gW5MIn2NQZyaEZ61ubXwJmD5UMNrM8=
|
github.com/elastic/go-elasticsearch/v9 v9.3.4/go.mod h1:ubKUMJCJbX5V/gW5MIn2NQZyaEZ61ubXwJmD5UMNrM8=
|
||||||
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
|
github.com/eliukblau/pixterm/pkg/ansimage v0.0.0-20191210081756-9fb6cf8c2f75 h1:vbix8DDQ/rfatfFr/8cf/sJfIL69i4BcZfjrVOxsMqk=
|
||||||
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
|
github.com/eliukblau/pixterm/pkg/ansimage v0.0.0-20191210081756-9fb6cf8c2f75/go.mod h1:0gZuvTO1ikSA5LtTI6E13LEOdWQNjIo5MTQOvrV0eFg=
|
||||||
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
|
||||||
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
|
||||||
|
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
|
||||||
github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w=
|
github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w=
|
||||||
github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE=
|
github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE=
|
||||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||||
@@ -31,33 +76,109 @@ github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
|||||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||||
|
github.com/go-openapi/analysis v0.25.2 h1:I0vy4n3alz+DHTiN1PRhCb7QZxkK6g5YmswZKv2TKuw=
|
||||||
|
github.com/go-openapi/analysis v0.25.2/go.mod h1:Uhs1t/2XR10EnwONYILGEzw8gcfGIG5Xk5K2AxnhqDo=
|
||||||
|
github.com/go-openapi/errors v0.22.8 h1:oP7sW7TWc3wFFjrzzj0nI83H2qMBkNjNfSd+XRejk/I=
|
||||||
|
github.com/go-openapi/errors v0.22.8/go.mod h1:BuUoHcYrU6E7V9gfj1I5wLQqgtIHnup/alXZ8KdgQ0w=
|
||||||
|
github.com/go-openapi/jsonpointer v0.23.1 h1:1HBACs7XIwR2RcmItfdSFlALhGbe6S92p0ry4d1GWg4=
|
||||||
|
github.com/go-openapi/jsonpointer v0.23.1/go.mod h1:iWRmZTrGn7XwYhtPt/fvdSFj1OfNBngqRT2UG3BxSqY=
|
||||||
|
github.com/go-openapi/jsonreference v0.21.6 h1:NZ5nGfnaM1n4I43Xjm1e5/M2GjOwQwndQz22uhxwD+Y=
|
||||||
|
github.com/go-openapi/jsonreference v0.21.6/go.mod h1:xzbgtQ3ZbWxvET3AxdzCJlJt6vkovbf+IfSPJjD0tUY=
|
||||||
|
github.com/go-openapi/loads v0.24.0 h1:4LLorXRPTzIN9V6ngMUZbAscsBOUBk3Oa8cClu/bFrQ=
|
||||||
|
github.com/go-openapi/loads v0.24.0/go.mod h1:xQMgX+hw5xRAhGrcDXxeMw78IFqUpIzhleu3HqPhyF4=
|
||||||
|
github.com/go-openapi/spec v0.22.5 h1:KhO7RBlKQfonUWX2WzQCoLIXVA6AcNqDGZ3a1Dutdlo=
|
||||||
|
github.com/go-openapi/spec v0.22.5/go.mod h1:vxpOtMya5TXtENXKE5bKqv5NjocVhyhxHrlZfvKnZ74=
|
||||||
|
github.com/go-openapi/strfmt v0.26.3 h1:rzmslHarJgBbf2qfGge+X3htclQfmXqBZMm0Too0HhU=
|
||||||
|
github.com/go-openapi/strfmt v0.26.3/go.mod h1:a5nsUw0oRpQzZeOwx8bi6cKbzFZslpbCKt1LEot+KnQ=
|
||||||
|
github.com/go-openapi/swag/conv v0.26.1 h1:slr5FVkg9Wc3Y5zcwenD8Sd/PQ94b2I/QJI7N7KTBpg=
|
||||||
|
github.com/go-openapi/swag/conv v0.26.1/go.mod h1:mvQXgPptZk9GTrFgGwWvT4q+dN+zQej9JfmGwnipz1A=
|
||||||
|
github.com/go-openapi/swag/jsonname v0.26.1 h1:VReupaV6WxlAsCn0e4DUfgV6bPmINnPpyJDLqSfNPcE=
|
||||||
|
github.com/go-openapi/swag/jsonname v0.26.1/go.mod h1:OvdW6BoWoj33pTfi7x9vFrgmT+fk7aw0BRwvCE0YOuc=
|
||||||
|
github.com/go-openapi/swag/jsonutils v0.26.1 h1:2hdBfFkHg+7Wrz2VsCbeyR6hzkRDs7AztnMR2u84yOY=
|
||||||
|
github.com/go-openapi/swag/jsonutils v0.26.1/go.mod h1:U+RMJH3wa+6BRiphuRtIyI8fW9HPFqFQ4sHk2oRx0UQ=
|
||||||
|
github.com/go-openapi/swag/jsonutils/fixtures_test v0.26.1 h1:1CD7NiLLb/TXl3tOnFYU4b+mNfb5rtgHkaA+q7RMYYQ=
|
||||||
|
github.com/go-openapi/swag/jsonutils/fixtures_test v0.26.1/go.mod h1:ZWafc8nMdYzTE3uYY6W86f0n46+IF0g4uUyRhJw/kXc=
|
||||||
|
github.com/go-openapi/swag/loading v0.26.1 h1:E9K4wqXeROlhjFQ13K9zMz6ojFGXIggGe+ad1odrK9w=
|
||||||
|
github.com/go-openapi/swag/loading v0.26.1/go.mod h1:3qvRIlWzWdq1HvmldwmuJ2ohpcAryN6xVt2OTKd0/7E=
|
||||||
|
github.com/go-openapi/swag/mangling v0.26.1 h1:gpYI4WuPKFJJVjV5cDLGlDVJhFIxYjQc7yN5eEb4CqM=
|
||||||
|
github.com/go-openapi/swag/mangling v0.26.1/go.mod h1:POETDH01hqAdASXfw7ISEd9bCOE6xBHOt8NHmGZRmYM=
|
||||||
|
github.com/go-openapi/swag/stringutils v0.26.1 h1:f88uYyTso7TnHrKM/bUBsQ5e2wKf37cpgo6pvbzd9yU=
|
||||||
|
github.com/go-openapi/swag/stringutils v0.26.1/go.mod h1:Sc6d3bU8fgk5AyZR8/8jEQ+Is/Ald+TD/IIggPN8UJk=
|
||||||
|
github.com/go-openapi/swag/typeutils v0.26.1 h1:yg42FgMzRR6PVQ3M3qHz1s+Y6/P4HoJ3cBarXa3OVnU=
|
||||||
|
github.com/go-openapi/swag/typeutils v0.26.1/go.mod h1:VfnV+oUtSP2vCSCn2aJgnr8OevUYemyIzzS1VOzS10o=
|
||||||
|
github.com/go-openapi/swag/yamlutils v0.26.1 h1:0TSLK+lXs9vfIhAWzBeI/lOzEnIoot6WTCO1aAeWFTk=
|
||||||
|
github.com/go-openapi/swag/yamlutils v0.26.1/go.mod h1:7W5b7PRX9MxwL7TjeG7H8HkyBGRsIDRObhyMWFgBI2M=
|
||||||
|
github.com/go-openapi/testify/enable/yaml/v2 v2.5.1 h1:q9NtHwK4qHF7yZziBPvZyv7zWAIk8ok88Gh2mR6Jpc8=
|
||||||
|
github.com/go-openapi/testify/enable/yaml/v2 v2.5.1/go.mod h1:JW0MXIotCYps/XsgJnG3a8Q7rE5xAiBwoOD5OfaIQBk=
|
||||||
|
github.com/go-openapi/testify/v2 v2.5.1 h1:TMdhCaw8fUNraVSf3Omoob1dO/AzBfhtFAPW0an6sBo=
|
||||||
|
github.com/go-openapi/testify/v2 v2.5.1/go.mod h1:SgsVHtfooshd0tublTtJ50FPKhujf47YRqauXXOUxfw=
|
||||||
|
github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
|
||||||
|
github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||||
github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU=
|
github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU=
|
||||||
github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||||
github.com/lmittmann/tint v1.1.3 h1:Hv4EaHWXQr+GTFnOU4VKf8UvAtZgn0VuKT+G0wFlO3I=
|
github.com/gomarkdown/markdown v0.0.0-20191123064959-2c17d62f5098 h1:Qxs3bNRWe8GTcKMxYOSXm0jx6j0de8XUtb/fsP3GZ0I=
|
||||||
github.com/lmittmann/tint v1.1.3/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE=
|
github.com/gomarkdown/markdown v0.0.0-20191123064959-2c17d62f5098/go.mod h1:aii0r/K0ZnHv7G0KF7xy1v0A7s2Ljrb5byB7MO5p6TU=
|
||||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
|
github.com/kyokomi/emoji/v2 v2.2.8 h1:jcofPxjHWEkJtkIbcLHvZhxKgCPl6C7MyjTrD4KDqUE=
|
||||||
|
github.com/kyokomi/emoji/v2 v2.2.8/go.mod h1:JUcn42DTdsXJo1SWanHh4HKDEyPaR5CqkmoirZZP9qE=
|
||||||
|
github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||||
|
github.com/lucasb-eyer/go-colorful v1.4.0 h1:UtrWVfLdarDgc44HcS7pYloGHJUjHV/4FwW4TvVgFr4=
|
||||||
|
github.com/lucasb-eyer/go-colorful v1.4.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||||
|
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||||
|
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||||
github.com/mattn/go-isatty v0.0.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs=
|
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||||
github.com/mattn/go-isatty v0.0.21/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
|
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
|
||||||
github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
|
github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
|
||||||
github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
|
github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
|
||||||
github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
|
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
|
||||||
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
|
||||||
|
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
||||||
github.com/mattn/go-runewidth v0.0.23 h1:7ykA0T0jkPpzSvMS5i9uoNn2Xy3R383f9HDx3RybWcw=
|
github.com/mattn/go-runewidth v0.0.23 h1:7ykA0T0jkPpzSvMS5i9uoNn2Xy3R383f9HDx3RybWcw=
|
||||||
github.com/mattn/go-runewidth v0.0.23/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
github.com/mattn/go-runewidth v0.0.23/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
||||||
|
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||||
|
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
|
||||||
|
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
|
||||||
|
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
|
||||||
|
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
||||||
|
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
|
||||||
|
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
|
||||||
|
github.com/oklog/ulid/v2 v2.1.1 h1:suPZ4ARWLOJLegGFiZZ1dFAkqzhMjL3J1TzI+5wHz8s=
|
||||||
|
github.com/oklog/ulid/v2 v2.1.1/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ=
|
||||||
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 h1:zrbMGy9YXpIeTnGj4EljqMiZsIcE09mmF8XsD5AYOJc=
|
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 h1:zrbMGy9YXpIeTnGj4EljqMiZsIcE09mmF8XsD5AYOJc=
|
||||||
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6/go.mod h1:rEKTHC9roVVicUIfZK7DYrdIoM0EOr8mK1Hj5s3JjH0=
|
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6/go.mod h1:rEKTHC9roVVicUIfZK7DYrdIoM0EOr8mK1Hj5s3JjH0=
|
||||||
github.com/olekukonko/errors v1.2.0 h1:10Zcn4GeV59t/EGqJc8fUjtFT/FuUh5bTMzZ1XwmCRo=
|
github.com/olekukonko/errors v1.2.0 h1:10Zcn4GeV59t/EGqJc8fUjtFT/FuUh5bTMzZ1XwmCRo=
|
||||||
github.com/olekukonko/errors v1.2.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y=
|
github.com/olekukonko/errors v1.2.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y=
|
||||||
github.com/olekukonko/ll v0.1.6 h1:lGVTHO+Qc4Qm+fce/2h2m5y9LvqaW+DCN7xW9hsU3uA=
|
|
||||||
github.com/olekukonko/ll v0.1.6/go.mod h1:NVUmjBb/aCtUpjKk75BhWrOlARz3dqsM+OtszpY4o88=
|
|
||||||
github.com/olekukonko/ll v0.1.8 h1:ysHCJRGHYKzmBSdz9w5AySztx7lG8SQY+naTGYUbsz8=
|
github.com/olekukonko/ll v0.1.8 h1:ysHCJRGHYKzmBSdz9w5AySztx7lG8SQY+naTGYUbsz8=
|
||||||
github.com/olekukonko/ll v0.1.8/go.mod h1:RPRC6UcscfFZgjo1nulkfMH5IM0QAYim0LfnMvUuozw=
|
github.com/olekukonko/ll v0.1.8/go.mod h1:RPRC6UcscfFZgjo1nulkfMH5IM0QAYim0LfnMvUuozw=
|
||||||
github.com/olekukonko/tablewriter v1.1.4 h1:ORUMI3dXbMnRlRggJX3+q7OzQFDdvgbN9nVWj1drm6I=
|
github.com/olekukonko/tablewriter v1.1.4 h1:ORUMI3dXbMnRlRggJX3+q7OzQFDdvgbN9nVWj1drm6I=
|
||||||
github.com/olekukonko/tablewriter v1.1.4/go.mod h1:+kedxuyTtgoZLwif3P1Em4hARJs+mVnzKxmsCL/C5RY=
|
github.com/olekukonko/tablewriter v1.1.4/go.mod h1:+kedxuyTtgoZLwif3P1Em4hARJs+mVnzKxmsCL/C5RY=
|
||||||
|
github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o=
|
||||||
|
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
|
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||||
|
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||||
|
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||||
|
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
||||||
|
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
|
||||||
|
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
github.com/tidwall/gjson v1.19.0 h1:xwxm7n691Uf3u5OFjzngavjGTh55KX5q/9w9xHW88JU=
|
github.com/tidwall/gjson v1.19.0 h1:xwxm7n691Uf3u5OFjzngavjGTh55KX5q/9w9xHW88JU=
|
||||||
github.com/tidwall/gjson v1.19.0/go.mod h1:V37/opeE/JbLUOfH0QTXiNez2l0RUjYUhpT4szFQAfc=
|
github.com/tidwall/gjson v1.19.0/go.mod h1:V37/opeE/JbLUOfH0QTXiNez2l0RUjYUhpT4szFQAfc=
|
||||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||||
@@ -66,30 +187,50 @@ 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.8.0 h1:XqKPrm0q4P0q5JpoclYoCAv0/MIvH/jZ2umzuf8pNTI=
|
|
||||||
github.com/urfave/cli/v3 v3.8.0/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso=
|
|
||||||
github.com/urfave/cli/v3 v3.9.0 h1:AV9lIiPv3ukYnxunaCUsHnEozptYmDN2F0+yWqLMn/c=
|
|
||||||
github.com/urfave/cli/v3 v3.9.0/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso=
|
|
||||||
github.com/urfave/cli/v3 v3.9.1-0.20260524212652-be8b79d0c8de h1:ESKPiS7inVoBnv4FmgGNZdjWBI/wmvaragoyD3D9nM4=
|
github.com/urfave/cli/v3 v3.9.1-0.20260524212652-be8b79d0c8de 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.9.1-0.20260524212652-be8b79d0c8de/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso=
|
||||||
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||||
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||||
go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
|
go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
|
||||||
go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
|
go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
|
||||||
go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
|
go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
|
||||||
go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
|
go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
|
||||||
|
go.opentelemetry.io/otel/sdk v1.29.0 h1:vkqKjk7gwhS8VaWb0POZKmIEDimRCMsopNYnriHyryo=
|
||||||
|
go.opentelemetry.io/otel/sdk v1.29.0/go.mod h1:pM8Dx5WKnvxLCb+8lG1PRNIDxu9g9b9g59Qr7hfAAok=
|
||||||
go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
|
go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
|
||||||
go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
|
go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
|
||||||
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM=
|
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||||
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80=
|
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||||
|
golang.org/dl v0.0.0-20190829154251-82a15e2f2ead/go.mod h1:IUMfjQLJQd4UTqG1Z90tenwKoCX93Gn3MAQJMOSBsDQ=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
|
||||||
|
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
|
||||||
|
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||||
|
golang.org/x/image v0.0.0-20191206065243-da761ea9ff43 h1:gQ6GUSD102fPgli+Yb4cR/cGaHF7tNBt+GYoRCpGC7s=
|
||||||
|
golang.org/x/image v0.0.0-20191206065243-da761ea9ff43/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||||
|
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
|
||||||
|
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
|
||||||
|
golang.org/x/sys v0.0.0-20181128092732-4ed8d59d0b35/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-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.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
|
||||||
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
|
||||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc=
|
||||||
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y=
|
||||||
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
|
||||||
|
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import (
|
|||||||
"log/slog"
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"reflect"
|
||||||
|
|
||||||
"github.com/alecthomas/repr"
|
"github.com/alecthomas/repr"
|
||||||
"github.com/elastic/elastic-transport-go/v8/elastictransport"
|
"github.com/elastic/elastic-transport-go/v8/elastictransport"
|
||||||
@@ -34,7 +35,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
Version string = `v0.0.15`
|
Version string = `v0.0.20`
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -54,11 +55,29 @@ type Config struct {
|
|||||||
Output string // -o <mode>
|
Output string // -o <mode>
|
||||||
Clusters map[string]*Cluster
|
Clusters map[string]*Cluster
|
||||||
DefaultCluster *Cluster
|
DefaultCluster *Cluster
|
||||||
|
HaveJQ bool // determined at runtime by ourselfes
|
||||||
|
ProfileFile string // for internal use (golang profiling)
|
||||||
|
|
||||||
Index string // index: -i
|
Index string // index: -i
|
||||||
Failed, Partials bool // index: flags
|
Failed, Partials bool // index: flags
|
||||||
Shards, Replicas int // index create+allocation: -s -r
|
Shards, Replicas int // index create+allocation: -s -r
|
||||||
Wait bool // index create: -w
|
Wait bool // index create: -w
|
||||||
Primary bool // index allocation: -p
|
Primary bool // index allocation: -p
|
||||||
|
Searchable bool // index fields: -s
|
||||||
|
Aggretable bool // index fields: -a
|
||||||
|
|
||||||
|
Priority int // index template create: -p
|
||||||
|
Settings []string // index template create: -s
|
||||||
|
Patterns []string // index template create: -i
|
||||||
|
Components []string // index template create: -c
|
||||||
|
Meta []string // index template create: -M
|
||||||
|
Aliases []string // index template create: -A
|
||||||
|
Stream bool // index template create: -S
|
||||||
|
AutoCreate bool // index template create: -a
|
||||||
|
Mode string // index template create: -a
|
||||||
|
Retention string // index template create: -r
|
||||||
|
Rollover bool // index template create: -R
|
||||||
|
|
||||||
From, To, MaxItems int // search: flags
|
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
|
||||||
@@ -68,14 +87,32 @@ type Config struct {
|
|||||||
Range string // search: -r
|
Range string // search: -r
|
||||||
TimestampFormat string // search: --timestamp-format
|
TimestampFormat string // search: --timestamp-format
|
||||||
Explain bool // search: -e
|
Explain bool // search: -e
|
||||||
|
Validate bool // search: --validate
|
||||||
|
|
||||||
SortBy string // sort: -k
|
SortBy string // sort: -k
|
||||||
Ascending bool // sort: -a
|
Ascending bool // sort: -a
|
||||||
|
|
||||||
Exclude string // cluster compare: -e (regexp)
|
Exclude string // cluster compare: -e (regexp)
|
||||||
All, Verbose bool // cluster status: -a -v
|
All, Verbose bool // cluster status: -a -v
|
||||||
Persistent, Transient, Default bool // -p -t -D cluster settings set
|
Persistent, Transient, Default bool // cluster settings set: -p -t -D
|
||||||
|
|
||||||
Force bool // ccr follower renew: -f
|
Force bool // ccr follower renew: -f
|
||||||
HaveJQ bool // determined at runtime by ourselfes
|
|
||||||
DebugHTTP bool // root: --debug-http
|
DebugHTTP bool // root: --debug-http
|
||||||
|
Separator string // role diff: -s
|
||||||
|
NotDeployed bool // role diff: -n
|
||||||
|
Undefined bool // role diff: -u
|
||||||
|
Diff bool // role diff: -D
|
||||||
|
Hidden bool // ds ls: -H
|
||||||
|
|
||||||
|
// rollover
|
||||||
|
MaxAge string
|
||||||
|
MaxDocs, MaxShardSize, MaxShardDocs int // roll over
|
||||||
|
DryRun bool // rollover: -n
|
||||||
|
|
||||||
|
Tag string // api ls: -t
|
||||||
|
|
||||||
|
Ilm Ilm // ilm create
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewConfig() *Config {
|
func NewConfig() *Config {
|
||||||
@@ -126,6 +163,7 @@ func (conf *Config) Init() error {
|
|||||||
if err == nil {
|
if err == nil {
|
||||||
conf.DefaultCluster = cluster
|
conf.DefaultCluster = cluster
|
||||||
conf.CurrentCluster = name
|
conf.CurrentCluster = name
|
||||||
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -138,17 +176,33 @@ func (conf *Config) Init() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// we are using reflect to clone a config obj w/o the ES stuff for
|
||||||
|
// shorter repr.Println() output (the ES structure is just too large)
|
||||||
|
func (conf *Config) Clone() Config {
|
||||||
|
clone := Config{}
|
||||||
|
|
||||||
|
ref := reflect.ValueOf(*conf)
|
||||||
|
typeOfS := ref.Type()
|
||||||
|
|
||||||
|
for i := 0; i < ref.NumField(); i++ {
|
||||||
|
field := typeOfS.Field(i).Name
|
||||||
|
|
||||||
|
if field == "Clusters" || field == "DefaultCluster" || !ref.Field(i).CanInterface() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
reflect.ValueOf(&clone).Elem().FieldByName(field).Set(reflect.ValueOf(ref.Field(i).Interface()))
|
||||||
|
}
|
||||||
|
|
||||||
|
return clone
|
||||||
|
}
|
||||||
|
|
||||||
func (conf *Config) PrintDebug() {
|
func (conf *Config) PrintDebug() {
|
||||||
if !conf.Debug {
|
if !conf.Debug {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
clone := *conf
|
clone := conf.Clone()
|
||||||
|
|
||||||
for name := range clone.Clusters {
|
|
||||||
clone.Clusters[name] = nil
|
|
||||||
}
|
|
||||||
clone.DefaultCluster = nil
|
|
||||||
|
|
||||||
fmt.Println("config:")
|
fmt.Println("config:")
|
||||||
repr.Println(clone)
|
repr.Println(clone)
|
||||||
|
|||||||
72
pkg/cfg/ilm.go
Normal file
72
pkg/cfg/ilm.go
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
/*
|
||||||
|
Copyright © 2026 Thomas von Dein
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package cfg
|
||||||
|
|
||||||
|
type Ilm struct {
|
||||||
|
Description string
|
||||||
|
|
||||||
|
HotMinAge string
|
||||||
|
HotRolloverMaxAge string
|
||||||
|
HotRolloverMaxPrimaryShardSize int64
|
||||||
|
HotRolloverMaxDocs int64
|
||||||
|
HotReadonly bool
|
||||||
|
|
||||||
|
WarmMinAge string
|
||||||
|
WarmForceMerge int
|
||||||
|
WarmShrinkShards int
|
||||||
|
WarmPriority int
|
||||||
|
|
||||||
|
ColdMinAge string
|
||||||
|
ColdSearchableSnapshotRepo string
|
||||||
|
ColdForceMerge int
|
||||||
|
ColdPriority int
|
||||||
|
|
||||||
|
FrozenMinAge string
|
||||||
|
|
||||||
|
DeleteMinAge string
|
||||||
|
DeleteSearchableSnapshots bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg *Ilm) HaveHot() bool {
|
||||||
|
return cfg.HotMinAge != "" ||
|
||||||
|
cfg.HotRolloverMaxAge != "" ||
|
||||||
|
cfg.HotRolloverMaxDocs > 0 ||
|
||||||
|
cfg.HotRolloverMaxPrimaryShardSize > 0 ||
|
||||||
|
cfg.HotReadonly
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg *Ilm) HaveWarm() bool {
|
||||||
|
return cfg.WarmMinAge != "" ||
|
||||||
|
cfg.WarmForceMerge > 0 ||
|
||||||
|
cfg.WarmShrinkShards > 0 ||
|
||||||
|
cfg.WarmPriority > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg *Ilm) HaveCold() bool {
|
||||||
|
return cfg.ColdMinAge != "" ||
|
||||||
|
cfg.ColdSearchableSnapshotRepo != "" ||
|
||||||
|
cfg.ColdForceMerge > 0 ||
|
||||||
|
cfg.ColdPriority > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg *Ilm) HaveFrozen() bool {
|
||||||
|
return cfg.FrozenMinAge != ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg *Ilm) HaveDelete() bool {
|
||||||
|
return cfg.DeleteSearchableSnapshots || cfg.DeleteMinAge != ""
|
||||||
|
}
|
||||||
530
pkg/es/api.go
Normal file
530
pkg/es/api.go
Normal file
@@ -0,0 +1,530 @@
|
|||||||
|
/*
|
||||||
|
Copyright © 2026 Thomas von Dein
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package es
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log/slog"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"regexp"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"codeberg.org/scip/esctl/assets"
|
||||||
|
"codeberg.org/scip/esctl/pkg/cfg"
|
||||||
|
"codeberg.org/scip/esctl/pkg/printer"
|
||||||
|
markdown "github.com/MichaelMure/go-term-markdown"
|
||||||
|
"github.com/charmbracelet/lipgloss"
|
||||||
|
"github.com/chzyer/readline"
|
||||||
|
"github.com/go-openapi/spec"
|
||||||
|
"golang.org/x/term"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
DefaultMargin = 4
|
||||||
|
intro = `Input format: verb path [data]"
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
post /yourindex/_ccr/pause_follow
|
||||||
|
put /yourindex/_settings {"number_of_replicas": 1}
|
||||||
|
|
||||||
|
You can also put multiline JSON after the path like:
|
||||||
|
|
||||||
|
put /yourindex/_settings
|
||||||
|
{
|
||||||
|
"number_of_replicas": 1
|
||||||
|
}
|
||||||
|
|
||||||
|
If you do NOT supply a JSON in the first line, you need to hit ENTER
|
||||||
|
twice to complete.`
|
||||||
|
)
|
||||||
|
|
||||||
|
// holds an API operation via go-openapi/spec
|
||||||
|
type Op struct {
|
||||||
|
Verb string
|
||||||
|
Op *spec.Operation
|
||||||
|
}
|
||||||
|
|
||||||
|
type Param struct {
|
||||||
|
Description, Param string
|
||||||
|
}
|
||||||
|
|
||||||
|
// API operation parameter type
|
||||||
|
type Params struct {
|
||||||
|
Path []Param
|
||||||
|
Query []Param
|
||||||
|
}
|
||||||
|
|
||||||
|
func ApiRepl(conf *cfg.Config) error {
|
||||||
|
verbs := []string{"post", "get", "put", "delete"}
|
||||||
|
|
||||||
|
fmt.Println(intro)
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
|
reader, err := readline.NewEx(&readline.Config{
|
||||||
|
Prompt: "> ",
|
||||||
|
HistoryFile: os.Getenv("HOME") + "/.config/esctl/history",
|
||||||
|
HistoryLimit: 500,
|
||||||
|
InterruptPrompt: "^C",
|
||||||
|
EOFPrompt: "exit",
|
||||||
|
HistorySearchFold: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to initialize readline lib: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
text, err := reader.Readline()
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
text = strings.TrimSpace(text)
|
||||||
|
|
||||||
|
if text == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := strings.SplitN(strings.TrimSpace(text), " ", 3)
|
||||||
|
if len(parts) < 2 {
|
||||||
|
fmt.Println("error: you need to input a verb, uri [and post data]")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if !slices.Contains(verbs, strings.ToLower(parts[0])) {
|
||||||
|
fmt.Println("error: verb must be one of " + strings.Join(verbs, ","))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.HasPrefix(parts[1], "/") {
|
||||||
|
parts[1] = "/" + parts[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
json := ""
|
||||||
|
if len(parts) == 3 {
|
||||||
|
// put /uri {json}
|
||||||
|
json = parts[2]
|
||||||
|
}
|
||||||
|
|
||||||
|
// put /uri/<Ret> [json]
|
||||||
|
data, err := readJSON(json)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
raw, err := CallAPI(conf, parts[0], parts[1], data)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("failed to call API: %s\n", esErrorString(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := prettyfiJson(conf, raw); err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func encodeAuth(username, password string) string {
|
||||||
|
return base64.StdEncoding.EncodeToString([]byte(username + ":" + password))
|
||||||
|
}
|
||||||
|
|
||||||
|
func CallAPI(conf *cfg.Config, verb, path, data string) ([]byte, error) {
|
||||||
|
verb = strings.ToUpper(verb)
|
||||||
|
|
||||||
|
// we're using port-forwards anyway
|
||||||
|
tr := &http.Transport{
|
||||||
|
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||||
|
}
|
||||||
|
|
||||||
|
client := &http.Client{Transport: tr}
|
||||||
|
|
||||||
|
req, err := http.NewRequest(verb, conf.DefaultCluster.Uri+path, bytes.NewBuffer([]byte(data)))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Add("Content-Type", "application/json")
|
||||||
|
req.Header.Add("accept", "application/json")
|
||||||
|
req.Header.Add("Authorization", "Basic "+encodeAuth(conf.DefaultCluster.User, conf.DefaultCluster.Pass))
|
||||||
|
|
||||||
|
// actually execute the request
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read and print response
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read response body: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return body, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func prettyfiJson(conf *cfg.Config, raw []byte) error {
|
||||||
|
if conf.HaveJQ {
|
||||||
|
cmd := exec.CommandContext(context.Background(), "jq", "-C")
|
||||||
|
cmd.Stdin = bytes.NewReader(raw)
|
||||||
|
|
||||||
|
var out bytes.Buffer
|
||||||
|
cmd.Stdout = &out
|
||||||
|
|
||||||
|
err := cmd.Run()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println(out.String())
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var pretty bytes.Buffer
|
||||||
|
err := json.Indent(&pretty, raw, "", "\t")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("json parse error: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println(pretty.String())
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// interactively read arbitrary JSON data from STDIN, which is
|
||||||
|
// virtually a repl inside the primary repl
|
||||||
|
func readJSON(input string) (string, error) {
|
||||||
|
data := ""
|
||||||
|
|
||||||
|
if input != "" {
|
||||||
|
data = input
|
||||||
|
} else {
|
||||||
|
scanner := bufio.NewScanner(os.Stdin)
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := strings.TrimSpace(scanner.Text())
|
||||||
|
|
||||||
|
if line == "" {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
data += line
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if data == "" {
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// validate
|
||||||
|
check := map[string]any{}
|
||||||
|
err := json.Unmarshal([]byte(data), &check)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("error: input data is not proper JSON: %s", err)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ApiList(conf *cfg.Config, pattern string) error {
|
||||||
|
assets.LoadAssetOpenApi()
|
||||||
|
|
||||||
|
filter := regexp.MustCompile(pattern)
|
||||||
|
|
||||||
|
table := printer.NewTable(conf, 4, 0)
|
||||||
|
table.Addheaders("path", "http verb", "tag", "description")
|
||||||
|
|
||||||
|
for path, item := range assets.OpenAPI.Spec().Paths.Paths {
|
||||||
|
if pattern != "" {
|
||||||
|
if !filter.MatchString(path) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ops := findOperation(item.PathItemProps)
|
||||||
|
|
||||||
|
for _, op := range ops {
|
||||||
|
tags := findTags(op)
|
||||||
|
|
||||||
|
if conf.Tag != "" {
|
||||||
|
if !slices.Contains(tags, conf.Tag) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
table.AddRow(
|
||||||
|
path,
|
||||||
|
op.Verb,
|
||||||
|
strings.Join(tags, ","),
|
||||||
|
strings.TrimSpace(op.Op.Summary),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
table.Sort()
|
||||||
|
return table.Print()
|
||||||
|
}
|
||||||
|
|
||||||
|
// for completion
|
||||||
|
func ApiPathNames() []string {
|
||||||
|
assets.LoadAssetOpenApi()
|
||||||
|
|
||||||
|
paths := []string{}
|
||||||
|
|
||||||
|
for path := range assets.OpenAPI.Spec().Paths.Paths {
|
||||||
|
paths = append(paths, path)
|
||||||
|
}
|
||||||
|
|
||||||
|
return paths
|
||||||
|
}
|
||||||
|
|
||||||
|
func ApiShow(conf *cfg.Config, showpath, verb string) error {
|
||||||
|
assets.LoadAssetOpenApi()
|
||||||
|
out := printer.Builder{}
|
||||||
|
|
||||||
|
op, err := matchOperation(showpath, verb)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.Debug("api operation", "op", op)
|
||||||
|
|
||||||
|
cleanMarkup := regexp.MustCompile(`<[^<>]+>`)
|
||||||
|
|
||||||
|
width := getTermWidth()
|
||||||
|
params := getApiParameters(op, showpath, width)
|
||||||
|
sample := getApiExample(op)
|
||||||
|
description := markdown.Render(cleanMarkup.ReplaceAllString(op.Op.Description, ""), width, DefaultMargin)
|
||||||
|
|
||||||
|
var bold = lipgloss.NewStyle().
|
||||||
|
Bold(true)
|
||||||
|
var paragraph = lipgloss.NewStyle().
|
||||||
|
MarginBottom(1).
|
||||||
|
MarginLeft(DefaultMargin)
|
||||||
|
var boldparagraph = lipgloss.NewStyle().
|
||||||
|
MarginBottom(1).
|
||||||
|
MarginLeft(DefaultMargin).
|
||||||
|
Bold(true)
|
||||||
|
var indentparagraph = lipgloss.NewStyle().
|
||||||
|
MarginBottom(1).
|
||||||
|
MarginLeft(2)
|
||||||
|
|
||||||
|
out.WriteStringLine(bold.Render("ID: " + op.Op.ID))
|
||||||
|
out.WriteStringLine(paragraph.Render(fmt.Sprintf("%s %s", op.Verb, showpath)))
|
||||||
|
|
||||||
|
out.WriteStringLine(bold.Render("Tags"))
|
||||||
|
out.WriteStringLine(paragraph.Render(strings.Join(op.Op.Tags, ", ")))
|
||||||
|
|
||||||
|
out.WriteStringLine(bold.Render("Summary"))
|
||||||
|
out.WriteStringLine(paragraph.Render(strings.TrimSpace(op.Op.Summary)))
|
||||||
|
|
||||||
|
out.WriteStringLine(bold.Render("Description"))
|
||||||
|
out.WriteStringLine(string(description)) // already indented
|
||||||
|
|
||||||
|
if len(params.Path) > 0 {
|
||||||
|
out.WriteStringLine(bold.Render("Path Parameters"))
|
||||||
|
for _, param := range params.Path {
|
||||||
|
out.WriteStringLine(boldparagraph.Render(param.Param))
|
||||||
|
if param.Description != "" {
|
||||||
|
//out.WriteStringLine(indentparagraph.Render(param.Description))
|
||||||
|
out.WriteStringLine(param.Description)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(params.Query) > 0 {
|
||||||
|
out.WriteStringLine(bold.Render("Query Parameters"))
|
||||||
|
for _, param := range params.Query {
|
||||||
|
out.WriteStringLine(boldparagraph.Render(param.Param))
|
||||||
|
if param.Description != "" {
|
||||||
|
out.WriteStringLine(indentparagraph.Render(param.Description))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if sample != "" {
|
||||||
|
out.WriteStringLine(bold.Render("Example"))
|
||||||
|
out.WriteStringLine(paragraph.Render(sample))
|
||||||
|
}
|
||||||
|
|
||||||
|
printer.Pager(showpath, out.String())
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// find all tags associated with op
|
||||||
|
func findTags(op *Op) []string {
|
||||||
|
return op.Op.Tags
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get API parameters
|
||||||
|
func getApiParameters(op *Op, path string, width int) Params {
|
||||||
|
params := Params{}
|
||||||
|
pathParams := []string{}
|
||||||
|
pathParamsReg := regexp.MustCompile(`{([a-z_]+)}`)
|
||||||
|
|
||||||
|
// find params in url path
|
||||||
|
for _, match := range pathParamsReg.FindAllStringSubmatch(path, -1) {
|
||||||
|
if len(match) == 2 {
|
||||||
|
pathParams = append(pathParams, match[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
empty := spec.ParamProps{}
|
||||||
|
for _, param := range op.Op.Parameters {
|
||||||
|
if param.ParamProps == empty {
|
||||||
|
for _, token := range param.Refable.Ref.Ref.GetPointer().DecodedTokens() {
|
||||||
|
parts := strings.Split(token, "-")
|
||||||
|
if len(parts) == 2 {
|
||||||
|
param := parts[1]
|
||||||
|
if slices.Contains(pathParams, param) {
|
||||||
|
params.Path = append(params.Path, Param{Param: param})
|
||||||
|
} else {
|
||||||
|
params.Query = append(params.Query, Param{Param: param})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
par := Param{
|
||||||
|
Description: string(markdown.Render(param.Description, width, DefaultMargin)),
|
||||||
|
Param: param.Name,
|
||||||
|
}
|
||||||
|
|
||||||
|
if param.In == "query" {
|
||||||
|
params.Query = append(params.Query, par)
|
||||||
|
} else {
|
||||||
|
params.Path = append(params.Path, par)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return params
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get console example
|
||||||
|
func getApiExample(op *Op) string {
|
||||||
|
sample := ""
|
||||||
|
|
||||||
|
samples, sampleexist := op.Op.Extensions["x-codeSamples"]
|
||||||
|
if sampleexist {
|
||||||
|
for _, itemany := range samples.([]any) {
|
||||||
|
item := itemany.(map[string]any)
|
||||||
|
|
||||||
|
lang, haslang := item["lang"]
|
||||||
|
if haslang {
|
||||||
|
if lang == "Console" {
|
||||||
|
source, hassource := item["source"]
|
||||||
|
if hassource {
|
||||||
|
sample = strings.TrimSpace(source.(string))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sample
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find an API operation which matches given path and verb.
|
||||||
|
// If no verb is given and only 1 op exists, return this one,
|
||||||
|
// otherwise showpath+verb have to match precisely.
|
||||||
|
func matchOperation(showpath, verb string) (*Op, error) {
|
||||||
|
ops := []*Op{}
|
||||||
|
op := &Op{}
|
||||||
|
var found bool
|
||||||
|
|
||||||
|
for path, item := range assets.OpenAPI.Spec().Paths.Paths {
|
||||||
|
if path == showpath {
|
||||||
|
ops = findOperation(item.PathItemProps)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(ops) == 0 {
|
||||||
|
return nil, errors.New("no matching API call found")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(ops) == 1 && verb == "" {
|
||||||
|
return ops[0], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, item := range ops {
|
||||||
|
if strings.ToLower(item.Verb) == verb {
|
||||||
|
op = item
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !found {
|
||||||
|
return nil, errors.New("no matching API call found for given path+verb")
|
||||||
|
}
|
||||||
|
|
||||||
|
return op, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func findOperation(item spec.PathItemProps) []*Op {
|
||||||
|
ops := []*Op{}
|
||||||
|
|
||||||
|
if item.Post != nil {
|
||||||
|
ops = append(ops, &Op{"POST", item.Post})
|
||||||
|
}
|
||||||
|
|
||||||
|
if item.Get != nil {
|
||||||
|
ops = append(ops, &Op{"GET", item.Get})
|
||||||
|
}
|
||||||
|
|
||||||
|
if item.Delete != nil {
|
||||||
|
ops = append(ops, &Op{"DELETE", item.Delete})
|
||||||
|
}
|
||||||
|
|
||||||
|
if item.Put != nil {
|
||||||
|
ops = append(ops, &Op{"PUT", item.Put})
|
||||||
|
}
|
||||||
|
|
||||||
|
if item.Patch != nil {
|
||||||
|
ops = append(ops, &Op{"PATCH", item.Patch})
|
||||||
|
}
|
||||||
|
|
||||||
|
return ops
|
||||||
|
}
|
||||||
|
|
||||||
|
func getTermWidth() int {
|
||||||
|
if term.IsTerminal(int(os.Stdout.Fd())) {
|
||||||
|
width, _, err := term.GetSize(int(os.Stdout.Fd()))
|
||||||
|
if err == nil {
|
||||||
|
return width - DefaultMargin
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 80
|
||||||
|
}
|
||||||
@@ -27,6 +27,8 @@ import (
|
|||||||
"codeberg.org/scip/esctl/pkg/cfg"
|
"codeberg.org/scip/esctl/pkg/cfg"
|
||||||
"codeberg.org/scip/esctl/pkg/printer"
|
"codeberg.org/scip/esctl/pkg/printer"
|
||||||
"github.com/dustin/go-humanize"
|
"github.com/dustin/go-humanize"
|
||||||
|
"github.com/elastic/go-elasticsearch/v9/typedapi/cat/indices"
|
||||||
|
"github.com/elastic/go-elasticsearch/v9/typedapi/cat/tasks"
|
||||||
"github.com/elastic/go-elasticsearch/v9/typedapi/ccr/stats"
|
"github.com/elastic/go-elasticsearch/v9/typedapi/ccr/stats"
|
||||||
"github.com/elastic/go-elasticsearch/v9/typedapi/cluster/health"
|
"github.com/elastic/go-elasticsearch/v9/typedapi/cluster/health"
|
||||||
clusterstats "github.com/elastic/go-elasticsearch/v9/typedapi/cluster/stats"
|
clusterstats "github.com/elastic/go-elasticsearch/v9/typedapi/cluster/stats"
|
||||||
@@ -39,6 +41,8 @@ const (
|
|||||||
ResponseInfo
|
ResponseInfo
|
||||||
ResponseCcr
|
ResponseCcr
|
||||||
ResponseStats
|
ResponseStats
|
||||||
|
ResponseIndices
|
||||||
|
ResponseTasks
|
||||||
)
|
)
|
||||||
|
|
||||||
type ClusterIndices map[string]map[string]*types.IndicesRecord
|
type ClusterIndices map[string]map[string]*types.IndicesRecord
|
||||||
@@ -49,6 +53,8 @@ type apiResponse struct {
|
|||||||
health *health.Response
|
health *health.Response
|
||||||
ccr *stats.Response
|
ccr *stats.Response
|
||||||
stats *clusterstats.Response
|
stats *clusterstats.Response
|
||||||
|
indices *indices.Response
|
||||||
|
tasks *tasks.Response
|
||||||
which int
|
which int
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,7 +100,7 @@ func ClusterList(conf *cfg.Config) error {
|
|||||||
// have to do 3 of'em for each cluster. This speeds things up.
|
// have to do 3 of'em for each cluster. This speeds things up.
|
||||||
func ClusterStatus(conf *cfg.Config) error {
|
func ClusterStatus(conf *cfg.Config) error {
|
||||||
clusters := []string{}
|
clusters := []string{}
|
||||||
gocount := 3
|
gocount := 5
|
||||||
if conf.Verbose {
|
if conf.Verbose {
|
||||||
gocount++
|
gocount++
|
||||||
}
|
}
|
||||||
@@ -120,6 +126,8 @@ func ClusterStatus(conf *cfg.Config) error {
|
|||||||
go getClusterData(es, wg, responses, "health")
|
go getClusterData(es, wg, responses, "health")
|
||||||
go getClusterData(es, wg, responses, "info")
|
go getClusterData(es, wg, responses, "info")
|
||||||
go getClusterData(es, wg, responses, "ccrstats")
|
go getClusterData(es, wg, responses, "ccrstats")
|
||||||
|
go getClusterData(es, wg, responses, "indices")
|
||||||
|
go getClusterData(es, wg, responses, "tasks")
|
||||||
|
|
||||||
if conf.Verbose {
|
if conf.Verbose {
|
||||||
go getClusterData(es, wg, responses, "stats")
|
go getClusterData(es, wg, responses, "stats")
|
||||||
@@ -131,6 +139,8 @@ func ClusterStatus(conf *cfg.Config) error {
|
|||||||
var info *info.Response
|
var info *info.Response
|
||||||
var ccrstats *stats.Response
|
var ccrstats *stats.Response
|
||||||
var clusterstats *clusterstats.Response
|
var clusterstats *clusterstats.Response
|
||||||
|
var indexstats *indices.Response
|
||||||
|
var taskstatus *tasks.Response
|
||||||
|
|
||||||
for i := 0; i < gocount; i++ {
|
for i := 0; i < gocount; i++ {
|
||||||
r := <-responses
|
r := <-responses
|
||||||
@@ -148,11 +158,17 @@ func ClusterStatus(conf *cfg.Config) error {
|
|||||||
info = r.info
|
info = r.info
|
||||||
case ResponseStats:
|
case ResponseStats:
|
||||||
clusterstats = r.stats
|
clusterstats = r.stats
|
||||||
|
case ResponseIndices:
|
||||||
|
indexstats = r.indices
|
||||||
|
case ResponseTasks:
|
||||||
|
taskstatus = r.tasks
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
slog.Debug("ES result", "cluster health", clusterhealth)
|
slog.Debug("ES result", "cluster health", clusterhealth)
|
||||||
|
|
||||||
|
isleader := len(ccrstats.AutoFollowStats.AutoFollowedClusters) == 0
|
||||||
|
|
||||||
ccrfollowing := ""
|
ccrfollowing := ""
|
||||||
if len(ccrstats.AutoFollowStats.AutoFollowedClusters) > 0 {
|
if len(ccrstats.AutoFollowStats.AutoFollowedClusters) > 0 {
|
||||||
// is following another cluster
|
// is following another cluster
|
||||||
@@ -163,6 +179,22 @@ func ClusterStatus(conf *cfg.Config) error {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// look for red indices, if any
|
||||||
|
redindices := 0
|
||||||
|
for _, index := range *indexstats {
|
||||||
|
if *index.Health == "red" {
|
||||||
|
redindices++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// look for long running tasks
|
||||||
|
longtasks := 0
|
||||||
|
for _, task := range *taskstatus {
|
||||||
|
if strings.Contains(*task.RunningTime, "d") {
|
||||||
|
longtasks++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
table := printer.NewTable(conf, 2, 7)
|
table := printer.NewTable(conf, 2, 7)
|
||||||
table.Addheaders(cluster, "status")
|
table.Addheaders(cluster, "status")
|
||||||
|
|
||||||
@@ -170,10 +202,22 @@ func ClusterStatus(conf *cfg.Config) error {
|
|||||||
{"Cluster Name", clusterhealth.ClusterName},
|
{"Cluster Name", clusterhealth.ClusterName},
|
||||||
{"ES Status", printer.Colorize(conf, clusterhealth.Status.Name, clusterhealth.Status.Name)},
|
{"ES Status", printer.Colorize(conf, clusterhealth.Status.Name, clusterhealth.Status.Name)},
|
||||||
{"ES Version", info.Version.Int},
|
{"ES Version", info.Version.Int},
|
||||||
|
{"Is Leader", fmt.Sprintf("%t", isleader)},
|
||||||
{"Active Shards", fmt.Sprintf("%d", clusterhealth.ActiveShards)},
|
{"Active Shards", fmt.Sprintf("%d", clusterhealth.ActiveShards)},
|
||||||
{"Active Primary Shards", fmt.Sprintf("%d", clusterhealth.ActivePrimaryShards)},
|
{"Active Primary Shards", fmt.Sprintf("%d", clusterhealth.ActivePrimaryShards)},
|
||||||
|
{"Unassigned Shards", fmt.Sprintf("%d", clusterhealth.UnassignedShards)},
|
||||||
|
{"Unassigned Primary Shards", fmt.Sprintf("%d", clusterhealth.UnassignedPrimaryShards)},
|
||||||
|
{"Pending Tasks", fmt.Sprintf("%d", clusterhealth.NumberOfPendingTasks)},
|
||||||
{"Nodes", fmt.Sprintf("%d", clusterhealth.NumberOfNodes)},
|
{"Nodes", fmt.Sprintf("%d", clusterhealth.NumberOfNodes)},
|
||||||
|
{"Red Indices", fmt.Sprintf("%d", redindices)},
|
||||||
|
{"Long Running Tasks", fmt.Sprintf("%d", longtasks)},
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isleader {
|
||||||
|
table.Entries = append(table.Entries, [][]string{
|
||||||
{"AutoFollow (success/failed indices)", ccrfollowing},
|
{"AutoFollow (success/failed indices)", ccrfollowing},
|
||||||
|
{"Followed Indices", fmt.Sprintf("%d", len(ccrstats.FollowStats.Indices))},
|
||||||
|
}...)
|
||||||
}
|
}
|
||||||
|
|
||||||
if conf.Verbose {
|
if conf.Verbose {
|
||||||
@@ -202,11 +246,8 @@ func gatherClusterStats(conf *cfg.Config, clusterstats *clusterstats.Response, t
|
|||||||
clusterstats.Nodes.Jvm.Versions[0].VmVersion}, " ")
|
clusterstats.Nodes.Jvm.Versions[0].VmVersion}, " ")
|
||||||
}
|
}
|
||||||
|
|
||||||
isleader := checkClusterIsLeader(conf, conf.CurrentCluster)
|
|
||||||
|
|
||||||
table.Entries = append(table.Entries, [][]string{
|
table.Entries = append(table.Entries, [][]string{
|
||||||
{"Indicies", fmt.Sprintf("%d", clusterstats.Indices.Count)},
|
{"Indicies", fmt.Sprintf("%d", clusterstats.Indices.Count)},
|
||||||
{"Is Leader", fmt.Sprintf("%t", isleader)},
|
|
||||||
{"Docs", fmt.Sprintf("%d", clusterstats.Indices.Docs.Count)},
|
{"Docs", fmt.Sprintf("%d", clusterstats.Indices.Docs.Count)},
|
||||||
{"Total Size", humanize.Bytes(uint64(clusterstats.Indices.Docs.TotalSizeInBytes))},
|
{"Total Size", humanize.Bytes(uint64(clusterstats.Indices.Docs.TotalSizeInBytes))},
|
||||||
{"Total Queries", fmt.Sprintf("%d", querycount)},
|
{"Total Queries", fmt.Sprintf("%d", querycount)},
|
||||||
@@ -217,7 +258,7 @@ func gatherClusterStats(conf *cfg.Config, clusterstats *clusterstats.Response, t
|
|||||||
humanize.Bytes(uint64(clusterstats.Indices.Store.SizeInBytes)),
|
humanize.Bytes(uint64(clusterstats.Indices.Store.SizeInBytes)),
|
||||||
humanize.Bytes(uint64(*clusterstats.Indices.Store.TotalDataSetSizeInBytes)),
|
humanize.Bytes(uint64(*clusterstats.Indices.Store.TotalDataSetSizeInBytes)),
|
||||||
)},
|
)},
|
||||||
{"JVM Heap", fmt.Sprintf(
|
{"JVM Heap Memory", fmt.Sprintf(
|
||||||
"%s/%s",
|
"%s/%s",
|
||||||
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)),
|
||||||
|
|||||||
@@ -27,36 +27,6 @@ import (
|
|||||||
"github.com/urfave/cli/v3"
|
"github.com/urfave/cli/v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
// recursively traverse the raw settings hash and build a flat map
|
|
||||||
// consisting of the translated path and its value.
|
|
||||||
//
|
|
||||||
// e.g.
|
|
||||||
// logger:
|
|
||||||
//
|
|
||||||
// org:
|
|
||||||
// elasticsearch:
|
|
||||||
// transport:
|
|
||||||
// OutboundHandler: "ERROR"
|
|
||||||
//
|
|
||||||
// gets:
|
|
||||||
//
|
|
||||||
// logger.org.elasticsearch.transport.OutboundHandler: "ERROR"
|
|
||||||
func getJsonPath(raw map[string]any, topic string) map[string]string {
|
|
||||||
paths := map[string]string{}
|
|
||||||
|
|
||||||
for name, data := range raw {
|
|
||||||
path := topic + "." + name
|
|
||||||
switch value := data.(type) {
|
|
||||||
case string:
|
|
||||||
paths[path] = value
|
|
||||||
case map[string]any:
|
|
||||||
paths = getJsonPath(value, path)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return paths
|
|
||||||
}
|
|
||||||
|
|
||||||
func ClusterSettingsList(conf *cfg.Config) error {
|
func ClusterSettingsList(conf *cfg.Config) error {
|
||||||
res, err := conf.DefaultCluster.ES.Cluster.GetSettings().
|
res, err := conf.DefaultCluster.ES.Cluster.GetSettings().
|
||||||
Header("content-type", "application/json").
|
Header("content-type", "application/json").
|
||||||
@@ -87,7 +57,7 @@ func ClusterSettingsList(conf *cfg.Config) error {
|
|||||||
return fmt.Errorf("failed to unmarshall setting for topic %s: %s", topic, err)
|
return fmt.Errorf("failed to unmarshall setting for topic %s: %s", topic, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
paths := getJsonPath(data, topic)
|
paths := getJsonPath(map[string]string{}, data, topic)
|
||||||
slog.Debug("settings", topic, paths)
|
slog.Debug("settings", topic, paths)
|
||||||
|
|
||||||
for setting, value := range paths {
|
for setting, value := range paths {
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
@@ -389,6 +390,26 @@ func getClusterData(es *elasticsearch.TypedClient, wg *sync.WaitGroup, reschan c
|
|||||||
ar.stats = res
|
ar.stats = res
|
||||||
ar.which = ResponseStats
|
ar.which = ResponseStats
|
||||||
arerr = err
|
arerr = err
|
||||||
|
|
||||||
|
case "indices":
|
||||||
|
res, err := es.Cat.Indices().
|
||||||
|
Header("content-type", "application/json").
|
||||||
|
Header("accept", "application/json").
|
||||||
|
Do(context.Background())
|
||||||
|
|
||||||
|
ar.indices = &res
|
||||||
|
ar.which = ResponseIndices
|
||||||
|
arerr = err
|
||||||
|
|
||||||
|
case "tasks":
|
||||||
|
res, err := es.Cat.Tasks().
|
||||||
|
Header("content-type", "application/json").
|
||||||
|
Header("accept", "application/json").
|
||||||
|
Do(context.Background())
|
||||||
|
|
||||||
|
ar.tasks = &res
|
||||||
|
ar.which = ResponseTasks
|
||||||
|
arerr = err
|
||||||
}
|
}
|
||||||
|
|
||||||
if arerr != nil {
|
if arerr != nil {
|
||||||
@@ -397,3 +418,44 @@ func getClusterData(es *elasticsearch.TypedClient, wg *sync.WaitGroup, reschan c
|
|||||||
|
|
||||||
reschan <- ar
|
reschan <- ar
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// recursively traverse the raw settings hash and build a flat map
|
||||||
|
// consisting of the translated path and its value.
|
||||||
|
//
|
||||||
|
// e.g.
|
||||||
|
// logger:
|
||||||
|
//
|
||||||
|
// org:
|
||||||
|
// elasticsearch:
|
||||||
|
// transport:
|
||||||
|
// OutboundHandler: "ERROR"
|
||||||
|
//
|
||||||
|
// gets:
|
||||||
|
//
|
||||||
|
// logger.org.elasticsearch.transport.OutboundHandler: "ERROR"
|
||||||
|
func getJsonPath(paths map[string]string, raw map[string]any, topic string) map[string]string {
|
||||||
|
for name, data := range raw {
|
||||||
|
path := topic + "." + name
|
||||||
|
|
||||||
|
switch value := data.(type) {
|
||||||
|
case string:
|
||||||
|
paths[path] = value
|
||||||
|
case *string:
|
||||||
|
paths[path] = *value
|
||||||
|
case int:
|
||||||
|
paths[path] = strconv.Itoa(value)
|
||||||
|
case *int:
|
||||||
|
paths[path] = strconv.Itoa(*value)
|
||||||
|
case map[string]any:
|
||||||
|
paths = getJsonPath(paths, value, path)
|
||||||
|
case []any:
|
||||||
|
val := []string{}
|
||||||
|
for _, item := range value {
|
||||||
|
val = append(val, fmt.Sprintf("%v", item))
|
||||||
|
}
|
||||||
|
paths[path] = strings.Join(val, ",")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return paths
|
||||||
|
}
|
||||||
|
|||||||
259
pkg/es/datastream.go
Normal file
259
pkg/es/datastream.go
Normal file
@@ -0,0 +1,259 @@
|
|||||||
|
/*
|
||||||
|
Copyright © 2026 Thomas von Dein
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package es
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"regexp"
|
||||||
|
|
||||||
|
"codeberg.org/scip/esctl/pkg/cfg"
|
||||||
|
"codeberg.org/scip/esctl/pkg/printer"
|
||||||
|
"github.com/dustin/go-humanize"
|
||||||
|
"github.com/elastic/go-elasticsearch/v9/typedapi/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
func DatastreamNames(conf *cfg.Config) ([]string, error) {
|
||||||
|
res, err := conf.DefaultCluster.ES.Indices.GetDataStream().
|
||||||
|
Header("content-type", "application/json").
|
||||||
|
Header("accept", "application/json").
|
||||||
|
Do(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get data streams: %s", esErrorString(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
dss := make([]string, len(res.DataStreams))
|
||||||
|
for idx, ds := range res.DataStreams {
|
||||||
|
dss[idx] = ds.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
return dss, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func DatastreamList(conf *cfg.Config) error {
|
||||||
|
res, err := conf.DefaultCluster.ES.Indices.GetDataStream().
|
||||||
|
Header("content-type", "application/json").
|
||||||
|
Header("accept", "application/json").
|
||||||
|
Do(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get data streams: %s", esErrorString(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.Debug("ES result", "data streams", res)
|
||||||
|
|
||||||
|
list := filterDatastreams(conf, res.DataStreams)
|
||||||
|
|
||||||
|
size := len(list)
|
||||||
|
|
||||||
|
if conf.MaxItems > 0 {
|
||||||
|
if size > conf.MaxItems {
|
||||||
|
size = conf.MaxItems
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
table := printer.NewTable(conf, 7, size)
|
||||||
|
table.Addheaders("name", "ilm policy", "hidden", "system", "replicated", "generation", "timestamp field")
|
||||||
|
|
||||||
|
for idx, ds := range list {
|
||||||
|
name := printer.Colorize(conf, ds.Status.String(), ds.Name)
|
||||||
|
policy := ""
|
||||||
|
if ds.IlmPolicy != nil {
|
||||||
|
policy = *ds.IlmPolicy
|
||||||
|
}
|
||||||
|
|
||||||
|
table.Entries[idx] = []string{
|
||||||
|
name,
|
||||||
|
policy,
|
||||||
|
fmt.Sprintf("%t", ds.Hidden),
|
||||||
|
fmt.Sprintf("%t", *ds.System),
|
||||||
|
fmt.Sprintf("%t", *ds.Replicated),
|
||||||
|
fmt.Sprintf("%d", ds.Generation),
|
||||||
|
ds.TimestampField.Name,
|
||||||
|
}
|
||||||
|
|
||||||
|
if idx == size-1 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
table.Sort()
|
||||||
|
if err := table.Print(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func filterDatastreams(conf *cfg.Config, list []types.DataStream) []types.DataStream {
|
||||||
|
selected := []types.DataStream{}
|
||||||
|
|
||||||
|
for _, ds := range list {
|
||||||
|
if !conf.Hidden && ds.Hidden {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
selected = append(selected, ds)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(conf.Filter) == 0 {
|
||||||
|
return selected
|
||||||
|
}
|
||||||
|
|
||||||
|
// we support just one filter here, for now
|
||||||
|
filter := *regexp.MustCompile(conf.Filter[0])
|
||||||
|
newlist := []types.DataStream{}
|
||||||
|
|
||||||
|
for _, ds := range selected {
|
||||||
|
if filter.MatchString(ds.Name) {
|
||||||
|
newlist = append(newlist, ds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return newlist
|
||||||
|
}
|
||||||
|
|
||||||
|
func DatastreamShow(conf *cfg.Config, dsname string) error {
|
||||||
|
res, err := conf.DefaultCluster.ES.Indices.GetDataStream().
|
||||||
|
Name(dsname).
|
||||||
|
Header("content-type", "application/json").
|
||||||
|
Header("accept", "application/json").
|
||||||
|
Do(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get data stream: %s", esErrorString(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.Debug("ES result", "data stream", res)
|
||||||
|
|
||||||
|
if len(res.DataStreams) == 0 {
|
||||||
|
return errors.New("no data stream found with that name")
|
||||||
|
}
|
||||||
|
|
||||||
|
stats, err := conf.DefaultCluster.ES.Indices.DataStreamsStats().
|
||||||
|
Name(dsname).
|
||||||
|
Header("content-type", "application/json").
|
||||||
|
Header("accept", "application/json").
|
||||||
|
Do(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get data stream stats: %s", esErrorString(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
table := printer.NewTable(conf, 11, 2)
|
||||||
|
table.Addheaders("data stream property", "value")
|
||||||
|
|
||||||
|
ds := res.DataStreams[0]
|
||||||
|
|
||||||
|
name := printer.Colorize(conf, ds.Status.String(), ds.Name)
|
||||||
|
policy := ""
|
||||||
|
if ds.IlmPolicy != nil {
|
||||||
|
policy = *ds.IlmPolicy
|
||||||
|
}
|
||||||
|
|
||||||
|
table.Entries = [][]string{
|
||||||
|
{"name", name},
|
||||||
|
{"ilm policy", policy},
|
||||||
|
{"hidden", fmt.Sprintf("%t", ds.Hidden)},
|
||||||
|
{"system", fmt.Sprintf("%t", *ds.System)},
|
||||||
|
{"replicated", fmt.Sprintf("%t", *ds.Replicated)},
|
||||||
|
{"generation", fmt.Sprintf("%d", ds.Generation)},
|
||||||
|
{"timestamp field", ds.TimestampField.Name},
|
||||||
|
{"backing indices", fmt.Sprintf("%d", stats.BackingIndices)},
|
||||||
|
{"size", humanize.Bytes(uint64(stats.TotalStoreSizeBytes))},
|
||||||
|
{"shards-failed", fmt.Sprintf("%d", stats.Shards_.Failed)},
|
||||||
|
{"shards-successful", fmt.Sprintf("%d", stats.Shards_.Successful)},
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := table.Print(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
table = printer.NewTable(conf, 5, len(ds.Indices))
|
||||||
|
table.Addheaders("backing index name", "uuid", "prefer ilm", "ilm policy", "managed by")
|
||||||
|
|
||||||
|
for idx, index := range ds.Indices {
|
||||||
|
policy := ""
|
||||||
|
if ds.IlmPolicy != nil {
|
||||||
|
policy = *ds.IlmPolicy
|
||||||
|
}
|
||||||
|
|
||||||
|
table.Entries[idx] = []string{
|
||||||
|
index.IndexName,
|
||||||
|
index.IndexUuid,
|
||||||
|
fmt.Sprintf("%t", *index.PreferIlm),
|
||||||
|
policy,
|
||||||
|
index.ManagedBy.Name,
|
||||||
|
}
|
||||||
|
|
||||||
|
if idx < len(ds.Indices) {
|
||||||
|
fmt.Println()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
table.Sort()
|
||||||
|
if err := table.Print(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func DatastreamCreate(conf *cfg.Config, dsname string) error {
|
||||||
|
_, err := conf.DefaultCluster.ES.Indices.CreateDataStream(dsname).
|
||||||
|
Header("content-type", "application/json").
|
||||||
|
Header("accept", "application/json").
|
||||||
|
Do(context.Background())
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create datastream: %s", esErrorString(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func DatastreamDelete(conf *cfg.Config, dsname string) error {
|
||||||
|
_, err := conf.DefaultCluster.ES.Indices.DeleteDataStream(dsname).
|
||||||
|
Header("content-type", "application/json").
|
||||||
|
Header("accept", "application/json").
|
||||||
|
Do(context.Background())
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to delete datastream: %s", esErrorString(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func DatastreamRollover(conf *cfg.Config, ds string) error {
|
||||||
|
res, err := RolloverAlias(conf, ds)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to rollover data stream: %s", esErrorString(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
table := printer.NewTable(conf, 2, 5)
|
||||||
|
table.Addheaders("rollover response", "value")
|
||||||
|
table.Entries = [][]string{
|
||||||
|
{"acknowledged", fmt.Sprintf("%t", res.Acknowledged)},
|
||||||
|
{"rolled over", fmt.Sprintf("%t", res.RolledOver)},
|
||||||
|
{"shards acknowledged", fmt.Sprintf("%t", res.ShardsAcknowledged)},
|
||||||
|
{"old index", res.OldIndex},
|
||||||
|
{"new index", res.NewIndex},
|
||||||
|
}
|
||||||
|
|
||||||
|
return table.Print()
|
||||||
|
}
|
||||||
@@ -33,7 +33,12 @@ func esErrorString(err error) string {
|
|||||||
causes += fmt.Sprintf("%s\n", *cause.Reason)
|
causes += fmt.Sprintf("%s\n", *cause.Reason)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if e.ErrorCause.Reason != nil {
|
||||||
msg = *e.ErrorCause.Reason + ": " + causes
|
msg = *e.ErrorCause.Reason + ": " + causes
|
||||||
|
} else {
|
||||||
|
msg = fmt.Sprintf("http status %d: ", e.Status)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return msg
|
return msg
|
||||||
|
|||||||
301
pkg/es/ilm.go
Normal file
301
pkg/es/ilm.go
Normal file
@@ -0,0 +1,301 @@
|
|||||||
|
/*
|
||||||
|
Copyright © 2026 Thomas von Dein
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package es
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"codeberg.org/scip/esctl/pkg/cfg"
|
||||||
|
"codeberg.org/scip/esctl/pkg/printer"
|
||||||
|
"github.com/alecthomas/repr"
|
||||||
|
"github.com/elastic/go-elasticsearch/v9/typedapi/esdsl"
|
||||||
|
"github.com/elastic/go-elasticsearch/v9/typedapi/ilm/putlifecycle"
|
||||||
|
"github.com/elastic/go-elasticsearch/v9/typedapi/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
func IlmRetry(conf *cfg.Config, index string) error {
|
||||||
|
_, err := conf.DefaultCluster.ES.Ilm.Retry(index).
|
||||||
|
Do(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to retry ilm: %s", esErrorString(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func IlmStatus(conf *cfg.Config) error {
|
||||||
|
res, err := conf.DefaultCluster.ES.Ilm.GetStatus().
|
||||||
|
Do(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get ilm status: %s", esErrorString(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println(res.OperationMode.Name)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func IlmNames(conf *cfg.Config) ([]string, error) {
|
||||||
|
res, err := conf.DefaultCluster.ES.Ilm.GetLifecycle().
|
||||||
|
Do(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get ilm policies: %s", esErrorString(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
names := make([]string, len(res))
|
||||||
|
idx := 0
|
||||||
|
|
||||||
|
for name := range res {
|
||||||
|
names[idx] = name
|
||||||
|
}
|
||||||
|
|
||||||
|
return names, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func IlmList(conf *cfg.Config, pattern string) error {
|
||||||
|
ilm := conf.DefaultCluster.ES.Ilm.GetLifecycle()
|
||||||
|
|
||||||
|
if pattern != "" {
|
||||||
|
ilm.FilterPath(pattern)
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := ilm.Do(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get ilm policies: %s", esErrorString(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
if conf.Debug {
|
||||||
|
repr.Println(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
table := printer.NewTable(conf, 5, 0)
|
||||||
|
table.Addheaders("ilm policy", "hot", "warm", "frozen", "delete")
|
||||||
|
|
||||||
|
for name, ilm := range res {
|
||||||
|
table.AddRow(name,
|
||||||
|
ilmPhaseString(ilm.Policy.Phases.Hot, true),
|
||||||
|
ilmPhaseString(ilm.Policy.Phases.Warm, true),
|
||||||
|
ilmPhaseString(ilm.Policy.Phases.Frozen, true),
|
||||||
|
ilmPhaseString(ilm.Policy.Phases.Delete, true),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
table.Sort()
|
||||||
|
return table.Print()
|
||||||
|
}
|
||||||
|
|
||||||
|
func IlmShow(conf *cfg.Config, policy string) error {
|
||||||
|
res, err := conf.DefaultCluster.ES.Ilm.GetLifecycle().
|
||||||
|
Policy(policy).
|
||||||
|
Do(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get ilm status: %s", esErrorString(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
if conf.Debug {
|
||||||
|
repr.Println(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
ilm, exists := res[policy]
|
||||||
|
if !exists {
|
||||||
|
return errors.New("no ilm policy retrieved")
|
||||||
|
}
|
||||||
|
|
||||||
|
table := printer.NewTable(conf, 2, 5)
|
||||||
|
table.Addheaders("ilm policy setting", "value")
|
||||||
|
|
||||||
|
table.Entries = [][]string{
|
||||||
|
{"policy", policy},
|
||||||
|
{"hot phase", ilmPhaseString(ilm.Policy.Phases.Hot, false)},
|
||||||
|
{"warm phase", ilmPhaseString(ilm.Policy.Phases.Warm, false)},
|
||||||
|
{"frozen phase", ilmPhaseString(ilm.Policy.Phases.Frozen, false)},
|
||||||
|
{"delete phase", ilmPhaseString(ilm.Policy.Phases.Delete, false)},
|
||||||
|
}
|
||||||
|
|
||||||
|
return table.Print()
|
||||||
|
}
|
||||||
|
|
||||||
|
func ilmPhaseString(phase *types.Phase, short bool) string {
|
||||||
|
if phase == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
out := []string{fmt.Sprintf("move-after:%s", phase.MinAge)}
|
||||||
|
|
||||||
|
if short {
|
||||||
|
return strings.Join(out, ",")
|
||||||
|
}
|
||||||
|
|
||||||
|
if phase.Actions.Rollover != nil {
|
||||||
|
if phase.Actions.Rollover.MaxAge != "" {
|
||||||
|
out = append(out, fmt.Sprintf("rollover:%s", phase.Actions.Rollover.MaxAge))
|
||||||
|
if phase.Actions.Rollover.MaxPrimaryShardSize != nil {
|
||||||
|
out = append(out, fmt.Sprintf("shardsize:%s", phase.Actions.Rollover.MaxPrimaryShardSize))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if phase.Actions.Delete != nil {
|
||||||
|
out = append(out, fmt.Sprintf("delete-searchable-snapshot:%t", *phase.Actions.Delete.DeleteSearchableSnapshot))
|
||||||
|
}
|
||||||
|
|
||||||
|
if phase.Actions.SearchableSnapshot != nil {
|
||||||
|
out = append(out, fmt.Sprintf("searchable-snapshot:%s", phase.Actions.SearchableSnapshot.SnapshotRepository))
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Join(out, ",")
|
||||||
|
}
|
||||||
|
|
||||||
|
func IlmExplain(conf *cfg.Config, index string) error {
|
||||||
|
res, err := conf.DefaultCluster.ES.Ilm.ExplainLifecycle(index).
|
||||||
|
Do(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get ilm state: %s", esErrorString(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.Debug("ilm status", "ilm", res)
|
||||||
|
|
||||||
|
explain, exists := res.Indices[index]
|
||||||
|
if !exists {
|
||||||
|
return errors.New("no ilm status retrieved")
|
||||||
|
}
|
||||||
|
|
||||||
|
ilm := explain.(*types.LifecycleExplainManaged)
|
||||||
|
|
||||||
|
table := printer.NewTable(conf, 2, 0)
|
||||||
|
table.Addheaders("ilm status field", "value")
|
||||||
|
|
||||||
|
table.Entries = [][]string{
|
||||||
|
{"index", ilm.Index},
|
||||||
|
{"ilm policy", *ilm.Policy},
|
||||||
|
{"action", *ilm.Action},
|
||||||
|
{"step", *ilm.Step},
|
||||||
|
{"age", fmt.Sprintf("%s", ilm.Age)},
|
||||||
|
{"managed", fmt.Sprintf("%t", ilm.Managed)},
|
||||||
|
{"phase", *ilm.Phase},
|
||||||
|
{"phase execution", ilmPhaseString(ilm.PhaseExecution.PhaseDefinition, false)},
|
||||||
|
}
|
||||||
|
|
||||||
|
return table.Print()
|
||||||
|
}
|
||||||
|
|
||||||
|
func IlmCreate(conf *cfg.Config, policyname string) error {
|
||||||
|
ilm := conf.DefaultCluster.ES.Ilm.PutLifecycle(policyname)
|
||||||
|
cfg := conf.Ilm
|
||||||
|
|
||||||
|
var phases types.PhasesVariant = esdsl.NewPhases()
|
||||||
|
|
||||||
|
if cfg.HaveHot() {
|
||||||
|
hot := types.Phase{}
|
||||||
|
var actions types.IlmActionsVariant = esdsl.NewIlmActions()
|
||||||
|
rollover := &types.RolloverAction{}
|
||||||
|
haveroll := false
|
||||||
|
|
||||||
|
if cfg.HotMinAge != "" {
|
||||||
|
hot.MinAge = cfg.HotMinAge
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.HotRolloverMaxAge != "" {
|
||||||
|
rollover.MaxAge = cfg.HotRolloverMaxAge
|
||||||
|
haveroll = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.HotRolloverMaxDocs != 0 {
|
||||||
|
rollover.MaxDocs = &cfg.HotRolloverMaxDocs
|
||||||
|
haveroll = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.HotRolloverMaxPrimaryShardSize != 0 {
|
||||||
|
rollover.MaxPrimaryShardSize = &cfg.HotRolloverMaxPrimaryShardSize
|
||||||
|
}
|
||||||
|
|
||||||
|
if haveroll {
|
||||||
|
actions.IlmActionsCaster().Rollover = rollover
|
||||||
|
}
|
||||||
|
|
||||||
|
hot.Actions = actions.IlmActionsCaster()
|
||||||
|
phases.PhasesCaster().Hot = &hot
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.HaveWarm() {
|
||||||
|
warm := types.Phase{}
|
||||||
|
var actions types.IlmActionsVariant = esdsl.NewIlmActions()
|
||||||
|
|
||||||
|
if cfg.WarmForceMerge != 0 {
|
||||||
|
actions.IlmActionsCaster().Forcemerge = &types.ForceMergeAction{MaxNumSegments: cfg.WarmForceMerge}
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.WarmMinAge != "" {
|
||||||
|
warm.MinAge = cfg.WarmMinAge
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.WarmPriority != 0 {
|
||||||
|
actions.IlmActionsCaster().SetPriority = &types.SetPriorityAction{Priority: &cfg.WarmPriority}
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.WarmShrinkShards != 0 {
|
||||||
|
actions.IlmActionsCaster().Shrink = &types.ShrinkAction{NumberOfShards: &cfg.WarmShrinkShards}
|
||||||
|
}
|
||||||
|
|
||||||
|
warm.Actions = actions.IlmActionsCaster()
|
||||||
|
phases.PhasesCaster().Warm = &warm
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.HaveFrozen() {
|
||||||
|
froze := phases.PhasesCaster().Frozen
|
||||||
|
|
||||||
|
if cfg.FrozenMinAge != "" {
|
||||||
|
froze.MinAge = cfg.FrozenMinAge
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.HaveDelete() {
|
||||||
|
del := phases.PhasesCaster().Delete
|
||||||
|
delete := types.DeleteAction{}
|
||||||
|
var actions types.IlmActionsVariant = esdsl.NewIlmActions()
|
||||||
|
|
||||||
|
if cfg.DeleteMinAge != "" {
|
||||||
|
del.MinAge = cfg.DeleteMinAge
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.DeleteSearchableSnapshots {
|
||||||
|
delete.DeleteSearchableSnapshot = &cfg.DeleteSearchableSnapshots
|
||||||
|
}
|
||||||
|
|
||||||
|
actions.IlmActionsCaster().Delete = &delete
|
||||||
|
del.Actions = actions.IlmActionsCaster()
|
||||||
|
phases.PhasesCaster().Delete = del
|
||||||
|
}
|
||||||
|
|
||||||
|
put := &putlifecycle.Request{}
|
||||||
|
policy := &types.IlmPolicy{}
|
||||||
|
policy.IlmPolicyCaster().Phases = *phases.PhasesCaster()
|
||||||
|
put.Policy = policy
|
||||||
|
|
||||||
|
ilm.Request(put)
|
||||||
|
|
||||||
|
_, err := ilm.Do(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed create ilm policy: %s", esErrorString(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -21,6 +21,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -52,15 +53,30 @@ 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 {
|
||||||
|
// apply partials filter first
|
||||||
|
selectedlist := indices.Response{}
|
||||||
|
for _, index := range list {
|
||||||
|
if !conf.Partials && strings.HasPrefix(*index.Index, "partial-") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasPrefix(*index.Index, ".ds-") {
|
||||||
|
// ignore data stream backing indicies
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedlist = append(selectedlist, index)
|
||||||
|
}
|
||||||
|
|
||||||
if len(conf.Filter) == 0 {
|
if len(conf.Filter) == 0 {
|
||||||
return list
|
return selectedlist
|
||||||
}
|
}
|
||||||
|
|
||||||
// we support just one filter here, for now
|
// we support just one filter here, for now
|
||||||
filter := *regexp.MustCompile(conf.Filter[0])
|
filter := *regexp.MustCompile(conf.Filter[0])
|
||||||
newlist := indices.Response{}
|
newlist := indices.Response{}
|
||||||
|
|
||||||
for _, index := range list {
|
for _, index := range selectedlist {
|
||||||
if filter.MatchString(*index.Index) {
|
if filter.MatchString(*index.Index) {
|
||||||
newlist = append(newlist, index)
|
newlist = append(newlist, index)
|
||||||
}
|
}
|
||||||
@@ -110,15 +126,11 @@ func IndexList(conf *cfg.Config) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
table.Sort()
|
table.Sort()
|
||||||
if err := table.Print(); err != nil {
|
return table.Print()
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
func IndexShow(conf *cfg.Config, indexpattern string) error {
|
||||||
}
|
res, err := conf.DefaultCluster.ES.Indices.Get(indexpattern).
|
||||||
|
|
||||||
func IndexShow(conf *cfg.Config, index string) error {
|
|
||||||
res, err := conf.DefaultCluster.ES.Indices.Get(index).
|
|
||||||
// we need to add custom request headers, required for older ES instances
|
// we need to add custom request headers, required for older ES instances
|
||||||
Header("content-type", "application/json").
|
Header("content-type", "application/json").
|
||||||
Header("accept", "application/json").
|
Header("accept", "application/json").
|
||||||
@@ -127,11 +139,10 @@ func IndexShow(conf *cfg.Config, index string) error {
|
|||||||
return fmt.Errorf("failed to get index: %s", esErrorString(err))
|
return fmt.Errorf("failed to get index: %s", esErrorString(err))
|
||||||
}
|
}
|
||||||
|
|
||||||
slog.Debug("ES result", "index", res)
|
for name, index := range res {
|
||||||
|
fields := make([]string, len(index.Mappings.Properties))
|
||||||
fields := make([]string, len(res[index].Mappings.Properties))
|
|
||||||
idx := 0
|
idx := 0
|
||||||
for field := range res[index].Mappings.Properties {
|
for field := range index.Mappings.Properties {
|
||||||
fields[idx] = field
|
fields[idx] = field
|
||||||
idx++
|
idx++
|
||||||
}
|
}
|
||||||
@@ -139,7 +150,7 @@ func IndexShow(conf *cfg.Config, index string) error {
|
|||||||
table := printer.NewTable(conf, 2, 5)
|
table := printer.NewTable(conf, 2, 5)
|
||||||
table.Addheaders("index property", "value")
|
table.Addheaders("index property", "value")
|
||||||
|
|
||||||
ts, err := strconv.ParseInt(res[index].Settings.Index.CreationDate.(string), 10, 64)
|
ts, err := strconv.ParseInt(index.Settings.Index.CreationDate.(string), 10, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ts = 0
|
ts = 0
|
||||||
}
|
}
|
||||||
@@ -147,11 +158,11 @@ func IndexShow(conf *cfg.Config, index string) error {
|
|||||||
created := time.Unix(ts/1000, 0)
|
created := time.Unix(ts/1000, 0)
|
||||||
|
|
||||||
table.Entries = [][]string{
|
table.Entries = [][]string{
|
||||||
{"name", index},
|
{"name", name},
|
||||||
{"replicas", *res[index].Settings.Index.NumberOfReplicas},
|
{"replicas", *index.Settings.Index.NumberOfReplicas},
|
||||||
{"shards", *res[index].Settings.Index.NumberOfShards},
|
{"shards", *index.Settings.Index.NumberOfShards},
|
||||||
{"created", created.Format("2006-01-02 15:04:05")},
|
{"created", created.Format("2006-01-02 15:04:05")},
|
||||||
{"uuid", *res[index].Settings.Index.Uuid},
|
{"uuid", *index.Settings.Index.Uuid},
|
||||||
{"fields", strings.Join(fields, ",")},
|
{"fields", strings.Join(fields, ",")},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -159,6 +170,9 @@ func IndexShow(conf *cfg.Config, index string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fmt.Println()
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -311,17 +325,26 @@ func IndexFields(conf *cfg.Config, index string) error {
|
|||||||
return fmt.Errorf("failed to retrieve field capabilties: %s", esErrorString(err))
|
return fmt.Errorf("failed to retrieve field capabilties: %s", esErrorString(err))
|
||||||
}
|
}
|
||||||
|
|
||||||
table := printer.NewTable(conf, 5, len(res.Fields))
|
table := printer.NewTable(conf, 5, 0)
|
||||||
table.Addheaders("field", "type", "searchable", "aggretable", "metadata")
|
table.Addheaders("field", "type", "searchable", "aggretable", "metadata")
|
||||||
|
|
||||||
idx := 0
|
idx := 0
|
||||||
for name, field := range res.Fields {
|
for name, field := range res.Fields {
|
||||||
for fieldtype, caps := range field {
|
for fieldtype, caps := range field {
|
||||||
// fields only have 1 type, so this one is it
|
// fields only have 1 type, so this one is it
|
||||||
table.Entries[idx] = []string{name, fieldtype,
|
switch {
|
||||||
|
case conf.Searchable && !caps.Searchable:
|
||||||
|
continue
|
||||||
|
case conf.Aggretable && !caps.Aggregatable:
|
||||||
|
continue
|
||||||
|
case len(conf.Filter) > 0 && !slices.Contains(conf.Filter, fieldtype):
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
table.Entries = append(table.Entries, []string{name, fieldtype,
|
||||||
fmt.Sprintf("%t", caps.Searchable),
|
fmt.Sprintf("%t", caps.Searchable),
|
||||||
fmt.Sprintf("%t", caps.Aggregatable),
|
fmt.Sprintf("%t", caps.Aggregatable),
|
||||||
fmt.Sprintf("%t", *caps.MetadataField)}
|
fmt.Sprintf("%t", *caps.MetadataField)})
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -113,3 +113,23 @@ func IndexAliasDelete(conf *cfg.Config, index, alias string) error {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func IndexAliasRollover(conf *cfg.Config, alias string) error {
|
||||||
|
res, err := RolloverAlias(conf, alias)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to rollover index alias: %s", esErrorString(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
table := printer.NewTable(conf, 2, 5)
|
||||||
|
table.Addheaders("rollover response", "value")
|
||||||
|
table.Entries = [][]string{
|
||||||
|
{"acknowledged", fmt.Sprintf("%t", res.Acknowledged)},
|
||||||
|
{"rolled over", fmt.Sprintf("%t", res.RolledOver)},
|
||||||
|
{"shards acknowledged", fmt.Sprintf("%t", res.ShardsAcknowledged)},
|
||||||
|
{"old index", res.OldIndex},
|
||||||
|
{"new index", res.NewIndex},
|
||||||
|
}
|
||||||
|
|
||||||
|
return table.Print()
|
||||||
|
}
|
||||||
|
|||||||
503
pkg/es/index_template.go
Normal file
503
pkg/es/index_template.go
Normal file
@@ -0,0 +1,503 @@
|
|||||||
|
/*
|
||||||
|
Copyright © 2026 Thomas von Dein
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package es
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"codeberg.org/scip/esctl/pkg/cfg"
|
||||||
|
"codeberg.org/scip/esctl/pkg/printer"
|
||||||
|
"github.com/elastic/go-elasticsearch/v9/typedapi/esdsl"
|
||||||
|
"github.com/elastic/go-elasticsearch/v9/typedapi/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// used for completion
|
||||||
|
func IndexTemplateList(conf *cfg.Config) error {
|
||||||
|
res, err := conf.DefaultCluster.ES.Indices.GetIndexTemplate().
|
||||||
|
Header("content-type", "application/json").
|
||||||
|
Header("accept", "application/json").
|
||||||
|
Do(context.Background())
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get index templates: %s", esErrorString(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.Debug("res", "index templates", res)
|
||||||
|
|
||||||
|
table := printer.NewTable(conf, 5, len(res.IndexTemplates))
|
||||||
|
table.Addheaders("name", "description", "priority")
|
||||||
|
|
||||||
|
for idx, tpl := range res.IndexTemplates {
|
||||||
|
desc, err := json.Marshal(tpl.IndexTemplate.Meta_["description"])
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to unmarshal meta json data: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
table.Entries[idx] = []string{
|
||||||
|
tpl.Name,
|
||||||
|
string(desc),
|
||||||
|
fmt.Sprintf("%d", tpl.IndexTemplate.Priority),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
table.Sort()
|
||||||
|
return table.Print()
|
||||||
|
}
|
||||||
|
|
||||||
|
func IndexTemplateShow(conf *cfg.Config, tplname string) error {
|
||||||
|
res, err := conf.DefaultCluster.ES.Indices.GetIndexTemplate().
|
||||||
|
Name(tplname).
|
||||||
|
Header("content-type", "application/json").
|
||||||
|
Header("accept", "application/json").
|
||||||
|
Do(context.Background())
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get index template: %s", esErrorString(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.Debug("res", "index template", res)
|
||||||
|
|
||||||
|
if len(res.IndexTemplates) != 1 {
|
||||||
|
return errors.New("multiple or no index template matched the pattern")
|
||||||
|
}
|
||||||
|
|
||||||
|
tpl := res.IndexTemplates[0]
|
||||||
|
|
||||||
|
table := printer.NewTable(conf, 2, 6)
|
||||||
|
table.Addheaders("index template property", "value")
|
||||||
|
|
||||||
|
desc, err := json.Marshal(tpl.IndexTemplate.Meta_["description"])
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to unmarshal meta json data: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
hasds := tpl.IndexTemplate.DataStream != nil
|
||||||
|
|
||||||
|
aliases := []string{}
|
||||||
|
for alias := range tpl.IndexTemplate.Template.Aliases {
|
||||||
|
aliases = append(aliases, alias)
|
||||||
|
}
|
||||||
|
|
||||||
|
table.Entries = [][]string{
|
||||||
|
{"name", tpl.Name},
|
||||||
|
{"description", string(desc)},
|
||||||
|
{"index patterns", strings.Join(tpl.IndexTemplate.IndexPatterns, ",")},
|
||||||
|
{"composed of", strings.Join(tpl.IndexTemplate.ComposedOf, ",")},
|
||||||
|
{"data stream enabled", fmt.Sprintf("%t", hasds)},
|
||||||
|
{"aliases", strings.Join(aliases, ",")},
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := table.Print(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
table = printer.NewTable(conf, 2, 0)
|
||||||
|
table.Addheaders("index setting property", "value")
|
||||||
|
|
||||||
|
err = getIndexTemplateSettings(conf, tplname, table)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println()
|
||||||
|
if err := table.Print(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
table = printer.NewTable(conf, 2, 0)
|
||||||
|
table.Addheaders("index field mapping", "type")
|
||||||
|
|
||||||
|
if tpl.IndexTemplate.Template.Mappings != nil {
|
||||||
|
for name, field := range tpl.IndexTemplate.Template.Mappings.Properties {
|
||||||
|
typeval := ""
|
||||||
|
|
||||||
|
switch val := field.(type) {
|
||||||
|
case *types.IntegerNumberProperty:
|
||||||
|
typeval = val.Type
|
||||||
|
case *types.KeywordProperty:
|
||||||
|
typeval = val.Type
|
||||||
|
case *types.DateProperty:
|
||||||
|
typeval = val.Type
|
||||||
|
}
|
||||||
|
|
||||||
|
table.Entries = append(table.Entries, []string{
|
||||||
|
name, typeval,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println()
|
||||||
|
if err := table.Print(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func IndexTemplateCreate(conf *cfg.Config, name string, mappings []string) error {
|
||||||
|
settings := esdsl.NewIndexSettings()
|
||||||
|
maps := esdsl.NewIndexTemplateMapping()
|
||||||
|
|
||||||
|
create := conf.DefaultCluster.ES.Indices.PutIndexTemplate(name).
|
||||||
|
Header("content-type", "application/json").
|
||||||
|
Header("accept", "application/json")
|
||||||
|
|
||||||
|
if conf.Shards > 0 {
|
||||||
|
settings = settings.NumberOfShards(strconv.Itoa(conf.Shards))
|
||||||
|
}
|
||||||
|
|
||||||
|
if conf.Replicas > 0 {
|
||||||
|
settings = settings.NumberOfReplicas(strconv.Itoa(conf.Replicas))
|
||||||
|
}
|
||||||
|
|
||||||
|
if conf.Stream {
|
||||||
|
create.DataStream(esdsl.NewDataStreamVisibility())
|
||||||
|
|
||||||
|
if conf.Retention != "" {
|
||||||
|
maps.Lifecycle(
|
||||||
|
esdsl.NewDataStreamLifecycle().
|
||||||
|
DataRetention(
|
||||||
|
esdsl.NewDuration().String(conf.Retention)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if conf.AutoCreate {
|
||||||
|
create.AllowAutoCreate(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
if conf.Mode != "" {
|
||||||
|
settings = settings.Mode(conf.Mode)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(mappings) > 0 {
|
||||||
|
typemaps, err := modMappings(mappings)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
maps.Mappings(typemaps)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(conf.Components) > 0 {
|
||||||
|
create.ComposedOf(conf.Components...)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, alias := range conf.Aliases {
|
||||||
|
maps.AddAlias(alias, esdsl.NewAlias())
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(conf.Meta) > 0 {
|
||||||
|
metadata, err := modMeta(conf, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
create.Meta_(esdsl.NewMetadata(metadata))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(conf.Settings) > 0 {
|
||||||
|
usersettings, err := modSettings(conf)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
settings = settings.IndexSettings(usersettings)
|
||||||
|
}
|
||||||
|
|
||||||
|
maps.Settings(settings)
|
||||||
|
create.Template(maps)
|
||||||
|
create.IndexPatterns(conf.Patterns...)
|
||||||
|
|
||||||
|
_, err := create.Do(context.Background())
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create index template: %s", esErrorString(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME: func's too long, refactor
|
||||||
|
// FIXME: it's not yet possible to remove items in lists (aliases, mappings, settings), just overwrite them
|
||||||
|
func IndexTemplateModify(conf *cfg.Config, name string, mappings []string) error {
|
||||||
|
maps := esdsl.NewIndexTemplateMapping()
|
||||||
|
|
||||||
|
// load existing index mapping
|
||||||
|
res, err := conf.DefaultCluster.ES.Indices.GetIndexTemplate().
|
||||||
|
Name(name).
|
||||||
|
Header("content-type", "application/json").
|
||||||
|
Header("accept", "application/json").
|
||||||
|
Do(context.Background())
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get index template: %s", esErrorString(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.Debug("res", "index template", res)
|
||||||
|
|
||||||
|
if len(res.IndexTemplates) != 1 {
|
||||||
|
return errors.New("multiple or no index template matched the pattern")
|
||||||
|
}
|
||||||
|
|
||||||
|
tpl := res.IndexTemplates[0]
|
||||||
|
|
||||||
|
// our modify PUT request
|
||||||
|
modify := conf.DefaultCluster.ES.Indices.PutIndexTemplate(name).
|
||||||
|
Header("content-type", "application/json").
|
||||||
|
Header("accept", "application/json")
|
||||||
|
|
||||||
|
// load existing settings, if any
|
||||||
|
settings := tpl.IndexTemplate.Template.Settings
|
||||||
|
|
||||||
|
// pre fill mappings and aliases
|
||||||
|
maps.Mappings(tpl.IndexTemplate.Template.Mappings)
|
||||||
|
maps.Aliases(tpl.IndexTemplate.Template.Aliases)
|
||||||
|
|
||||||
|
// pre fill components
|
||||||
|
modify.ComposedOf(tpl.IndexTemplate.ComposedOf...)
|
||||||
|
|
||||||
|
// pre fill data stream config
|
||||||
|
if conf.Stream {
|
||||||
|
modify.DataStream(esdsl.NewDataStreamVisibility())
|
||||||
|
|
||||||
|
if conf.Retention != "" {
|
||||||
|
maps.Lifecycle(
|
||||||
|
esdsl.NewDataStreamLifecycle().
|
||||||
|
DataRetention(
|
||||||
|
esdsl.NewDuration().String(conf.Retention)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
modify.AllowAutoCreate(conf.AutoCreate)
|
||||||
|
|
||||||
|
if conf.Mode != "" {
|
||||||
|
*settings.Mode = conf.Mode
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(mappings) > 0 {
|
||||||
|
typemaps, err := modMappings(mappings)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
maps.Mappings(typemaps)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(conf.Components) > 0 {
|
||||||
|
modify.ComposedOf(conf.Components...)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, alias := range conf.Aliases {
|
||||||
|
maps.AddAlias(alias, esdsl.NewAlias())
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(conf.Meta) > 0 {
|
||||||
|
// pre fill meta, if any
|
||||||
|
metadata, err := modMeta(conf, tpl.IndexTemplate.Meta_)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
modify.Meta_(esdsl.NewMetadata(metadata))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(conf.Settings) > 0 {
|
||||||
|
usersettings, err := modSettings(conf)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
settings.IndexSettings = usersettings
|
||||||
|
}
|
||||||
|
|
||||||
|
patterns := tpl.IndexTemplate.IndexPatterns
|
||||||
|
if len(conf.Patterns) > 0 {
|
||||||
|
patterns = conf.Patterns
|
||||||
|
}
|
||||||
|
|
||||||
|
maps.Settings(settings)
|
||||||
|
modify.Template(maps)
|
||||||
|
modify.IndexPatterns(patterns...)
|
||||||
|
|
||||||
|
_, err = modify.Do(context.Background())
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to modify index template: %s", esErrorString(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
if conf.Rollover {
|
||||||
|
if err := rolloverAliasIndexTemplate(conf, name); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func IndexTemplateDelete(conf *cfg.Config, name string) error {
|
||||||
|
_, err := conf.DefaultCluster.ES.Indices.DeleteIndexTemplate(name).
|
||||||
|
Header("content-type", "application/json").
|
||||||
|
Header("accept", "application/json").
|
||||||
|
Do(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to delete index template: %s", esErrorString(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Based on given index template find the associated index patterns,
|
||||||
|
// find indices matching those, find their associated aliases and
|
||||||
|
// rollover all we find. Only run when conf.Rollover==true
|
||||||
|
func rolloverAliasIndexTemplate(conf *cfg.Config, name string) error {
|
||||||
|
res, err := conf.DefaultCluster.ES.Indices.GetIndexTemplate().
|
||||||
|
Name(name).
|
||||||
|
Do(context.Background())
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get index template: %s", esErrorString(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.Debug("res", "index template", res)
|
||||||
|
|
||||||
|
if len(res.IndexTemplates) != 1 {
|
||||||
|
return errors.New("multiple or no index template matched the pattern")
|
||||||
|
}
|
||||||
|
|
||||||
|
patterns := res.IndexTemplates[0].IndexTemplate.IndexPatterns
|
||||||
|
aliases := map[string]int{}
|
||||||
|
|
||||||
|
// find all aliases matching the patterns
|
||||||
|
for _, pattern := range patterns {
|
||||||
|
res, err := conf.DefaultCluster.ES.Indices.ResolveIndex(pattern).
|
||||||
|
Do(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to resolve index pattern: %s", esErrorString(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, index := range res.Indices {
|
||||||
|
for _, alias := range index.Aliases {
|
||||||
|
aliases[alias] = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(aliases) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
table := printer.NewTable(conf, 4, 0)
|
||||||
|
table.Addheaders("rollover alias", "status", "ack", "new index")
|
||||||
|
|
||||||
|
// apply rollover to all matching aliases, if any
|
||||||
|
for alias := range aliases {
|
||||||
|
res, err := RolloverAlias(conf, alias)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
table.Entries = append(table.Entries, []string{
|
||||||
|
alias,
|
||||||
|
fmt.Sprintf("%t", res.RolledOver),
|
||||||
|
fmt.Sprintf("%t", res.Acknowledged),
|
||||||
|
res.NewIndex,
|
||||||
|
})
|
||||||
|
|
||||||
|
if err := table.Print(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func modMappings(mappings []string) (types.TypeMappingVariant, error) {
|
||||||
|
typemaps := esdsl.NewTypeMapping()
|
||||||
|
|
||||||
|
for _, mapping := range mappings {
|
||||||
|
parts := strings.Split(mapping, ":")
|
||||||
|
if len(parts) != 2 {
|
||||||
|
return nil, fmt.Errorf(
|
||||||
|
"invalid mapping %s, expect <name:type> (type: integer, text, date, keyword)",
|
||||||
|
mapping)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch parts[1] {
|
||||||
|
case "text":
|
||||||
|
typemaps.AddProperty(parts[0], esdsl.NewTextProperty())
|
||||||
|
case "integer":
|
||||||
|
typemaps.AddProperty(parts[0], esdsl.NewIntegerNumberProperty())
|
||||||
|
case "date":
|
||||||
|
typemaps.AddProperty(parts[0], esdsl.NewDateProperty())
|
||||||
|
case "keyword":
|
||||||
|
typemaps.AddProperty(parts[0], esdsl.NewKeywordProperty())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return typemaps, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func modMeta(conf *cfg.Config, meta types.Metadata) (map[string]json.RawMessage, error) {
|
||||||
|
metadata := map[string]json.RawMessage{}
|
||||||
|
|
||||||
|
for key, value := range meta {
|
||||||
|
metadata[key] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, meta := range conf.Meta {
|
||||||
|
parts := strings.Split(meta, ":")
|
||||||
|
if len(parts) != 2 {
|
||||||
|
return nil, errors.New("meta data must be in the form key:value")
|
||||||
|
}
|
||||||
|
|
||||||
|
msg, err := json.Marshal(parts[1])
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to json marshal metadata %s: %w", meta, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
metadata[parts[0]] = msg
|
||||||
|
}
|
||||||
|
|
||||||
|
return metadata, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func modSettings(conf *cfg.Config) (map[string]json.RawMessage, error) {
|
||||||
|
usersettings := map[string]json.RawMessage{}
|
||||||
|
|
||||||
|
for _, meta := range conf.Settings {
|
||||||
|
parts := strings.Split(meta, ":")
|
||||||
|
if len(parts) != 2 {
|
||||||
|
return nil, errors.New("settings data must be in the form key:value")
|
||||||
|
}
|
||||||
|
|
||||||
|
msg, err := json.Marshal(parts[1])
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to json marshal metadata %s: %w", meta, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
usersettings[parts[0]] = msg
|
||||||
|
}
|
||||||
|
|
||||||
|
return usersettings, nil
|
||||||
|
}
|
||||||
61
pkg/es/index_template_settings.go
Normal file
61
pkg/es/index_template_settings.go
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
package es
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"codeberg.org/scip/esctl/pkg/cfg"
|
||||||
|
"codeberg.org/scip/esctl/pkg/printer"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TplTemplateData struct {
|
||||||
|
Settings map[string]any `json:"settings"`
|
||||||
|
Aliases map[string]any `json:"aliases"`
|
||||||
|
Lifecycle map[string]any `json:"lifecycle"`
|
||||||
|
Mappings map[string]any `json:"mappings"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TplTemplate struct {
|
||||||
|
Template TplTemplateData `json:"template"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TplTpl struct {
|
||||||
|
IndexTemplate TplTemplate `json:"index_template"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Tpl struct {
|
||||||
|
IndexTemplates []TplTpl `json:"index_templates"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func getIndexTemplateSettings(conf *cfg.Config, tplname string, table *printer.Table) error {
|
||||||
|
raw, err := CallAPI(conf, "GET", "/_index_template/"+tplname, "")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
data := Tpl{}
|
||||||
|
if err = json.Unmarshal(raw, &data); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if conf.Debug {
|
||||||
|
if err := prettyfiJson(conf, raw); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(data.IndexTemplates) == 0 {
|
||||||
|
return fmt.Errorf("index_template %s not found", tplname)
|
||||||
|
}
|
||||||
|
|
||||||
|
tpl := data.IndexTemplates[0].IndexTemplate.Template.Settings
|
||||||
|
for topic, val := range tpl {
|
||||||
|
paths := getJsonPath(map[string]string{}, val.(map[string]any), topic)
|
||||||
|
|
||||||
|
for setting, value := range paths {
|
||||||
|
table.Entries = append(table.Entries, []string{setting, fmt.Sprintf("%v", value)})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
224
pkg/es/repl.go
224
pkg/es/repl.go
@@ -1,224 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright © 2026 Thomas von Dein
|
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU General Public License as published by
|
|
||||||
the Free Software Foundation, either version 3 of the License, or
|
|
||||||
(at your option) any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License
|
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
package es
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"crypto/tls"
|
|
||||||
"encoding/base64"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"slices"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"codeberg.org/scip/esctl/pkg/cfg"
|
|
||||||
"github.com/chzyer/readline"
|
|
||||||
)
|
|
||||||
|
|
||||||
const intro = `Input format: verb path [data]"
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
post /yourindex/_ccr/pause_follow
|
|
||||||
put /yourindex/_settings {"number_of_replicas": 1}
|
|
||||||
|
|
||||||
You can also put multiline JSON after the path like:
|
|
||||||
|
|
||||||
put /yourindex/_settings
|
|
||||||
{
|
|
||||||
"number_of_replicas": 1
|
|
||||||
}
|
|
||||||
|
|
||||||
If you do NOT supply a JSON in the first line, you need to hit ENTER
|
|
||||||
twice to complete.`
|
|
||||||
|
|
||||||
func Repl(conf *cfg.Config) error {
|
|
||||||
verbs := []string{"post", "get", "put", "delete"}
|
|
||||||
|
|
||||||
fmt.Println(intro)
|
|
||||||
fmt.Println()
|
|
||||||
|
|
||||||
reader, err := readline.NewEx(&readline.Config{
|
|
||||||
Prompt: "> ",
|
|
||||||
HistoryFile: os.Getenv("HOME") + "/.config/esctl/history",
|
|
||||||
HistoryLimit: 500,
|
|
||||||
InterruptPrompt: "^C",
|
|
||||||
EOFPrompt: "exit",
|
|
||||||
HistorySearchFold: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to initialize readline lib: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for {
|
|
||||||
text, err := reader.Readline()
|
|
||||||
if err != nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
text = strings.TrimSpace(text)
|
|
||||||
|
|
||||||
if text == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
parts := strings.SplitN(strings.TrimSpace(text), " ", 3)
|
|
||||||
if len(parts) < 2 {
|
|
||||||
fmt.Println("error: you need to input a verb, uri [and post data]")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if !slices.Contains(verbs, strings.ToLower(parts[0])) {
|
|
||||||
fmt.Println("error: verb must be one of " + strings.Join(verbs, ","))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if !strings.HasPrefix(parts[1], "/") {
|
|
||||||
parts[1] = "/" + parts[1]
|
|
||||||
}
|
|
||||||
|
|
||||||
json := ""
|
|
||||||
if len(parts) == 3 {
|
|
||||||
// put /uri {json}
|
|
||||||
json = parts[2]
|
|
||||||
}
|
|
||||||
|
|
||||||
// put /uri/<Ret> [json]
|
|
||||||
data, err := readJSON(json)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = CallAPI(conf, parts[0], parts[1], data)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("failed to call API: %s\n", esErrorString(err))
|
|
||||||
}
|
|
||||||
|
|
||||||
reader.SetPrompt("> ")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func encodeAuth(username, password string) string {
|
|
||||||
return base64.StdEncoding.EncodeToString([]byte(username + ":" + password))
|
|
||||||
}
|
|
||||||
|
|
||||||
func CallAPI(conf *cfg.Config, verb, path, data string) error {
|
|
||||||
verb = strings.ToUpper(verb)
|
|
||||||
|
|
||||||
// we're using port-forwards anyway
|
|
||||||
tr := &http.Transport{
|
|
||||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
|
||||||
}
|
|
||||||
|
|
||||||
client := &http.Client{Transport: tr}
|
|
||||||
|
|
||||||
req, err := http.NewRequest(verb, conf.DefaultCluster.Uri+path, bytes.NewBuffer([]byte(data)))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
req.Header.Add("Content-Type", "application/json")
|
|
||||||
req.Header.Add("accept", "application/json")
|
|
||||||
req.Header.Add("Authorization", "Basic "+encodeAuth(conf.DefaultCluster.User, conf.DefaultCluster.Pass))
|
|
||||||
|
|
||||||
// actually execute the request
|
|
||||||
resp, err := client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read and print response
|
|
||||||
body, err := io.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to read response body: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return prettyfiJson(conf, body)
|
|
||||||
}
|
|
||||||
|
|
||||||
func prettyfiJson(conf *cfg.Config, raw []byte) error {
|
|
||||||
if conf.HaveJQ {
|
|
||||||
cmd := exec.CommandContext(context.Background(), "jq", "-C")
|
|
||||||
cmd.Stdin = bytes.NewReader(raw)
|
|
||||||
|
|
||||||
var out bytes.Buffer
|
|
||||||
cmd.Stdout = &out
|
|
||||||
|
|
||||||
err := cmd.Run()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Println(out.String())
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var pretty bytes.Buffer
|
|
||||||
err := json.Indent(&pretty, raw, "", "\t")
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("json parse error: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Println(pretty.String())
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// interactively read arbitrary JSON data from STDIN, which is
|
|
||||||
// virtually a repl inside the primary repl
|
|
||||||
func readJSON(input string) (string, error) {
|
|
||||||
data := ""
|
|
||||||
|
|
||||||
if input != "" {
|
|
||||||
data = input
|
|
||||||
} else {
|
|
||||||
scanner := bufio.NewScanner(os.Stdin)
|
|
||||||
for scanner.Scan() {
|
|
||||||
line := strings.TrimSpace(scanner.Text())
|
|
||||||
|
|
||||||
if line == "" {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
data += line
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if data == "" {
|
|
||||||
return data, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// validate
|
|
||||||
check := map[string]any{}
|
|
||||||
err := json.Unmarshal([]byte(data), &check)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("error: input data is not proper JSON: %s", err)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
return data, nil
|
|
||||||
}
|
|
||||||
@@ -27,6 +27,23 @@ import (
|
|||||||
"github.com/elastic/go-elasticsearch/v9/typedapi/types"
|
"github.com/elastic/go-elasticsearch/v9/typedapi/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func RoleNames(conf *cfg.Config) ([]string, error) {
|
||||||
|
res, err := conf.DefaultCluster.ES.Security.GetRole().
|
||||||
|
Do(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get roles: %s", esErrorString(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
res, err := conf.DefaultCluster.ES.Security.GetRole().
|
res, err := conf.DefaultCluster.ES.Security.GetRole().
|
||||||
Do(context.Background())
|
Do(context.Background())
|
||||||
@@ -50,11 +67,7 @@ func RoleList(conf *cfg.Config) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
table.Sort()
|
table.Sort()
|
||||||
if err := table.Print(); err != nil {
|
return table.Print()
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func RoleShow(conf *cfg.Config, rolename string) error {
|
func RoleShow(conf *cfg.Config, rolename string) error {
|
||||||
|
|||||||
354
pkg/es/role_diff.go
Normal file
354
pkg/es/role_diff.go
Normal file
@@ -0,0 +1,354 @@
|
|||||||
|
/*
|
||||||
|
Copyright © 2026 Thomas von Dein
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package es
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/csv"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"log/slog"
|
||||||
|
"os"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"codeberg.org/scip/esctl/pkg/cfg"
|
||||||
|
"codeberg.org/scip/esctl/pkg/printer"
|
||||||
|
"github.com/alecthomas/repr"
|
||||||
|
"github.com/elastic/go-elasticsearch/v9/typedapi/security/getrole"
|
||||||
|
)
|
||||||
|
|
||||||
|
// use static csv record positions as const vars so we can modify it
|
||||||
|
// if the csv format ever changes
|
||||||
|
const (
|
||||||
|
Rindexname = iota
|
||||||
|
Rrole
|
||||||
|
Rindexprivilege
|
||||||
|
Rclusterprivilege
|
||||||
|
Radgroup
|
||||||
|
Rspace
|
||||||
|
Rretention
|
||||||
|
Rkibanaprivilege
|
||||||
|
Rfieldprivilege
|
||||||
|
)
|
||||||
|
|
||||||
|
type Record struct {
|
||||||
|
// filled from CSV input
|
||||||
|
index_name string
|
||||||
|
role string
|
||||||
|
index_privilege string
|
||||||
|
cluster_privilege []string
|
||||||
|
ad_group []string
|
||||||
|
space string
|
||||||
|
retention string
|
||||||
|
kibana_privilege string
|
||||||
|
field_privilege string
|
||||||
|
|
||||||
|
// set by ourselfes
|
||||||
|
defined bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type Register struct {
|
||||||
|
name string
|
||||||
|
deployed, defined bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// generic variant, we do not account for multiple rows of the same
|
||||||
|
// role, in such cases an entry will simply overwritten. Use
|
||||||
|
// getCsvRecord() for a single role.
|
||||||
|
func getCsvRecords(conf *cfg.Config, csvfile string) (map[string]Record, error) {
|
||||||
|
data, err := os.ReadFile(csvfile)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read CSV file: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
csvreader := csv.NewReader(bytes.NewReader(data))
|
||||||
|
csvreader.Comma = rune(conf.Separator[0])
|
||||||
|
csvreader.Comment = '#'
|
||||||
|
csvreader.TrimLeadingSpace = true
|
||||||
|
|
||||||
|
rows, err := csvreader.ReadAll()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse CSV: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
records := make(map[string]Record, len(rows)-1)
|
||||||
|
|
||||||
|
for idx, row := range rows {
|
||||||
|
if idx == 0 {
|
||||||
|
continue // header
|
||||||
|
}
|
||||||
|
|
||||||
|
records[row[1]] = Record{
|
||||||
|
index_name: row[Rindexname],
|
||||||
|
role: row[Rrole],
|
||||||
|
index_privilege: row[Rindexprivilege],
|
||||||
|
cluster_privilege: []string{row[Rindexprivilege]},
|
||||||
|
ad_group: []string{row[Rclusterprivilege]},
|
||||||
|
space: row[Rspace],
|
||||||
|
retention: row[Rretention],
|
||||||
|
kibana_privilege: row[Rkibanaprivilege],
|
||||||
|
field_privilege: row[Rfieldprivilege],
|
||||||
|
defined: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return records, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// same thing as above but for one specific role. supports multiple
|
||||||
|
// rows of the same record with different values which will be
|
||||||
|
// combined.
|
||||||
|
func getCsvRecord(conf *cfg.Config, csvfile, rolename string) (*Record, error) {
|
||||||
|
fd, err := os.Open(csvfile)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to open CSV file: %s", err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if err := fd.Close(); err != nil {
|
||||||
|
log.Fatalf("failed to close file: %s", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
scanner := bufio.NewScanner(fd)
|
||||||
|
record := Record{role: rolename}
|
||||||
|
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := strings.TrimSpace(scanner.Text())
|
||||||
|
if strings.HasPrefix(line, "#") || line == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
row := strings.Split(line, conf.Separator)
|
||||||
|
|
||||||
|
if row[Rrole] == rolename {
|
||||||
|
record.index_name = row[Rindexname]
|
||||||
|
record.index_privilege = row[Rindexprivilege]
|
||||||
|
record.cluster_privilege = strings.Split(row[Rclusterprivilege], ",")
|
||||||
|
record.ad_group = append(record.ad_group, row[Radgroup])
|
||||||
|
record.space = row[Rspace]
|
||||||
|
record.retention = row[Rretention]
|
||||||
|
record.kibana_privilege = row[Rkibanaprivilege]
|
||||||
|
record.field_privilege = row[Rfieldprivilege]
|
||||||
|
record.defined = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &record, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func diffRoles(conf *cfg.Config, records map[string]Record, res getrole.Response) []Register {
|
||||||
|
rows := []Register{}
|
||||||
|
filtered := []Register{}
|
||||||
|
deployed := map[string]int{}
|
||||||
|
|
||||||
|
// iterate over deployed roles
|
||||||
|
for name := range res {
|
||||||
|
reg := Register{name: name}
|
||||||
|
|
||||||
|
_, defined := records[name]
|
||||||
|
if defined {
|
||||||
|
reg.deployed = true
|
||||||
|
reg.defined = true
|
||||||
|
} else {
|
||||||
|
reg.deployed = true
|
||||||
|
reg.defined = false
|
||||||
|
}
|
||||||
|
|
||||||
|
deployed[name] = 1
|
||||||
|
|
||||||
|
rows = append(rows, reg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// iterate over records from CSV and register only those which are not deployed
|
||||||
|
for name := range records {
|
||||||
|
reg := Register{name: name, defined: true}
|
||||||
|
_, deployed := deployed[name]
|
||||||
|
if !deployed {
|
||||||
|
rows = append(rows, reg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, reg := range rows {
|
||||||
|
if (conf.NotDeployed && !reg.deployed) ||
|
||||||
|
(conf.Undefined && !reg.defined) ||
|
||||||
|
(conf.Diff && reg.deployed != reg.defined) ||
|
||||||
|
(!conf.Undefined && !conf.NotDeployed && !conf.Diff) {
|
||||||
|
filtered = append(filtered, reg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return filtered
|
||||||
|
}
|
||||||
|
|
||||||
|
func RoleDiff(conf *cfg.Config, csvfile, role string) error {
|
||||||
|
records, err := getCsvRecords(conf, csvfile)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if role != "" {
|
||||||
|
return RoleDiffSingle(conf, csvfile, role)
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := conf.DefaultCluster.ES.Security.GetRole().
|
||||||
|
Do(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get roles: %s", esErrorString(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
rows := diffRoles(conf, records, res)
|
||||||
|
|
||||||
|
table := printer.NewTable(conf, 3, len(rows))
|
||||||
|
table.Addheaders("role", "is deployed", "is defined")
|
||||||
|
|
||||||
|
for idx, row := range rows {
|
||||||
|
deployed := printer.Colorize(conf, "green", "deployed")
|
||||||
|
if !row.deployed {
|
||||||
|
deployed = printer.Colorize(conf, "red", "not deployed")
|
||||||
|
}
|
||||||
|
|
||||||
|
defined := printer.Colorize(conf, "green", "defined")
|
||||||
|
if !row.defined {
|
||||||
|
defined = printer.Colorize(conf, "red", "undefined")
|
||||||
|
}
|
||||||
|
|
||||||
|
table.Entries[idx] = []string{
|
||||||
|
row.name,
|
||||||
|
deployed,
|
||||||
|
defined,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
table.Sort()
|
||||||
|
|
||||||
|
return table.Print()
|
||||||
|
}
|
||||||
|
|
||||||
|
func getRoleMappingGroups(conf *cfg.Config, rolename string) ([]string, error) {
|
||||||
|
mappings, err := conf.DefaultCluster.ES.Security.GetRoleMapping().
|
||||||
|
Do(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get role mappings: %s", esErrorString(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
groups := []string{}
|
||||||
|
for _, mapping := range mappings {
|
||||||
|
if slices.Contains(mapping.Roles, rolename) {
|
||||||
|
for _, rule := range mapping.Rules.Any {
|
||||||
|
for _, group := range rule.Field["groups"] {
|
||||||
|
groups = append(groups, group.(string))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return groups, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func compareSlices(name string, a, b []string) {
|
||||||
|
slices.Sort(a)
|
||||||
|
slices.Sort(b)
|
||||||
|
|
||||||
|
if slices.Compare(a, b) != 0 {
|
||||||
|
fmt.Printf("%s differs:\ndeployed: %s\n csv: %s\n",
|
||||||
|
name, strings.Join(a, ","), strings.Join(b, ","))
|
||||||
|
} else {
|
||||||
|
fmt.Printf("deployed %s matches csv definition\n", name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func RoleDiffSingle(conf *cfg.Config, csvfile, rolename string) error {
|
||||||
|
res, err := conf.DefaultCluster.ES.Security.GetRole().
|
||||||
|
Name(rolename).
|
||||||
|
Do(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get role: %s", esErrorString(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
record, err := getCsvRecord(conf, csvfile, rolename)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !record.defined {
|
||||||
|
fmt.Printf("role %s is not defined\n", rolename)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
role, exists := res[rolename]
|
||||||
|
if !exists {
|
||||||
|
fmt.Printf("role %s is not deployed\n", rolename)
|
||||||
|
return nil
|
||||||
|
} else {
|
||||||
|
fmt.Printf("role %s is deployed\n", rolename)
|
||||||
|
}
|
||||||
|
slog.Debug("found role", "role", role)
|
||||||
|
|
||||||
|
if conf.Debug {
|
||||||
|
// slog.Debug doesn't print it, for whatever reason
|
||||||
|
repr.Println(record)
|
||||||
|
}
|
||||||
|
|
||||||
|
groups, err := getRoleMappingGroups(conf, rolename)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
slog.Debug("group mappings", "groups", groups)
|
||||||
|
|
||||||
|
// check cluster setting
|
||||||
|
clusters := []string{}
|
||||||
|
for _, cluster := range role.Cluster {
|
||||||
|
clusters = append(clusters, cluster.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// check index names+privs
|
||||||
|
indices := []string{}
|
||||||
|
privs := []string{}
|
||||||
|
for _, index := range role.Indices {
|
||||||
|
for _, name := range index.Names {
|
||||||
|
indices = append(indices, strings.ReplaceAll(name, "**", "*"))
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, priv := range index.Privileges {
|
||||||
|
privs = append(privs, priv.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// check kibana application space
|
||||||
|
spaces := []string{}
|
||||||
|
for _, app := range role.Applications {
|
||||||
|
for _, resource := range app.Resources {
|
||||||
|
if strings.Contains(resource, "space:") {
|
||||||
|
parts := strings.Split(resource, ":")
|
||||||
|
if len(parts) == 2 {
|
||||||
|
spaces = append(spaces, parts[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
compareSlices("cluster_privilege", clusters, record.cluster_privilege)
|
||||||
|
compareSlices("ad_group", groups, record.ad_group)
|
||||||
|
compareSlices("index_name", indices, []string{record.index_name})
|
||||||
|
compareSlices("index_privilege", privs, []string{record.index_privilege})
|
||||||
|
compareSlices("space", spaces, []string{record.space})
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
65
pkg/es/rollover.go
Normal file
65
pkg/es/rollover.go
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
/*
|
||||||
|
Copyright © 2026 Thomas von Dein
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package es
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"codeberg.org/scip/esctl/pkg/cfg"
|
||||||
|
"github.com/elastic/go-elasticsearch/v9/typedapi/esdsl"
|
||||||
|
"github.com/elastic/go-elasticsearch/v9/typedapi/indices/rollover"
|
||||||
|
"github.com/elastic/go-elasticsearch/v9/typedapi/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
func RolloverConditions(conf *cfg.Config) types.RolloverConditionsVariant {
|
||||||
|
cond := esdsl.NewRolloverConditions()
|
||||||
|
|
||||||
|
if conf.MaxAge != "" {
|
||||||
|
cond.MaxAge(esdsl.NewDuration().String(conf.MaxAge))
|
||||||
|
}
|
||||||
|
if conf.MaxDocs > 0 {
|
||||||
|
cond.MaxDocs(int64(conf.MaxDocs))
|
||||||
|
}
|
||||||
|
if conf.MaxShardSize > 0 {
|
||||||
|
bs := esdsl.NewByteSize()
|
||||||
|
bs.Int64(int64(conf.MaxShardSize))
|
||||||
|
cond.MaxPrimaryShardSize(bs)
|
||||||
|
}
|
||||||
|
if conf.MaxShardDocs > 0 {
|
||||||
|
cond.MaxPrimaryShardDocs(int64(conf.MaxShardDocs))
|
||||||
|
}
|
||||||
|
|
||||||
|
return cond
|
||||||
|
}
|
||||||
|
|
||||||
|
func RolloverAlias(conf *cfg.Config, alias string) (*rollover.Response, error) {
|
||||||
|
roll := conf.DefaultCluster.ES.Indices.Rollover(alias)
|
||||||
|
|
||||||
|
if conf.Wait {
|
||||||
|
roll.WaitForActiveShards("all")
|
||||||
|
}
|
||||||
|
|
||||||
|
if conf.DryRun {
|
||||||
|
roll.DryRun(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
cond := RolloverConditions(conf)
|
||||||
|
|
||||||
|
roll.Conditions(cond)
|
||||||
|
|
||||||
|
return roll.Do(context.Background())
|
||||||
|
}
|
||||||
@@ -18,6 +18,7 @@ package es
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
@@ -28,8 +29,10 @@ import (
|
|||||||
"github.com/alecthomas/repr"
|
"github.com/alecthomas/repr"
|
||||||
"github.com/elastic/go-elasticsearch/v9/typedapi/core/search"
|
"github.com/elastic/go-elasticsearch/v9/typedapi/core/search"
|
||||||
"github.com/elastic/go-elasticsearch/v9/typedapi/esdsl"
|
"github.com/elastic/go-elasticsearch/v9/typedapi/esdsl"
|
||||||
|
"github.com/elastic/go-elasticsearch/v9/typedapi/indices/validatequery"
|
||||||
"github.com/elastic/go-elasticsearch/v9/typedapi/types"
|
"github.com/elastic/go-elasticsearch/v9/typedapi/types"
|
||||||
"github.com/elastic/go-elasticsearch/v9/typedapi/types/enums/sortorder"
|
"github.com/elastic/go-elasticsearch/v9/typedapi/types/enums/sortorder"
|
||||||
|
"github.com/tidwall/gjson"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -43,8 +46,11 @@ Execute an ES search.
|
|||||||
additional filters can be given as -F key=value
|
additional filters can be given as -F key=value
|
||||||
*/
|
*/
|
||||||
func Search(conf *cfg.Config, queries []string) error {
|
func Search(conf *cfg.Config, queries []string) error {
|
||||||
searchEs := conf.DefaultCluster.ES.Search().
|
if conf.Validate {
|
||||||
Index(conf.Index)
|
return validateSearch(conf, queries)
|
||||||
|
}
|
||||||
|
|
||||||
|
searchEs := conf.DefaultCluster.ES.Search().Index(conf.Index)
|
||||||
|
|
||||||
queryCaster, err := prepareQuery(conf, queries)
|
queryCaster, err := prepareQuery(conf, queries)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -57,9 +63,11 @@ func Search(conf *cfg.Config, queries []string) error {
|
|||||||
|
|
||||||
searchEs = addSort(conf, searchEs)
|
searchEs = addSort(conf, searchEs)
|
||||||
|
|
||||||
switch conf.Tail {
|
switch {
|
||||||
case true:
|
case conf.Tail:
|
||||||
return searchTail(conf, searchEs)
|
return searchTail(conf, searchEs)
|
||||||
|
case conf.Explain:
|
||||||
|
return explainSearch(conf, searchEs)
|
||||||
default:
|
default:
|
||||||
if conf.To > MAXPAGE {
|
if conf.To > MAXPAGE {
|
||||||
return searchPit(conf, req)
|
return searchPit(conf, req)
|
||||||
@@ -69,6 +77,78 @@ func Search(conf *cfg.Config, queries []string) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func explainSearch(conf *cfg.Config, search *search.Search) error {
|
||||||
|
res, err := search.
|
||||||
|
Explain(true).
|
||||||
|
Size(1). // one's enough for explain
|
||||||
|
Do(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to call explain search (esdsl): %s", esErrorString(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
if conf.Debug {
|
||||||
|
raw, err := json.Marshal(res)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to marshal explain result: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
value := gjson.Get(string(raw), "hits.hits.0._explanation")
|
||||||
|
fmt.Println(value.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(res.Hits.Hits) > 0 {
|
||||||
|
ex := res.Hits.Hits[0].Explanation_
|
||||||
|
fmt.Println(ex.Description)
|
||||||
|
fmt.Println(ex.Value)
|
||||||
|
|
||||||
|
// recurse into explanation details (it's a tree)
|
||||||
|
for _, ex := range ex.Details {
|
||||||
|
explain(&ex, " ")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func explain(res *types.ExplanationDetail, indent string) {
|
||||||
|
fmt.Println(indent + "- " + res.Description)
|
||||||
|
for _, ex := range res.Details {
|
||||||
|
fmt.Println(indent + " - " + ex.Description)
|
||||||
|
fmt.Println(indent + fmt.Sprintf(" score: %f", res.Value))
|
||||||
|
|
||||||
|
explain(&ex, indent+" ")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateSearch(conf *cfg.Config, queries []string) error {
|
||||||
|
validate := conf.DefaultCluster.ES.Indices.ValidateQuery()
|
||||||
|
|
||||||
|
queryCaster, err := prepareQuery(conf, queries)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
req := &validatequery.Request{Query: queryCaster}
|
||||||
|
|
||||||
|
validate.Request(req)
|
||||||
|
|
||||||
|
res, err := validate.
|
||||||
|
Do(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to validate search (esdsl): %s", esErrorString(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.Debug("ES result", "search", res)
|
||||||
|
|
||||||
|
if res.Valid {
|
||||||
|
fmt.Println(printer.Colorize(conf, "green", "valid"))
|
||||||
|
} else {
|
||||||
|
fmt.Println(printer.Colorize(conf, "red", "invalid"))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func Debug(conf *cfg.Config) error {
|
func Debug(conf *cfg.Config) error {
|
||||||
res, err := conf.DefaultCluster.ES.Search().
|
res, err := conf.DefaultCluster.ES.Search().
|
||||||
Index(conf.Index).
|
Index(conf.Index).
|
||||||
@@ -99,9 +179,7 @@ func searchOnce(conf *cfg.Config, search *search.Search) error {
|
|||||||
|
|
||||||
slog.Debug("ES result", "search", res)
|
slog.Debug("ES result", "search", res)
|
||||||
|
|
||||||
for _, hit := range res.Hits.Hits {
|
printer.PrintDocs(conf, res.Hits.Hits)
|
||||||
printer.PrintDoc(conf, hit)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -141,9 +219,7 @@ func searchPit(conf *cfg.Config, req *search.Request) error {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, hit := range res.Hits.Hits {
|
printer.PrintDocs(conf, res.Hits.Hits)
|
||||||
printer.PrintDoc(conf, hit)
|
|
||||||
}
|
|
||||||
|
|
||||||
last := res.Hits.Hits[len(res.Hits.Hits)-1]
|
last := res.Hits.Hits[len(res.Hits.Hits)-1]
|
||||||
search = search.SearchAfterValues(last.Sort)
|
search = search.SearchAfterValues(last.Sort)
|
||||||
@@ -178,6 +254,7 @@ func searchTail(conf *cfg.Config, search *search.Search) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
printer.PrintDoc(conf, hit)
|
printer.PrintDoc(conf, hit)
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
docs[*hit.Id_] = 1
|
docs[*hit.Id_] = 1
|
||||||
}
|
}
|
||||||
|
|||||||
79
pkg/es/task.go
Normal file
79
pkg/es/task.go
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
/*
|
||||||
|
Copyright © 2026 Thomas von Dein
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package es
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"codeberg.org/scip/esctl/pkg/cfg"
|
||||||
|
"codeberg.org/scip/esctl/pkg/printer"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TaskList(conf *cfg.Config) error {
|
||||||
|
res, err := conf.DefaultCluster.ES.Cat.Tasks().
|
||||||
|
Header("content-type", "application/json").
|
||||||
|
Header("accept", "application/json").
|
||||||
|
Do(context.Background())
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get index templates: %s", esErrorString(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.Debug("res", "tasks", res)
|
||||||
|
|
||||||
|
table := printer.NewTable(conf, 8, len(res))
|
||||||
|
table.Addheaders("task id", "action", "start time", "run time", "node", "type")
|
||||||
|
for idx, task := range res {
|
||||||
|
var ts time.Time
|
||||||
|
|
||||||
|
epc, err := strconv.Atoi(*task.StartTime)
|
||||||
|
if err != nil {
|
||||||
|
ts = time.Now()
|
||||||
|
} else {
|
||||||
|
ts = time.Unix(int64(epc/1000), 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
table.Entries[idx] = []string{
|
||||||
|
*task.TaskId,
|
||||||
|
*task.Action,
|
||||||
|
ts.Format("2006-01-02T15:04:05"),
|
||||||
|
*task.RunningTime,
|
||||||
|
*task.Node,
|
||||||
|
*task.Type,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return table.Print()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TaskCancel(conf *cfg.Config, taskid string) error {
|
||||||
|
_, err := conf.DefaultCluster.ES.Tasks.Cancel().
|
||||||
|
TaskId(taskid).
|
||||||
|
Header("content-type", "application/json").
|
||||||
|
Header("accept", "application/json").
|
||||||
|
Do(context.Background())
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to cancel task: %s", esErrorString(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
28
pkg/printer/builder.go
Normal file
28
pkg/printer/builder.go
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
/*
|
||||||
|
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 "strings"
|
||||||
|
|
||||||
|
// a wrapper around strings.Builder for convenience
|
||||||
|
type Builder struct {
|
||||||
|
strings.Builder
|
||||||
|
}
|
||||||
|
|
||||||
|
func (builder *Builder) WriteStringLine(in string) {
|
||||||
|
builder.WriteString(in + "\n")
|
||||||
|
}
|
||||||
@@ -18,12 +18,24 @@ package printer
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"slices"
|
||||||
|
|
||||||
"codeberg.org/scip/esctl/pkg/cfg"
|
"codeberg.org/scip/esctl/pkg/cfg"
|
||||||
"github.com/elastic/go-elasticsearch/v9/typedapi/types"
|
"github.com/elastic/go-elasticsearch/v9/typedapi/types"
|
||||||
"github.com/tidwall/gjson"
|
"github.com/tidwall/gjson"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func PrintDocs(conf *cfg.Config, hits []types.Hit) {
|
||||||
|
if !conf.Ascending {
|
||||||
|
slices.Reverse(hits)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, hit := range hits {
|
||||||
|
PrintDoc(conf, hit)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
func PrintDoc(conf *cfg.Config, hit types.Hit) {
|
func PrintDoc(conf *cfg.Config, hit types.Hit) {
|
||||||
var score types.Float64
|
var score types.Float64
|
||||||
if hit.Score_ != nil {
|
if hit.Score_ != nil {
|
||||||
@@ -40,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.Println(docjson)
|
fmt.Print(docjson)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
136
pkg/printer/pager.go
Normal file
136
pkg/printer/pager.go
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
/*
|
||||||
|
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
|
||||||
|
|
||||||
|
// pager setup using bubbletea
|
||||||
|
// file shamlelessly copied from:
|
||||||
|
// https://github.com/charmbracelet/bubbletea/tree/main/examples/pager
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/bubbles/viewport"
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
"github.com/charmbracelet/lipgloss"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
titleStyle = func() lipgloss.Style {
|
||||||
|
b := lipgloss.RoundedBorder()
|
||||||
|
b.Right = "├"
|
||||||
|
return lipgloss.NewStyle().BorderStyle(b).Padding(0, 1)
|
||||||
|
}()
|
||||||
|
|
||||||
|
infoStyle = func() lipgloss.Style {
|
||||||
|
b := lipgloss.RoundedBorder()
|
||||||
|
b.Left = "┤"
|
||||||
|
return titleStyle.BorderStyle(b)
|
||||||
|
}()
|
||||||
|
)
|
||||||
|
|
||||||
|
type model struct {
|
||||||
|
content string
|
||||||
|
title string
|
||||||
|
ready bool
|
||||||
|
viewport viewport.Model
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m model) Init() tea.Cmd {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
|
var (
|
||||||
|
cmd tea.Cmd
|
||||||
|
cmds []tea.Cmd
|
||||||
|
)
|
||||||
|
|
||||||
|
switch msg := msg.(type) {
|
||||||
|
case tea.KeyMsg:
|
||||||
|
if k := msg.String(); k == "ctrl+c" || k == "q" || k == "esc" {
|
||||||
|
return m, tea.Quit
|
||||||
|
}
|
||||||
|
|
||||||
|
case tea.WindowSizeMsg:
|
||||||
|
headerHeight := lipgloss.Height(m.headerView())
|
||||||
|
footerHeight := lipgloss.Height(m.footerView())
|
||||||
|
verticalMarginHeight := headerHeight + footerHeight
|
||||||
|
|
||||||
|
if !m.ready {
|
||||||
|
// Since this program is using the full size of the viewport we
|
||||||
|
// need to wait until we've received the window dimensions before
|
||||||
|
// we can initialize the viewport. The initial dimensions come in
|
||||||
|
// quickly, though asynchronously, which is why we wait for them
|
||||||
|
// here.
|
||||||
|
m.viewport = viewport.New(msg.Width, msg.Height-verticalMarginHeight)
|
||||||
|
m.viewport.YPosition = headerHeight
|
||||||
|
m.viewport.SetContent(m.content)
|
||||||
|
m.ready = true
|
||||||
|
} else {
|
||||||
|
m.viewport.Width = msg.Width
|
||||||
|
m.viewport.Height = msg.Height - verticalMarginHeight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle keyboard and mouse events in the viewport
|
||||||
|
m.viewport, cmd = m.viewport.Update(msg)
|
||||||
|
cmds = append(cmds, cmd)
|
||||||
|
|
||||||
|
return m, tea.Batch(cmds...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m model) View() string {
|
||||||
|
if !m.ready {
|
||||||
|
return "\n Initializing..."
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s\n%s\n%s", m.headerView(), m.viewport.View(), m.footerView())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m model) headerView() string {
|
||||||
|
// title := titleStyle.Render("RPN Help Overview")
|
||||||
|
title := titleStyle.Render(m.title)
|
||||||
|
line := strings.Repeat("─", max(0, m.viewport.Width-lipgloss.Width(title)))
|
||||||
|
return lipgloss.JoinHorizontal(lipgloss.Center, title, line)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m model) footerView() string {
|
||||||
|
info := infoStyle.Render(fmt.Sprintf("%3.f%%", m.viewport.ScrollPercent()*100))
|
||||||
|
line := strings.Repeat("─", max(0, m.viewport.Width-lipgloss.Width(info)))
|
||||||
|
return lipgloss.JoinHorizontal(lipgloss.Center, line, info)
|
||||||
|
}
|
||||||
|
|
||||||
|
func max(a, b int) int {
|
||||||
|
if a > b {
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
func Pager(title, message string) {
|
||||||
|
p := tea.NewProgram(
|
||||||
|
model{content: message, title: title},
|
||||||
|
tea.WithAltScreen(), // use the full size of the terminal in its "alternate screen buffer"
|
||||||
|
tea.WithMouseCellMotion(), // turn on mouse support so we can track the mouse wheel
|
||||||
|
)
|
||||||
|
|
||||||
|
if _, err := p.Run(); err != nil {
|
||||||
|
fmt.Println("could not run pager:", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -292,6 +292,10 @@ func (data *Table) Addheaders(headers ...string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (data *Table) AddRow(fields ...string) {
|
||||||
|
data.Entries = append(data.Entries, fields)
|
||||||
|
}
|
||||||
|
|
||||||
// return the length of a string but only visible chars, w/o ansi color escapes
|
// return the length of a string but only visible chars, w/o ansi color escapes
|
||||||
func visibleLen(word string) int {
|
func visibleLen(word string) int {
|
||||||
return len(ansiCtrlSeq.ReplaceAllLiteralString(word, ""))
|
return len(ansiCtrlSeq.ReplaceAllLiteralString(word, ""))
|
||||||
|
|||||||
Reference in New Issue
Block a user