Files
esctl/pkg/es/cluster.go

213 lines
5.3 KiB
Go
Raw Permalink Normal View History

/*
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"
2026-05-11 14:03:30 +02:00
"slices"
2026-05-07 12:54:27 +02:00
"sync"
"codeberg.org/scip/esctl/pkg/cfg"
2026-05-07 12:54:27 +02:00
"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"
)
2026-05-07 12:54:27 +02:00
const (
ResponseHealth = iota
ResponseInfo
ResponseCcr
)
type ClusterIndices map[string]map[string]*types.IndicesRecord
2026-05-07 12:54:27 +02:00
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 {
if !checkClusterFollower(conf, leader) {
return errors.New("leader/follower attribution is invalid, reverse cluster attribution and retry")
}
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("failed to get indicies on %s: %s", alias, err)
}
indices[alias] = make(map[string]*types.IndicesRecord, len(res))
for _, index := range res {
indices[alias][*index.Index] = &index
}
}
if !checkClusterStatus(conf, leader, follower) {
return errors.New("one of the two clusters is in a failed state")
}
findIlmErrors(conf, leader, follower)
if findIndicesOnlyOnLeader(conf, indices, leader, follower) &&
findOrphanedIndices(conf, indices, leader, follower) &&
findFailedFollowerIndices(conf, indices, follower) {
fmt.Println("everything's hunky-dory.")
}
return nil
}
func ClusterList(conf *cfg.Config) error {
table := NewTable(3, len(conf.Clusters))
table.Addheaders("cluster", "uri", "default")
idx := 0
2026-05-11 14:03:30 +02:00
names := make([]string, len(conf.Clusters))
for name := range conf.Clusters {
names[idx] = name
idx++
}
slices.Sort(names)
idx = 0
for idx, name := range names {
current := name == "default" || name == conf.CurrentCluster
2026-05-11 14:03:30 +02:00
cluster := conf.Clusters[name]
_, 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++
}
if err := table.PrintMarkdown(); err != nil {
return err
}
return nil
}
2026-05-07 12:54:27 +02:00
// 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 {
clusters := []string{}
if conf.All {
for key := range conf.Clusters {
clusters = append(clusters, key)
}
} else {
clusters = []string{"default"}
}
for _, cluster := range clusters {
es := conf.DefaultCluster.ES
if cluster != "default" {
es = conf.Clusters[cluster].ES
}
2026-05-07 12:54:27 +02:00
responses := make(chan apiResponse, 3)
wg := &sync.WaitGroup{}
wg.Add(3)
go getClusterData(es, wg, responses, "health")
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
}
switch r.which {
case ResponseHealth:
clusterhealth = r.health
case ResponseCcr:
ccrstats = r.ccr
case ResponseInfo:
info = r.info
}
}
2026-05-07 12:54:27 +02:00
slog.Debug("ES result", "cluster health", clusterhealth)
2026-05-07 12:54:27 +02:00
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,
)
2026-05-06 13:10:02 +02:00
}
table := NewTable(2, 5)
table.Addheaders(cluster, "status")
table.entries = [][]string{
{"Cluster Name", Colorize(clusterhealth.Status.Name, clusterhealth.ClusterName)},
{"ES Version", info.Version.Int},
2026-05-07 12:54:27 +02:00
{"Active Shards", fmt.Sprintf("%d", clusterhealth.ActiveShards)},
{"Active Primary Shards", fmt.Sprintf("%d", clusterhealth.ActivePrimaryShards)},
{"Indicies", fmt.Sprintf("%d", len(clusterhealth.Indices))},
{"Nodes", fmt.Sprintf("%d", clusterhealth.NumberOfNodes)},
{"AutoFollow (success/failed indices)", ccrfollowing},
}
if err := table.PrintMarkdown(); err != nil {
return err
}
}
return nil
}