mirror of
https://codeberg.org/scip/esctl.git
synced 2026-08-24 15:14:18 +02:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
38ed9de0b4 | ||
|
|
5e82a3706b | ||
| 0122009774 | |||
| 8109481e23 | |||
|
|
af75bfa192 |
42
.goreleaser.yaml
Normal file
42
.goreleaser.yaml
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
# vim: set ts=2 sw=2 tw=0 fo=cnqoj
|
||||||
|
|
||||||
|
version: 2
|
||||||
|
|
||||||
|
before:
|
||||||
|
hooks:
|
||||||
|
- go mod tidy
|
||||||
|
|
||||||
|
gitea_urls:
|
||||||
|
api: https://codeberg.org/api/v1
|
||||||
|
download: https://codeberg.org
|
||||||
|
|
||||||
|
builds:
|
||||||
|
- env:
|
||||||
|
- CGO_ENABLED=0
|
||||||
|
goos:
|
||||||
|
- linux
|
||||||
|
- darwin
|
||||||
|
|
||||||
|
changelog:
|
||||||
|
sort: asc
|
||||||
|
filters:
|
||||||
|
exclude:
|
||||||
|
- "^docs:"
|
||||||
|
- "^test:"
|
||||||
|
groups:
|
||||||
|
- title: Improved
|
||||||
|
regexp: '^.*?(feat|add|new)(\([[:word:]]+\))??!?:.+$'
|
||||||
|
order: 0
|
||||||
|
- title: Fixed
|
||||||
|
regexp: '^.*?(bug|fix)(\([[:word:]]+\))??!?:.+$'
|
||||||
|
order: 1
|
||||||
|
- title: Changed
|
||||||
|
order: 999
|
||||||
|
|
||||||
|
release:
|
||||||
|
header: "# Release Notes"
|
||||||
|
footer: >-
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Full Changelog: [{{ .PreviousTag }}...{{ .Tag }}](https://codeberg.org/scip/epuppy/compare/{{ .PreviousTag }}...{{ .Tag }})
|
||||||
26
.woodpecker/build.yaml
Normal file
26
.woodpecker/build.yaml
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
matrix:
|
||||||
|
platform:
|
||||||
|
- linux/amd64
|
||||||
|
goversion:
|
||||||
|
- 1.25
|
||||||
|
|
||||||
|
labels:
|
||||||
|
platform: ${platform}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
build:
|
||||||
|
when:
|
||||||
|
event: [push,manual]
|
||||||
|
image: golang:${goversion}
|
||||||
|
commands:
|
||||||
|
- go get
|
||||||
|
- go build
|
||||||
|
|
||||||
|
linter:
|
||||||
|
when:
|
||||||
|
event: [push,manual]
|
||||||
|
image: golang:${goversion}
|
||||||
|
commands:
|
||||||
|
- curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/HEAD/install.sh | sh -s -- -b $(go env GOPATH)/bin v2.5.0
|
||||||
|
- golangci-lint --version
|
||||||
|
- golangci-lint run ./...
|
||||||
15
.woodpecker/release.yaml
Normal file
15
.woodpecker/release.yaml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# build release
|
||||||
|
|
||||||
|
labels:
|
||||||
|
platform: linux/amd64
|
||||||
|
|
||||||
|
steps:
|
||||||
|
goreleaser:
|
||||||
|
image: goreleaser/goreleaser
|
||||||
|
when:
|
||||||
|
event: [tag,manual]
|
||||||
|
environment:
|
||||||
|
GITEA_TOKEN:
|
||||||
|
from_secret: DEPLOY_TOKEN
|
||||||
|
commands:
|
||||||
|
- goreleaser release --clean --verbose
|
||||||
18
CODE_OF_CONDUCT.md
Normal file
18
CODE_OF_CONDUCT.md
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# CODE_OF_CONDUCT.md (compact model)
|
||||||
|
|
||||||
|
Purpose: Maintain a collaborative, harassment-free environment focused
|
||||||
|
on shipping quality software.
|
||||||
|
|
||||||
|
Standards: Be respectful; assume good intent; no harassment; no
|
||||||
|
discrimination; keep critiques technical.
|
||||||
|
|
||||||
|
Scope: All project spaces (issues, PRs, forums, events).
|
||||||
|
|
||||||
|
Reporting: Email tom AT vondein DOT org. Acknowledge within 72 hours.
|
||||||
|
|
||||||
|
Enforcement: Two maintainers review, one recuses on
|
||||||
|
conflict. Sanctions range from warning to removal. Summary posted (no
|
||||||
|
personal details).
|
||||||
|
|
||||||
|
Escalation: If you believe maintainers handled a report in bad faith,
|
||||||
|
escalate to our foundation committee (link) for independent review.
|
||||||
91
CONTRIBUTING.md
Normal file
91
CONTRIBUTING.md
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
## Project Goals
|
||||||
|
|
||||||
|
The idea behind this project is to build a small commandline tool to
|
||||||
|
manage an ElasticSearch cluster.
|
||||||
|
|
||||||
|
There will be no GUI, no web interface, no public API of some
|
||||||
|
sort. TUI subcommands maybe added.
|
||||||
|
|
||||||
|
The programming language used for this project will always be
|
||||||
|
[GOLANG](https://go.dev/) with the exception of the documentation
|
||||||
|
([Perl POD](https://perldoc.perl.org/perlpod)) and the Makefile.
|
||||||
|
|
||||||
|
# Contributing
|
||||||
|
|
||||||
|
You can contribute to this project in various ways:
|
||||||
|
|
||||||
|
## Open an issue
|
||||||
|
|
||||||
|
If you encounter a problem or don't understand how the program works
|
||||||
|
or if you think the documentation is unclear, please don't hesitate to
|
||||||
|
open an issue.
|
||||||
|
|
||||||
|
Please add as much information about the case as possible, such as:
|
||||||
|
|
||||||
|
- Your environment (operating system etc)
|
||||||
|
- program version
|
||||||
|
- Input data. Please replace sensitive information with mock data!
|
||||||
|
- Actual program output.
|
||||||
|
- Expected program output.
|
||||||
|
- Error message - if any.
|
||||||
|
|
||||||
|
Be aware that I am working on this (and some other) project in my
|
||||||
|
spare time which is scarce. Therefore please don't expect me to
|
||||||
|
respond to your query within hours or even days. Be patient, but I
|
||||||
|
WILL respond.
|
||||||
|
|
||||||
|
## Pull Requests
|
||||||
|
|
||||||
|
Code and documentation help is always much appreciated! Please follow
|
||||||
|
thes guidelines to successfully contribute:
|
||||||
|
|
||||||
|
- Every pull request shall be based on latest `development`
|
||||||
|
branch. `main` is only used for releases.
|
||||||
|
|
||||||
|
- Execute the unit tests before committing: `make test`. There shall
|
||||||
|
be no errors.
|
||||||
|
|
||||||
|
- Strive to be backwards compatible so that users who are already
|
||||||
|
using the program don't have to change their habits - unless it is
|
||||||
|
really neccessary.
|
||||||
|
|
||||||
|
- Try to add a unit test for your addition.
|
||||||
|
|
||||||
|
- Don't ever change existing unit tests!
|
||||||
|
|
||||||
|
- Add a meaningful and comprehensive rationale about your contribution:
|
||||||
|
- Why do you think it might be useful for others?
|
||||||
|
- What did you actually change or add?
|
||||||
|
- Is there an open issue which this PR fixes and if so, please link
|
||||||
|
to that issue.
|
||||||
|
|
||||||
|
- [Re-]format your code with `gofmt -s`.
|
||||||
|
|
||||||
|
- Avoid unneccesary dependencies, especially for very small functions.
|
||||||
|
|
||||||
|
- **If** a new dependency is being added, it must be compatible with
|
||||||
|
our [license agreement](LICENSE).
|
||||||
|
|
||||||
|
- You need to accept that the code or documentation you contribute
|
||||||
|
will be redistributed under the terms of said license agreement. If
|
||||||
|
your contribution is considerably large or if you contribute
|
||||||
|
regularly, then feel free to add your name and if you want your
|
||||||
|
email address to the *AUTHORS* section of the
|
||||||
|
manual page.
|
||||||
|
|
||||||
|
- Adhere to the above mentioned project goals.
|
||||||
|
|
||||||
|
- If you are unsure if your addition or change will be accepted,
|
||||||
|
better ask before starting coding. Open an issue about your proposal
|
||||||
|
and let's discuss it! That way we avoid doing unnessesary work on
|
||||||
|
both sides.
|
||||||
|
|
||||||
|
Each pull request will be carefully reviewed and if it is a useful
|
||||||
|
addition it will be accepted. However, please be prepared that
|
||||||
|
sometimes a PR will be rejected. The reasons may vary and will be
|
||||||
|
documented. Perhaps the above guidelines are not matched, or the
|
||||||
|
addition seems to be not so useful from my perspective, maybe there
|
||||||
|
are too much changes or there might be changes I don't even
|
||||||
|
understand.
|
||||||
|
|
||||||
|
But whatever happens: your contribution is always welcome!
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
[](https://ci.codeberg.org/repos/16999)
|
||||||
|
|
||||||
# esctl
|
# esctl
|
||||||
|
|
||||||
Elasticsearch CLI
|
Elasticsearch CLI
|
||||||
|
|||||||
2
TODO.md
2
TODO.md
@@ -3,3 +3,5 @@
|
|||||||
|
|
||||||
- Fix index names custom completion
|
- Fix index names custom completion
|
||||||
- add cluster default <name> which would add a flag to the config, so that no -C is needed subsequently
|
- add cluster default <name> which would add a flag to the config, so that no -C is needed subsequently
|
||||||
|
- add `cluster stats` from `/_cluster/stats` like mem, procs, open files, num indices, shards etc
|
||||||
|
or add these to `cluster status`, maybe add a `--stats` to include stats there?
|
||||||
|
|||||||
@@ -140,6 +140,27 @@ func ClusterSettingsList(conf *cfg.Config) *cli.Command {
|
|||||||
Usage: "show cluster settings",
|
Usage: "show cluster settings",
|
||||||
Aliases: []string{"ls", "get"},
|
Aliases: []string{"ls", "get"},
|
||||||
|
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: "persistent",
|
||||||
|
Usage: "only show persistent setting[s] (default)",
|
||||||
|
Destination: &conf.Persistent,
|
||||||
|
Aliases: []string{"p"},
|
||||||
|
},
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: "transient",
|
||||||
|
Usage: "only show transient setting[s]",
|
||||||
|
Destination: &conf.Transient,
|
||||||
|
Aliases: []string{"t"},
|
||||||
|
},
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: "default",
|
||||||
|
Usage: "only show default setting[s]",
|
||||||
|
Destination: &conf.Default,
|
||||||
|
Aliases: []string{"D"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||||
if err := es.ClusterSettingsList(conf); err != nil {
|
if err := es.ClusterSettingsList(conf); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||||||
package cfg
|
package cfg
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -30,7 +31,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
Version string = `v0.0.5`
|
Version string = `v0.0.6`
|
||||||
)
|
)
|
||||||
|
|
||||||
type Cluster struct {
|
type Cluster struct {
|
||||||
@@ -52,7 +53,7 @@ type Config struct {
|
|||||||
Filter []string // search: -F
|
Filter []string // search: -F
|
||||||
Exclude string // cluster compare: -e (regexp)
|
Exclude string // cluster compare: -e (regexp)
|
||||||
All bool // cluster status: -a
|
All bool // cluster status: -a
|
||||||
Persistent, Transient bool // -p -t cluster settings set
|
Persistent, Transient, Default bool // -p -t -D cluster settings set
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewConfig() *Config {
|
func NewConfig() *Config {
|
||||||
@@ -76,6 +77,10 @@ func (conf *Config) Init() error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := conf.SetupES(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
if conf.CurrentCluster != "" {
|
if conf.CurrentCluster != "" {
|
||||||
current, exists := conf.Clusters[conf.CurrentCluster]
|
current, exists := conf.Clusters[conf.CurrentCluster]
|
||||||
if !exists {
|
if !exists {
|
||||||
@@ -83,14 +88,29 @@ func (conf *Config) Init() error {
|
|||||||
} else {
|
} else {
|
||||||
conf.DefaultCluster = current
|
conf.DefaultCluster = current
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
if len(conf.Clusters) == 1 {
|
||||||
|
for _, cluster := range conf.Clusters {
|
||||||
|
conf.DefaultCluster = cluster
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for _, cluster := range conf.Clusters {
|
||||||
|
_, err := cluster.ES.Cluster.Health().
|
||||||
|
Header("content-type", "application/json").
|
||||||
|
Header("accept", "application/json").
|
||||||
|
Do(context.Background())
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
conf.DefaultCluster = cluster
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if conf.Debug {
|
if conf.Debug {
|
||||||
repr.Println(conf)
|
repr.Println(conf)
|
||||||
}
|
}
|
||||||
|
|
||||||
conf.SetupES()
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,8 +22,12 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"codeberg.org/scip/esctl/pkg/cfg"
|
"codeberg.org/scip/esctl/pkg/cfg"
|
||||||
|
"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/core/info"
|
||||||
"github.com/elastic/go-elasticsearch/v9/typedapi/types"
|
"github.com/elastic/go-elasticsearch/v9/typedapi/types"
|
||||||
"github.com/urfave/cli/v3"
|
"github.com/urfave/cli/v3"
|
||||||
)
|
)
|
||||||
@@ -32,8 +36,22 @@ const (
|
|||||||
DefaultExclude = `(part|monitoring|.internal|metrics-endpoint)`
|
DefaultExclude = `(part|monitoring|.internal|metrics-endpoint)`
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
ResponseHealth = iota
|
||||||
|
ResponseInfo
|
||||||
|
ResponseCcr
|
||||||
|
)
|
||||||
|
|
||||||
type ClusterIndices map[string]map[string]*types.IndicesRecord
|
type ClusterIndices map[string]map[string]*types.IndicesRecord
|
||||||
|
|
||||||
|
type apiResponse struct {
|
||||||
|
error error
|
||||||
|
info *info.Response
|
||||||
|
health *health.Response
|
||||||
|
ccr *stats.Response
|
||||||
|
which int
|
||||||
|
}
|
||||||
|
|
||||||
func ClusterCompare(conf *cfg.Config, leader, follower string) error {
|
func ClusterCompare(conf *cfg.Config, leader, follower string) error {
|
||||||
if !checkClusterFollower(conf, leader) {
|
if !checkClusterFollower(conf, leader) {
|
||||||
return errors.New("leader/follower attribution is invalid, reverse cluster attribution and retry")
|
return errors.New("leader/follower attribution is invalid, reverse cluster attribution and retry")
|
||||||
@@ -60,7 +78,7 @@ func ClusterCompare(conf *cfg.Config, leader, follower string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !checkClusterStatus(conf, leader, follower) {
|
if !checkClusterStatus(conf, leader, follower) {
|
||||||
return errors.New("One of the two clusters is in a failed state")
|
return errors.New("one of the two clusters is in a failed state")
|
||||||
}
|
}
|
||||||
|
|
||||||
findIlmErrors(conf, leader, follower)
|
findIlmErrors(conf, leader, follower)
|
||||||
@@ -75,26 +93,42 @@ func ClusterCompare(conf *cfg.Config, leader, follower string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func ClusterList(conf *cfg.Config) error {
|
func ClusterList(conf *cfg.Config) error {
|
||||||
table := NewTable(2, len(conf.Clusters))
|
table := NewTable(3, len(conf.Clusters))
|
||||||
table.Addheaders("cluster", "uri")
|
|
||||||
|
table.Addheaders("cluster", "uri", "default")
|
||||||
|
|
||||||
idx := 0
|
idx := 0
|
||||||
for name, cluster := range conf.Clusters {
|
for name, cluster := range conf.Clusters {
|
||||||
table.entries[idx] = []string{name, cluster.Uri}
|
current := name == "default" || name == conf.CurrentCluster
|
||||||
|
|
||||||
|
_, err := cluster.ES.Cluster.Health().
|
||||||
|
Header("content-type", "application/json").
|
||||||
|
Header("accept", "application/json").
|
||||||
|
Do(context.Background())
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
name = Colorize("green", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
table.entries[idx] = []string{name, cluster.Uri, fmt.Sprintf("%t", current)}
|
||||||
idx++
|
idx++
|
||||||
}
|
}
|
||||||
|
|
||||||
table.Sort()
|
table.Sort()
|
||||||
table.PrintMarkdown()
|
if err := table.PrintMarkdown(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// We're using goroutines here to parallelize API requests, since we
|
||||||
|
// 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{}
|
||||||
|
|
||||||
if conf.All {
|
if conf.All {
|
||||||
for key, _ := range conf.Clusters {
|
for key := range conf.Clusters {
|
||||||
clusters = append(clusters, key)
|
clusters = append(clusters, key)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -107,28 +141,65 @@ func ClusterStatus(conf *cfg.Config) error {
|
|||||||
es = conf.Clusters[cluster].ES
|
es = conf.Clusters[cluster].ES
|
||||||
}
|
}
|
||||||
|
|
||||||
res, err := es.Cluster.Health().
|
responses := make(chan apiResponse, 3)
|
||||||
Header("content-type", "application/json").
|
wg := &sync.WaitGroup{}
|
||||||
Header("accept", "application/json").
|
|
||||||
Do(context.Background())
|
wg.Add(3)
|
||||||
if err != nil {
|
go getClusterData(es, wg, responses, "health")
|
||||||
return fmt.Errorf("failed to getcluster health: %s", err)
|
go getClusterData(es, wg, responses, "info")
|
||||||
|
go getClusterData(es, wg, responses, "ccrstats")
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
var clusterhealth *health.Response
|
||||||
|
var info *info.Response
|
||||||
|
var ccrstats *stats.Response
|
||||||
|
|
||||||
|
for i := 0; i < 3; i++ {
|
||||||
|
r := <-responses
|
||||||
|
|
||||||
|
if r.error != nil {
|
||||||
|
return r.error
|
||||||
}
|
}
|
||||||
|
|
||||||
slog.Debug("ES result", "cluster health", res)
|
switch r.which {
|
||||||
|
case ResponseHealth:
|
||||||
|
clusterhealth = r.health
|
||||||
|
case ResponseCcr:
|
||||||
|
ccrstats = r.ccr
|
||||||
|
case ResponseInfo:
|
||||||
|
info = r.info
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.Debug("ES result", "cluster health", clusterhealth)
|
||||||
|
|
||||||
|
ccrfollowing := ""
|
||||||
|
if len(ccrstats.AutoFollowStats.AutoFollowedClusters) > 0 {
|
||||||
|
// is following another cluster
|
||||||
|
ccrfollowing = fmt.Sprintf("%s (%d/%d)",
|
||||||
|
ccrstats.AutoFollowStats.AutoFollowedClusters[0].ClusterName,
|
||||||
|
ccrstats.AutoFollowStats.NumberOfSuccessfulFollowIndices,
|
||||||
|
ccrstats.AutoFollowStats.NumberOfFailedFollowIndices,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
table := NewTable(2, 5)
|
table := NewTable(2, 5)
|
||||||
table.Addheaders(cluster, "status")
|
table.Addheaders(cluster, "status")
|
||||||
|
|
||||||
table.entries = [][]string{
|
table.entries = [][]string{
|
||||||
{"Cluster Name", Colorize(*&res.Status.Name, res.ClusterName)},
|
{"Cluster Name", Colorize(clusterhealth.Status.Name, clusterhealth.ClusterName)},
|
||||||
{"Active Shards", fmt.Sprintf("%d", res.ActiveShards)},
|
{"ES Version", info.Version.Int},
|
||||||
{"Active Primary Shards", fmt.Sprintf("%d", res.ActivePrimaryShards)},
|
{"Active Shards", fmt.Sprintf("%d", clusterhealth.ActiveShards)},
|
||||||
{"Indicies", fmt.Sprintf("%d", len(res.Indices))},
|
{"Active Primary Shards", fmt.Sprintf("%d", clusterhealth.ActivePrimaryShards)},
|
||||||
{"Nodes", fmt.Sprintf("%d", res.NumberOfNodes)},
|
{"Indicies", fmt.Sprintf("%d", len(clusterhealth.Indices))},
|
||||||
|
{"Nodes", fmt.Sprintf("%d", clusterhealth.NumberOfNodes)},
|
||||||
|
{"AutoFollow (success/failed indices)", ccrfollowing},
|
||||||
}
|
}
|
||||||
|
|
||||||
table.PrintMarkdown()
|
if err := table.PrintMarkdown(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -147,7 +218,16 @@ func ClusterSettingsList(conf *cfg.Config) error {
|
|||||||
table.Addheaders("setting", "value")
|
table.Addheaders("setting", "value")
|
||||||
entries := [][]string{}
|
entries := [][]string{}
|
||||||
|
|
||||||
for topic, val := range res.Persistent {
|
settingshash := res.Persistent // == map[string]json.RawMessage
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case conf.Transient:
|
||||||
|
settingshash = res.Transient
|
||||||
|
case conf.Default:
|
||||||
|
settingshash = res.Defaults
|
||||||
|
}
|
||||||
|
|
||||||
|
for topic, val := range settingshash {
|
||||||
data := map[string]map[string]any{}
|
data := map[string]map[string]any{}
|
||||||
|
|
||||||
err := json.Unmarshal(val, &data)
|
err := json.Unmarshal(val, &data)
|
||||||
@@ -168,9 +248,11 @@ func ClusterSettingsList(conf *cfg.Config) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
table.entries = entries
|
table.entries = entries
|
||||||
|
|
||||||
table.Sort()
|
table.Sort()
|
||||||
table.PrintMarkdown()
|
|
||||||
|
if err := table.PrintMarkdown(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,77 +18,18 @@ package es
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"codeberg.org/scip/esctl/pkg/cfg"
|
"codeberg.org/scip/esctl/pkg/cfg"
|
||||||
|
"github.com/elastic/go-elasticsearch/v9"
|
||||||
"github.com/elastic/go-elasticsearch/v9/typedapi/cluster/health"
|
"github.com/elastic/go-elasticsearch/v9/typedapi/cluster/health"
|
||||||
"github.com/elastic/go-elasticsearch/v9/typedapi/types"
|
"github.com/elastic/go-elasticsearch/v9/typedapi/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
// look for indicies only on leader
|
|
||||||
func findIndicesOnlyOnMaster(conf *cfg.Config, indices ClusterIndices, leader, follower string) {
|
|
||||||
exclude := regexp.MustCompile(DefaultExclude)
|
|
||||||
if conf.Exclude != "" {
|
|
||||||
exclude = regexp.MustCompile(conf.Exclude)
|
|
||||||
}
|
|
||||||
|
|
||||||
indexOnlyOnLeader := map[string]*types.IndicesRecord{}
|
|
||||||
for name, index := range indices[leader] {
|
|
||||||
if exclude.MatchString(name) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
_, followerHasIt := indices[follower][name]
|
|
||||||
if !followerHasIt {
|
|
||||||
// fetch index details
|
|
||||||
res, err := conf.Clusters[leader].ES.Indices.Get(name).
|
|
||||||
Header("content-type", "application/json").
|
|
||||||
Header("accept", "application/json").
|
|
||||||
Do(context.Background())
|
|
||||||
if err != nil {
|
|
||||||
continue // ignore it then
|
|
||||||
}
|
|
||||||
|
|
||||||
_, defined := res[name]
|
|
||||||
if !defined {
|
|
||||||
// json response map didn't contain the index
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
isWritable := false
|
|
||||||
for _, alias := range res[name].Aliases {
|
|
||||||
if *alias.IsWriteIndex {
|
|
||||||
isWritable = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if isWritable {
|
|
||||||
// ignore index if associated alias index is writing
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
indexOnlyOnLeader[name] = index
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
idx := 0
|
|
||||||
table := NewTable(3, len(indexOnlyOnLeader))
|
|
||||||
table.Addheaders("index only on leader", "size", "docscount")
|
|
||||||
|
|
||||||
for name, index := range indexOnlyOnLeader {
|
|
||||||
name := Colorize("red", name)
|
|
||||||
|
|
||||||
table.entries[idx] = []string{name, *index.DatasetSize, *index.DocsCount}
|
|
||||||
idx++
|
|
||||||
}
|
|
||||||
|
|
||||||
table.Sort()
|
|
||||||
table.PrintMarkdown()
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkClusterFollower(conf *cfg.Config, leader string) bool {
|
func checkClusterFollower(conf *cfg.Config, leader string) bool {
|
||||||
stats, err := conf.Clusters[leader].ES.Ccr.Stats().
|
stats, err := conf.Clusters[leader].ES.Ccr.Stats().
|
||||||
Header("content-type", "application/json").
|
Header("content-type", "application/json").
|
||||||
@@ -135,8 +76,8 @@ func checkClusterStatus(conf *cfg.Config, leader, follower string) bool {
|
|||||||
|
|
||||||
table.entries = [][]string{
|
table.entries = [][]string{
|
||||||
{"Cluster Name",
|
{"Cluster Name",
|
||||||
Colorize(*&status[leader].Status.Name, status[leader].ClusterName),
|
Colorize(status[leader].Status.Name, status[leader].ClusterName),
|
||||||
Colorize(*&status[follower].Status.Name, status[follower].ClusterName),
|
Colorize(status[follower].Status.Name, status[follower].ClusterName),
|
||||||
},
|
},
|
||||||
{"Active Shards",
|
{"Active Shards",
|
||||||
fmt.Sprintf("%d", status[leader].ActiveShards),
|
fmt.Sprintf("%d", status[leader].ActiveShards),
|
||||||
@@ -156,7 +97,10 @@ func checkClusterStatus(conf *cfg.Config, leader, follower string) bool {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
table.PrintMarkdown()
|
if err := table.PrintMarkdown(); err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
if status[leader].Status.Name == "green" && status[follower].Status.Name == "green" {
|
if status[leader].Status.Name == "green" && status[follower].Status.Name == "green" {
|
||||||
return true
|
return true
|
||||||
@@ -212,7 +156,11 @@ func findIlmErrors(conf *cfg.Config, leader, follower string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
table.Sort()
|
table.Sort()
|
||||||
table.PrintMarkdown()
|
|
||||||
|
if err := table.PrintMarkdown(); err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -282,7 +230,10 @@ func findIndicesOnlyOnLeader(conf *cfg.Config, indices ClusterIndices, leader, f
|
|||||||
}
|
}
|
||||||
|
|
||||||
table.Sort()
|
table.Sort()
|
||||||
table.PrintMarkdown()
|
if err := table.PrintMarkdown(); err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -329,7 +280,10 @@ func findOrphanedIndices(conf *cfg.Config, indices ClusterIndices, leader, follo
|
|||||||
}
|
}
|
||||||
|
|
||||||
table.Sort()
|
table.Sort()
|
||||||
table.PrintMarkdown()
|
if err := table.PrintMarkdown(); err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -360,7 +314,10 @@ func findFailedFollowerIndices(conf *cfg.Config, indices ClusterIndices, followe
|
|||||||
}
|
}
|
||||||
|
|
||||||
table.Sort()
|
table.Sort()
|
||||||
table.PrintMarkdown()
|
if err := table.PrintMarkdown(); err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -377,3 +334,48 @@ func splitArg(arg string) (string, string) {
|
|||||||
return parts[0], parts[1]
|
return parts[0], parts[1]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getClusterData(es *elasticsearch.TypedClient, wg *sync.WaitGroup, reschan chan apiResponse, which string) {
|
||||||
|
defer wg.Done()
|
||||||
|
|
||||||
|
ar := apiResponse{}
|
||||||
|
arerr := errors.New("")
|
||||||
|
|
||||||
|
switch which {
|
||||||
|
case "health":
|
||||||
|
res, err := es.Cluster.Health().
|
||||||
|
Header("content-type", "application/json").
|
||||||
|
Header("accept", "application/json").
|
||||||
|
Do(context.Background())
|
||||||
|
|
||||||
|
ar.health = res
|
||||||
|
ar.which = ResponseHealth
|
||||||
|
arerr = err
|
||||||
|
|
||||||
|
case "info":
|
||||||
|
res, err := es.Info().
|
||||||
|
Header("content-type", "application/json").
|
||||||
|
Header("accept", "application/json").
|
||||||
|
Do(context.Background())
|
||||||
|
|
||||||
|
ar.info = res
|
||||||
|
ar.which = ResponseInfo
|
||||||
|
arerr = err
|
||||||
|
|
||||||
|
case "ccrstats":
|
||||||
|
res, err := es.Ccr.Stats().
|
||||||
|
Header("content-type", "application/json").
|
||||||
|
Header("accept", "application/json").
|
||||||
|
Do(context.Background())
|
||||||
|
|
||||||
|
ar.ccr = res
|
||||||
|
ar.which = ResponseCcr
|
||||||
|
arerr = err
|
||||||
|
}
|
||||||
|
|
||||||
|
if arerr != nil {
|
||||||
|
ar.error = fmt.Errorf("failed to get cluster health: %s", arerr)
|
||||||
|
}
|
||||||
|
|
||||||
|
reschan <- ar
|
||||||
|
}
|
||||||
@@ -28,12 +28,6 @@ import (
|
|||||||
"github.com/elastic/go-elasticsearch/v9/typedapi/types/enums/healthstatus"
|
"github.com/elastic/go-elasticsearch/v9/typedapi/types/enums/healthstatus"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Settings struct {
|
|
||||||
wait_for_active_shards string
|
|
||||||
number_of_shards int
|
|
||||||
number_of_replicas int
|
|
||||||
}
|
|
||||||
|
|
||||||
// used for completion
|
// used for completion
|
||||||
func IndexNames(conf *cfg.Config) ([]string, error) {
|
func IndexNames(conf *cfg.Config) ([]string, error) {
|
||||||
res, err := conf.DefaultCluster.ES.Cat.Indices().
|
res, err := conf.DefaultCluster.ES.Cat.Indices().
|
||||||
@@ -42,7 +36,7 @@ func IndexNames(conf *cfg.Config) ([]string, error) {
|
|||||||
Do(context.Background())
|
Do(context.Background())
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("Error getting indicies: %s", err)
|
return nil, fmt.Errorf("failed to get indicies: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
indices := make([]string, len(res))
|
indices := make([]string, len(res))
|
||||||
@@ -65,7 +59,7 @@ func IndexList(conf *cfg.Config) error {
|
|||||||
|
|
||||||
res, err := cat.Do(context.Background())
|
res, err := cat.Do(context.Background())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Error getting indicies: %s", err)
|
return fmt.Errorf("failed to get indicies: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
slog.Debug("ES result", "indicies", res)
|
slog.Debug("ES result", "indicies", res)
|
||||||
@@ -92,7 +86,9 @@ func IndexList(conf *cfg.Config) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
table.Sort()
|
table.Sort()
|
||||||
table.PrintMarkdown()
|
if err := table.PrintMarkdown(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -131,7 +127,9 @@ func IndexShow(conf *cfg.Config, index string) error {
|
|||||||
{"uuid", *res[index].Settings.Index.Uuid},
|
{"uuid", *res[index].Settings.Index.Uuid},
|
||||||
}
|
}
|
||||||
|
|
||||||
table.PrintMarkdown()
|
if err := table.PrintMarkdown(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ func NodeList(conf *cfg.Config) error {
|
|||||||
// get nodes
|
// get nodes
|
||||||
nodes, err := conf.DefaultCluster.ES.Cat.Nodes().Do(context.Background())
|
nodes, err := conf.DefaultCluster.ES.Cat.Nodes().Do(context.Background())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Error getting nodes: %s", err)
|
return fmt.Errorf("failed to get nodes: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
slog.Debug("ES result", "nodes", nodes)
|
slog.Debug("ES result", "nodes", nodes)
|
||||||
@@ -49,7 +49,9 @@ func NodeList(conf *cfg.Config) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
table.Sort()
|
table.Sort()
|
||||||
table.PrintMarkdown()
|
if err := table.PrintMarkdown(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ func Search(conf *cfg.Config, q string) error {
|
|||||||
}).
|
}).
|
||||||
Do(context.Background())
|
Do(context.Background())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Error running search (esdsl): %s", err)
|
return fmt.Errorf("failed to run search (esdsl): %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
slog.Debug("ES result", "search", res)
|
slog.Debug("ES result", "search", res)
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ func SnapshotList(conf *cfg.Config) error {
|
|||||||
// get partial indicies
|
// get partial indicies
|
||||||
ires, err := conf.DefaultCluster.ES.Cat.Indices().Do(context.Background())
|
ires, err := conf.DefaultCluster.ES.Cat.Indices().Do(context.Background())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Error getting indicies: %s", err)
|
return fmt.Errorf("failed to get indicies: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
indicies := map[string]int{}
|
indicies := map[string]int{}
|
||||||
@@ -57,7 +57,7 @@ func SnapshotList(conf *cfg.Config) error {
|
|||||||
// get snapshots
|
// get snapshots
|
||||||
sres, err := conf.DefaultCluster.ES.Cat.Snapshots().Do(context.Background())
|
sres, err := conf.DefaultCluster.ES.Cat.Snapshots().Do(context.Background())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Error getting snapshots: %s", err)
|
return fmt.Errorf("failed to get snapshots: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
slog.Debug("ES result", "indicies", sres)
|
slog.Debug("ES result", "indicies", sres)
|
||||||
@@ -97,7 +97,9 @@ func SnapshotList(conf *cfg.Config) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
table.Sort()
|
table.Sort()
|
||||||
table.PrintMarkdown()
|
if err := table.PrintMarkdown(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -135,7 +137,9 @@ func SnapshotShow(conf *cfg.Config, snapshot string) error {
|
|||||||
{"shards-successful", fmt.Sprintf("%d", snap.Shards.Successful)},
|
{"shards-successful", fmt.Sprintf("%d", snap.Shards.Successful)},
|
||||||
}
|
}
|
||||||
|
|
||||||
table.PrintMarkdown()
|
if err := table.PrintMarkdown(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -84,11 +84,11 @@ func (data *Table) PrintMarkdown() error {
|
|||||||
table.Header(data.headers)
|
table.Header(data.headers)
|
||||||
|
|
||||||
if err := table.Bulk(data.entries); err != nil {
|
if err := table.Bulk(data.entries); err != nil {
|
||||||
return fmt.Errorf("Failed to add data to table renderer: %s", err)
|
return fmt.Errorf("failed to add data to table renderer: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := table.Render(); err != nil {
|
if err := table.Render(); err != nil {
|
||||||
fmt.Errorf("Failed to render table: %s", err)
|
return fmt.Errorf("failed to render table: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println(tableString.String())
|
fmt.Println(tableString.String())
|
||||||
|
|||||||
Reference in New Issue
Block a user