mirror of
https://codeberg.org/scip/esctl.git
synced 2026-08-24 13:14:18 +02:00
add "cluster compare" command and add 1st check (unsync indices) (#2)
This commit is contained in:
78
cmd/cluster.go
Normal file
78
cmd/cluster.go
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
/*
|
||||||
|
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 Cluster(conf *cfg.Config) *cli.Command {
|
||||||
|
return &cli.Command{
|
||||||
|
Name: "cluster",
|
||||||
|
Aliases: []string{"c"},
|
||||||
|
Usage: "manage cluster[s]",
|
||||||
|
|
||||||
|
Commands: []*cli.Command{
|
||||||
|
Compare(conf),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Compare(conf *cfg.Config) *cli.Command {
|
||||||
|
return &cli.Command{
|
||||||
|
Name: "compare",
|
||||||
|
Aliases: []string{"c"},
|
||||||
|
Usage: "compare cluster[s] (yaml config with 2 clusters required)",
|
||||||
|
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "exclude",
|
||||||
|
Usage: "regexp of indicies to exclude",
|
||||||
|
Destination: &conf.Exclude,
|
||||||
|
Aliases: []string{"e"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||||
|
leader := cmd.Args().Get(0)
|
||||||
|
follower := cmd.Args().Get(1)
|
||||||
|
|
||||||
|
if leader == "" || follower == "" {
|
||||||
|
return errors.New("no leader and follower aliases specified")
|
||||||
|
}
|
||||||
|
|
||||||
|
_, hasLeader := conf.Clusters[leader]
|
||||||
|
_, hasFollower := conf.Clusters[follower]
|
||||||
|
|
||||||
|
if !hasLeader || !hasFollower {
|
||||||
|
return errors.New("either leader or follower alias not configured")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := es.ClusterCompare(conf, leader, follower); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -76,11 +76,12 @@ func Main() int {
|
|||||||
Search(conf),
|
Search(conf),
|
||||||
Index(conf),
|
Index(conf),
|
||||||
Snapshot(conf),
|
Snapshot(conf),
|
||||||
|
Cluster(conf),
|
||||||
},
|
},
|
||||||
|
|
||||||
Before: func(ctx context.Context, cmd *cli.Command) (context.Context, error) {
|
Before: func(ctx context.Context, cmd *cli.Command) (context.Context, error) {
|
||||||
if err := conf.Init(); err != nil {
|
if err := conf.Init(); err != nil {
|
||||||
Finish(err)
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Init(conf)
|
log.Init(conf)
|
||||||
|
|||||||
@@ -44,10 +44,11 @@ type Config struct {
|
|||||||
Debug bool // -d
|
Debug bool // -d
|
||||||
Clusters map[string]*Cluster
|
Clusters map[string]*Cluster
|
||||||
DefaultCluster *Cluster
|
DefaultCluster *Cluster
|
||||||
From, To, MaxItems int
|
Index string // index: -i
|
||||||
Index string
|
Failed, Partials bool // index: flags
|
||||||
Filter []string
|
From, To, MaxItems int // search: flags
|
||||||
Failed, Partials bool
|
Filter []string // search: -F
|
||||||
|
Exclude string // cluster compare: -e (regexp)
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewConfig() *Config {
|
func NewConfig() *Config {
|
||||||
|
|||||||
120
pkg/es/cluster.go
Normal file
120
pkg/es/cluster.go
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
/*
|
||||||
|
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"
|
||||||
|
"regexp"
|
||||||
|
|
||||||
|
"codeberg.org/scip/esctl/pkg/cfg"
|
||||||
|
"github.com/elastic/go-elasticsearch/v9/typedapi/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
DefaultExclude = `(part|monitoring|.internal|metrics-endpoint)`
|
||||||
|
)
|
||||||
|
|
||||||
|
type ClusterIndices map[string]map[string]*types.IndicesRecord
|
||||||
|
|
||||||
|
func ClusterCompare(conf *cfg.Config, leader, follower string) error {
|
||||||
|
indices := ClusterIndices{}
|
||||||
|
|
||||||
|
for _, alias := range []string{leader, follower} {
|
||||||
|
cat := conf.Clusters[alias].ES.Cat.Indices().
|
||||||
|
// we need to add custom request headers, required for older ES instances
|
||||||
|
Header("content-type", "application/json").
|
||||||
|
Header("accept", "application/json")
|
||||||
|
|
||||||
|
res, err := cat.Do(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Error getting indicies on %s: %s", alias, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
indices[alias] = make(map[string]*types.IndicesRecord, len(res))
|
||||||
|
|
||||||
|
for _, index := range res {
|
||||||
|
indices[alias][*index.Index] = &index
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
findIndicesOnlyOnMaster(conf, indices, leader, follower)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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()
|
||||||
|
}
|
||||||
@@ -26,7 +26,10 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func IndexList(conf *cfg.Config) error {
|
func IndexList(conf *cfg.Config) error {
|
||||||
cat := conf.DefaultCluster.ES.Cat.Indices()
|
cat := conf.DefaultCluster.ES.Cat.Indices().
|
||||||
|
// we need to add custom request headers, required for older ES instances
|
||||||
|
Header("content-type", "application/json").
|
||||||
|
Header("accept", "application/json")
|
||||||
|
|
||||||
if conf.Failed {
|
if conf.Failed {
|
||||||
cat = cat.Health(healthstatus.Red)
|
cat = cat.Health(healthstatus.Red)
|
||||||
@@ -67,7 +70,11 @@ func IndexList(conf *cfg.Config) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func IndexShow(conf *cfg.Config, index string) error {
|
func IndexShow(conf *cfg.Config, index string) error {
|
||||||
res, err := conf.DefaultCluster.ES.Indices.Get(index).Do(context.Background())
|
res, err := conf.DefaultCluster.ES.Indices.Get(index).
|
||||||
|
// we need to add custom request headers, required for older ES instances
|
||||||
|
Header("content-type", "application/json").
|
||||||
|
Header("accept", "application/json").
|
||||||
|
Do(context.Background())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to get index: %s", err)
|
return fmt.Errorf("failed to get index: %s", err)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user