/* 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 . */ package es import ( "context" "fmt" "log/slog" "regexp" "strconv" "strings" "time" "codeberg.org/scip/esctl/pkg/cfg" "github.com/elastic/go-elasticsearch/v9/typedapi/cat/indices" "github.com/elastic/go-elasticsearch/v9/typedapi/esdsl" "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(). Header("content-type", "application/json"). Header("accept", "application/json"). Do(context.Background()) if err != nil { return nil, fmt.Errorf("failed to get indicies: %s", err) } indices := make([]string, len(res)) for idx, index := range res { indices[idx] = *index.Index } return indices, nil } func filterIndices(conf *cfg.Config, list indices.Response) indices.Response { if len(conf.Filter) == 0 { return list } // we support just one filter here, for now filter := *regexp.MustCompile(conf.Filter[0]) newlist := indices.Response{} for _, index := range list { if filter.MatchString(*index.Index) { newlist = append(newlist, index) } } return newlist } func IndexList(conf *cfg.Config) error { 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) } res, err := cat.Do(context.Background()) if err != nil { return fmt.Errorf("failed to get indicies: %s", err) } slog.Debug("ES result", "indicies", res) list := filterIndices(conf, res) size := len(list) if conf.MaxItems > 0 { if size > conf.MaxItems { size = conf.MaxItems } } table := NewTable(3, size) table.Addheaders("name", "size", "docscount") for idx, index := range list { name := Colorize(*index.Health, *index.Index) table.entries[idx] = []string{name, *index.DatasetSize, *index.DocsCount} if idx == size-1 { break } } table.Sort() if err := table.PrintMarkdown(); err != nil { return err } return nil } func IndexShow(conf *cfg.Config, index string) error { 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) } slog.Debug("ES result", "index", res) table := NewTable(2, 5) table.Addheaders("index property", "value") ts, err := strconv.ParseInt(res[index].Settings.Index.CreationDate.(string), 10, 64) if err != nil { ts = 0 } created := time.Unix(ts/1000, 0) table.entries = [][]string{ {"name", index}, {"replicas", *res[index].Settings.Index.NumberOfReplicas}, {"shards", *res[index].Settings.Index.NumberOfShards}, {"created", created.Format("2006-01-02 15:04:05")}, {"uuid", *res[index].Settings.Index.Uuid}, } if err := table.PrintMarkdown(); err != nil { return err } return nil } func IndexCreate(conf *cfg.Config, index string, mappings []string) error { settings := esdsl.NewIndexSettings() create := conf.DefaultCluster.ES.Indices.Create(index). Header("content-type", "application/json"). Header("accept", "application/json") if conf.Wait { create.WaitForActiveShards("all") } if conf.Shards > 0 { settings = settings.NumberOfShards(strconv.Itoa(conf.Shards)) } if conf.Replicas > 0 { settings = 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 (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) } _, err := create.Settings(settings). Do(context.Background()) if err != nil { return fmt.Errorf("failed to create index: %s", err) } return nil } func IndexDelete(conf *cfg.Config, index string) error { _, err := conf.DefaultCluster.ES.Indices.Delete(index). Header("content-type", "application/json"). Header("accept", "application/json"). Do(context.Background()) if err != nil { return fmt.Errorf("failed to delete index: %s", err) } return nil } func IndexClose(conf *cfg.Config, index string) error { create := conf.DefaultCluster.ES.Indices.Close(index). Header("content-type", "application/json"). Header("accept", "application/json") _, err := create.Do(context.Background()) if err != nil { return fmt.Errorf("failed to close index: %s", err) } return nil } func IndexAllocation(conf *cfg.Config, index string) error { res, err := conf.DefaultCluster.ES.Cluster.AllocationExplain(). Index(index). Primary(conf.Primary). Shard(conf.Shards). Header("content-type", "application/json"). Header("accept", "application/json"). Do(context.Background()) if err != nil { return fmt.Errorf("failed to get index allocation explain: %s", err) } slog.Debug("ES result", "index", res) currentNode := res.CurrentNode table := NewTable(2, 10) table.Addheaders("index allocation setting", "value") roles := make([]string, len(currentNode.Roles)) for idx, role := range currentNode.Roles { roles[idx] = role.Name } table.entries = [][]string{ {"Index", index}, {"Current node", currentNode.Name}, {"Current k8s node", currentNode.Attributes["k8s_node_name"]}, {"Current node address", currentNode.TransportAddress}, {"Current node id", currentNode.Id}, {"Current node weight", fmt.Sprintf("%d", currentNode.WeightRanking)}, {"Current node roles", strings.Join(roles, ",")}, {"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.PrintMarkdown(); err != nil { return err } return nil } func IndexModify(conf *cfg.Config, index string) error { settings := esdsl.NewIndexSettings().NumberOfReplicas(strconv.Itoa(conf.Replicas)) _, err := conf.DefaultCluster.ES.Indices.PutSettings(). Indices(index). Index(settings). Header("content-type", "application/json"). Header("accept", "application/json"). Do(context.Background()) if err != nil { return fmt.Errorf("failed to modify index settings: %s", err) } return nil }