Files
esctl/pkg/es/index.go

366 lines
8.6 KiB
Go
Raw 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"
2026-04-22 13:38:02 +02:00
"fmt"
"log/slog"
2026-05-19 13:58:08 +02:00
"regexp"
2026-06-03 10:14:41 +02:00
"slices"
"strconv"
"strings"
"time"
"codeberg.org/scip/esctl/pkg/cfg"
"codeberg.org/scip/esctl/pkg/printer"
2026-05-19 13:58:08 +02:00
"github.com/elastic/go-elasticsearch/v9/typedapi/cat/indices"
"github.com/elastic/go-elasticsearch/v9/typedapi/esdsl"
2026-04-22 13:38:02 +02:00
"github.com/elastic/go-elasticsearch/v9/typedapi/types/enums/healthstatus"
)
// used for completion
func IndexNames(conf *cfg.Config) ([]string, error) {
res, err := conf.DefaultCluster.ES().Cat.Indices().
Do(context.Background())
if err != nil {
return nil, fmt.Errorf("failed to get indicies: %s", esErrorString(err))
}
indices := make([]string, len(res))
for idx, index := range res {
indices[idx] = *index.Index
}
return indices, nil
}
2026-05-19 13:58:08 +02:00
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 !conf.Hidden && strings.HasPrefix(*index.Index, ".") {
continue
}
if strings.HasPrefix(*index.Index, ".ds-") {
// ignore data stream backing indicies
continue
}
selectedlist = append(selectedlist, index)
}
2026-05-19 13:58:08 +02:00
if len(conf.Filter) == 0 {
return selectedlist
2026-05-19 13:58:08 +02:00
}
// we support just one filter here, for now
filter := *regexp.MustCompile(conf.Filter[0])
newlist := indices.Response{}
for _, index := range selectedlist {
2026-05-19 13:58:08 +02:00
if filter.MatchString(*index.Index) {
newlist = append(newlist, index)
}
}
return newlist
}
func IndexList(conf *cfg.Config) error {
cat := conf.DefaultCluster.ES().Cat.Indices()
2026-04-22 13:38:02 +02:00
if conf.Failed {
cat = cat.Health(healthstatus.Red)
}
res, err := cat.Do(context.Background())
if err != nil {
return fmt.Errorf("failed to get indicies: %s", esErrorString(err))
}
slog.Debug("ES result", "indicies", res)
2026-05-19 13:58:08 +02:00
list := filterIndices(conf, res)
size := len(list)
if conf.MaxItems > 0 {
if size > conf.MaxItems {
size = conf.MaxItems
}
}
table := printer.NewTable(conf, 3, size)
2026-04-22 13:38:02 +02:00
table.Addheaders("name", "size", "docscount")
2026-05-19 13:58:08 +02:00
for idx, index := range list {
name := printer.Colorize(conf, *index.Health, *index.Index)
2026-07-07 07:29:03 +02:00
table.Entries[idx] = []any{name, *index.DatasetSize, *index.DocsCount}
if idx == size-1 {
break
}
}
table.Sort()
2026-06-15 15:18:21 +02:00
return table.Print()
}
2026-04-22 13:38:02 +02:00
func IndexShow(conf *cfg.Config, indexpattern string) error {
res, err := conf.DefaultCluster.ES().Indices.Get(indexpattern).
Do(context.Background())
2026-04-22 13:38:02 +02:00
if err != nil {
return fmt.Errorf("failed to get index: %s", esErrorString(err))
2026-04-22 13:38:02 +02:00
}
slog.Debug("index show", "index", res)
for name, index := range res {
fields := make([]string, len(index.Mappings.Properties))
idx := 0
for field := range index.Mappings.Properties {
fields[idx] = field
idx++
}
2026-04-22 13:38:02 +02:00
table := printer.NewTable(conf, 2, 7)
table.Addheaders("index property", "value")
ts, err := strconv.ParseInt(index.Settings.Index.CreationDate.(string), 10, 64)
if err != nil {
ts = 0
}
created := time.Unix(ts/1000, 0)
2026-07-07 07:29:03 +02:00
table.Entries = [][]any{
{"name", name},
{"replicas", *index.Settings.Index.NumberOfReplicas},
{"shards", *index.Settings.Index.NumberOfShards},
2026-07-07 07:29:03 +02:00
{"created", created},
{"uuid", *index.Settings.Index.Uuid},
{"version", *index.Settings.Index.Version.Created},
2026-07-07 07:29:03 +02:00
{"fields", fields},
}
if index.Settings.Index.Lifecycle != nil {
2026-07-07 07:29:03 +02:00
table.Entries = append(table.Entries, [][]any{
{"ilm policy", *index.Settings.Index.Lifecycle.Name},
{"ilm rollover alias", *index.Settings.Index.Lifecycle.RolloverAlias},
}...)
}
if err := table.Print(); err != nil {
return err
}
fmt.Println()
}
return nil
}
func IndexCreate(conf *cfg.Config, index string, mappings []string) error {
settings := esdsl.NewIndexSettings()
create := conf.DefaultCluster.ES().Indices.Create(index)
if conf.Wait {
create.WaitForActiveShards("all")
}
if conf.Shards > 0 {
settings.NumberOfShards(strconv.Itoa(conf.Shards))
}
if conf.Replicas > 0 {
settings.NumberOfReplicas(strconv.Itoa(conf.Replicas))
}
if len(mappings) > 0 {
maps := esdsl.NewTypeMapping()
for _, mapping := range mappings {
parts := strings.Split(mapping, ":")
if len(parts) != 2 {
return fmt.Errorf("invalid mapping %s, expect <name:type> (type: integer, text, date, keyword)", mapping)
}
switch parts[1] {
case "text":
maps.AddProperty(parts[0], esdsl.NewTextProperty())
case "integer":
maps.AddProperty(parts[0], esdsl.NewIntegerNumberProperty())
case "date":
maps.AddProperty(parts[0], esdsl.NewDateProperty())
case "keyword":
maps.AddProperty(parts[0], esdsl.NewKeywordProperty())
}
}
create.Mappings(maps)
}
if conf.Policy != "" {
settings.Lifecycle(
esdsl.NewIndexSettingsLifecycle().
Name(conf.Policy).
RolloverAlias(index))
}
_, err := create.Settings(settings).
Do(context.Background())
if err != nil {
return fmt.Errorf("failed to create index: %s", esErrorString(err))
}
return nil
}
func IndexDelete(conf *cfg.Config, index string) error {
_, err := conf.DefaultCluster.ES().Indices.Delete(index).
Do(context.Background())
if err != nil {
return fmt.Errorf("failed to delete index: %s", esErrorString(err))
}
2026-04-22 13:38:02 +02:00
return nil
}
func IndexClose(conf *cfg.Config, index string) error {
_, err := conf.DefaultCluster.ES().Indices.Close(index).
Do(context.Background())
if err != nil {
return fmt.Errorf("failed to close index: %s", esErrorString(err))
}
return nil
}
2026-05-18 13:58:08 +02:00
func IndexAllocation(conf *cfg.Config, index string) error {
res, err := conf.DefaultCluster.ES().Cluster.AllocationExplain().
2026-05-18 13:58:08 +02:00
Index(index).
Primary(conf.Primary).
2026-05-19 13:58:08 +02:00
Shard(conf.Shards).
2026-05-18 13:58:08 +02:00
Do(context.Background())
if err != nil {
return fmt.Errorf("failed to get index allocation explain: %s", esErrorString(err))
2026-05-18 13:58:08 +02:00
}
slog.Debug("ES result", "index", res)
currentNode := res.CurrentNode
table := printer.NewTable(conf, 2, 10)
2026-05-18 13:58:08 +02:00
table.Addheaders("index allocation setting", "value")
roles := make([]string, len(currentNode.Roles))
for idx, role := range currentNode.Roles {
roles[idx] = role.Name
}
2026-07-07 07:29:03 +02:00
table.Entries = [][]any{
2026-05-18 13:58:08 +02:00
{"Index", index},
{"Current node", currentNode.Name},
{"Current k8s node", currentNode.Attributes["k8s_node_name"]},
{"Current node address", currentNode.TransportAddress},
{"Current node id", currentNode.Id},
2026-07-07 07:29:03 +02:00
{"Current node weight", currentNode.WeightRanking},
{"Current node roles", roles},
2026-05-18 13:58:08 +02:00
{"Can rebalance cluster", res.CanRebalanceCluster.Name},
{"Can rebalance to another node", res.CanRebalanceToOtherNode.Name},
{"Can remain on current node", res.CanRemainOnCurrentNode.Name},
}
if err := table.Print(); err != nil {
2026-05-18 13:58:08 +02:00
return err
}
return nil
}
2026-05-19 13:58:08 +02:00
/*
2026-05-19 13:58:08 +02:00
func IndexModify(conf *cfg.Config, index string) error {
settings := esdsl.NewIndexSettings().NumberOfReplicas(strconv.Itoa(conf.Replicas))
_, err := conf.DefaultCluster.ES().Indices.PutSettings().
2026-05-19 13:58:08 +02:00
Indices(index).
Index(settings).
Do(context.Background())
if err != nil {
return fmt.Errorf("failed to modify index settings: %s", esErrorString(err))
}
return nil
}
*/
func IndexFields(conf *cfg.Config, index string) error {
res, err := conf.DefaultCluster.ES().FieldCaps().
Index(index).
Fields("*").
Do(context.Background())
if err != nil {
return fmt.Errorf("failed to retrieve field capabilties: %s", esErrorString(err))
}
2026-06-03 10:14:41 +02:00
table := printer.NewTable(conf, 5, 0)
table.Addheaders("field", "type", "searchable", "aggretable", "metadata")
idx := 0
for name, field := range res.Fields {
for fieldtype, caps := range field {
// fields only have 1 type, so this one is it
2026-06-03 10:14:41 +02:00
switch {
case conf.Searchable && !caps.Searchable:
continue
case conf.Aggretable && !caps.Aggregatable:
continue
case len(conf.Filter) > 0 && !slices.Contains(conf.Filter, fieldtype):
continue
}
2026-07-07 07:29:03 +02:00
table.AddRow(name, fieldtype,
caps.Searchable,
caps.Aggregatable,
*caps.MetadataField)
break
}
idx++
}
table.Sort()
if err := table.Print(); err != nil {
return err
2026-05-19 13:58:08 +02:00
}
return nil
}