mirror of
https://codeberg.org/scip/esctl.git
synced 2026-08-24 06:34: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),
|
||||
Index(conf),
|
||||
Snapshot(conf),
|
||||
Cluster(conf),
|
||||
},
|
||||
|
||||
Before: func(ctx context.Context, cmd *cli.Command) (context.Context, error) {
|
||||
if err := conf.Init(); err != nil {
|
||||
Finish(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.Init(conf)
|
||||
|
||||
@@ -44,10 +44,11 @@ type Config struct {
|
||||
Debug bool // -d
|
||||
Clusters map[string]*Cluster
|
||||
DefaultCluster *Cluster
|
||||
From, To, MaxItems int
|
||||
Index string
|
||||
Filter []string
|
||||
Failed, Partials bool
|
||||
Index string // index: -i
|
||||
Failed, Partials bool // index: flags
|
||||
From, To, MaxItems int // search: flags
|
||||
Filter []string // search: -F
|
||||
Exclude string // cluster compare: -e (regexp)
|
||||
}
|
||||
|
||||
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 {
|
||||
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 {
|
||||
cat = cat.Health(healthstatus.Red)
|
||||
@@ -67,7 +70,11 @@ func IndexList(conf *cfg.Config) 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 {
|
||||
return fmt.Errorf("failed to get index: %s", err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user