/*
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"
"encoding/json"
"errors"
"fmt"
"log/slog"
"maps"
"strconv"
"strings"
"codeberg.org/scip/esctl/pkg/cfg"
"codeberg.org/scip/esctl/pkg/printer"
"github.com/elastic/go-elasticsearch/v9/typedapi/esdsl"
"github.com/elastic/go-elasticsearch/v9/typedapi/types"
)
// used for completion
func IndexTemplateNames(conf *cfg.Config) ([]string, error) {
res, err := conf.DefaultCluster.ES().Indices.GetIndexTemplate().
Do(context.Background())
if err != nil {
return nil, fmt.Errorf("failed to get index templates: %w", esErrorString(err))
}
names := make([]string, len(res.IndexTemplates))
for idx, tpl := range res.IndexTemplates {
names[idx] = tpl.Name
}
return names, nil
}
func IndexTemplateList(conf *cfg.Config, filter string) error {
res, err := conf.DefaultCluster.ES().Indices.GetIndexTemplate().
Do(context.Background())
if err != nil {
return fmt.Errorf("failed to get index templates: %w", esErrorString(err))
}
slog.Debug("res", "index templates", res)
table := printer.NewTable(conf, 5, 0)
table.Addheaders("name", "description", "index patterns", "priority")
for _, tpl := range res.IndexTemplates {
if !conf.Hidden && strings.HasPrefix(tpl.Name, ".") {
continue
}
if filter != "" && !strings.Contains(tpl.Name, filter) {
continue
}
if len(conf.Filter) > 0 {
skip := true
for _, filter := range conf.Filter {
for _, pattern := range tpl.IndexTemplate.IndexPatterns {
if strings.Contains(pattern, filter) {
skip = false
}
}
}
if skip {
continue
}
}
desc := strings.TrimPrefix(strings.TrimSuffix(string(tpl.IndexTemplate.Meta_["description"]), `"`), `"`)
var prio int64
if tpl.IndexTemplate.Priority != nil {
prio = *tpl.IndexTemplate.Priority
}
table.AddRow(tpl.Name, desc, tpl.IndexTemplate.IndexPatterns, prio)
}
table.Sort()
return table.Print()
}
func IndexTemplateShow(conf *cfg.Config, tplname string) error {
res, err := conf.DefaultCluster.ES().Indices.GetIndexTemplate().
Name(tplname).
Do(context.Background())
if err != nil {
return fmt.Errorf("failed to get index template: %w", esErrorString(err))
}
slog.Debug("res", "index template", res)
if len(res.IndexTemplates) != 1 {
return errors.New("multiple or no index template matched the pattern")
}
tpl := res.IndexTemplates[0]
table := printer.NewTable(conf, 2, 6)
table.Addheaders("index template property", "value")
desc, err := json.Marshal(tpl.IndexTemplate.Meta_["description"])
if err != nil {
return fmt.Errorf("failed to unmarshal meta json data: %w", err)
}
hasds := tpl.IndexTemplate.DataStream != nil
aliases := []string{}
for alias := range tpl.IndexTemplate.Template.Aliases {
aliases = append(aliases, alias)
}
table.Entries = [][]any{
{"name", tpl.Name},
{"description", desc},
{"index patterns", tpl.IndexTemplate.IndexPatterns},
{"composed of", tpl.IndexTemplate.ComposedOf},
{"data stream enabled", hasds},
{"aliases", aliases},
}
if err := table.Print(); err != nil {
return err
}
table = printer.NewTable(conf, 2, 0)
table.Addheaders("index setting property", "value")
err = getIndexTemplateSettings(conf, tplname, table)
if err != nil {
return err
}
fmt.Println()
if err := table.Print(); err != nil {
return err
}
table = printer.NewTable(conf, 2, 0)
table.Addheaders("index field mapping", "type")
if tpl.IndexTemplate.Template.Mappings != nil {
for name, field := range tpl.IndexTemplate.Template.Mappings.Properties {
typeval := ""
switch val := field.(type) {
case *types.IntegerNumberProperty:
typeval = val.Type
case *types.KeywordProperty:
typeval = val.Type
case *types.DateProperty:
typeval = val.Type
}
table.AddRow(name, typeval)
}
fmt.Println()
if err := table.Print(); err != nil {
return err
}
}
return nil
}
func IndexTemplateCreate(conf *cfg.Config, name string, mappings []string) error {
settings := esdsl.NewIndexSettings()
maps := esdsl.NewIndexTemplateMapping()
create := conf.DefaultCluster.ES().Indices.PutIndexTemplate(name)
if conf.Shards > 0 {
settings.NumberOfShards(strconv.Itoa(conf.Shards))
}
if conf.Replicas > 0 {
settings.NumberOfReplicas(strconv.Itoa(conf.Replicas))
}
if conf.Policy != "" {
settings.Lifecycle(
esdsl.NewIndexSettingsLifecycle().
Name(conf.Policy))
}
if conf.Stream {
create.DataStream(esdsl.NewDataStreamVisibility())
if conf.Retention != "" {
maps.Lifecycle(
esdsl.NewDataStreamLifecycle().
DataRetention(
esdsl.NewDuration().String(conf.Retention)))
}
}
if conf.AutoCreate {
create.AllowAutoCreate(true)
}
if conf.Mode != "" {
settings = settings.Mode(conf.Mode)
}
if len(mappings) > 0 {
typemaps, err := modMappings(mappings)
if err != nil {
return err
}
maps.Mappings(typemaps)
}
if len(conf.Components) > 0 {
create.ComposedOf(conf.Components...)
}
for _, alias := range conf.Aliases {
maps.AddAlias(alias, esdsl.NewAlias())
}
if len(conf.Meta) > 0 {
metadata, err := modMeta(conf, nil)
if err != nil {
return err
}
create.Meta_(esdsl.NewMetadata(metadata))
}
if len(conf.Settings) > 0 {
usersettings, err := modSettings(conf)
if err != nil {
return err
}
settings = settings.IndexSettings(usersettings)
}
maps.Settings(settings)
create.Template(maps)
create.IndexPatterns(conf.Patterns...)
_, err := create.Do(context.Background())
if err != nil {
return fmt.Errorf("failed to create index template: %w", esErrorString(err))
}
return nil
}
// FIXME: func's too long, refactor
// FIXME: it's not yet possible to remove items in lists (aliases, mappings, settings), just overwrite them
func IndexTemplateModify(conf *cfg.Config, name string, mappings []string) error {
maps := esdsl.NewIndexTemplateMapping()
// load existing index mapping
res, err := conf.DefaultCluster.ES().Indices.GetIndexTemplate().
Name(name).
Do(context.Background())
if err != nil {
return fmt.Errorf("failed to get index template: %w", esErrorString(err))
}
slog.Debug("res", "index template", res)
if len(res.IndexTemplates) != 1 {
return errors.New("multiple or no index template matched the pattern")
}
tpl := res.IndexTemplates[0]
// our modify PUT request
modify := conf.DefaultCluster.ES().Indices.PutIndexTemplate(name)
// load existing settings, if any
settings := tpl.IndexTemplate.Template.Settings
// pre fill mappings and aliases
maps.Mappings(tpl.IndexTemplate.Template.Mappings)
maps.Aliases(tpl.IndexTemplate.Template.Aliases)
// pre fill components
modify.ComposedOf(tpl.IndexTemplate.ComposedOf...)
// pre fill data stream config
if conf.Stream {
modify.DataStream(esdsl.NewDataStreamVisibility())
if conf.Retention != "" {
maps.Lifecycle(
esdsl.NewDataStreamLifecycle().
DataRetention(
esdsl.NewDuration().String(conf.Retention)))
}
}
modify.AllowAutoCreate(conf.AutoCreate)
if conf.Mode != "" {
*settings.Mode = conf.Mode
}
if conf.Policy != "" {
settings.Lifecycle.Name = &conf.Policy
}
if len(mappings) > 0 {
typemaps, err := modMappings(mappings)
if err != nil {
return err
}
maps.Mappings(typemaps)
}
if len(conf.Components) > 0 {
modify.ComposedOf(conf.Components...)
}
for _, alias := range conf.Aliases {
maps.AddAlias(alias, esdsl.NewAlias())
}
if len(conf.Meta) > 0 {
// pre fill meta, if any
metadata, err := modMeta(conf, tpl.IndexTemplate.Meta_)
if err != nil {
return err
}
modify.Meta_(esdsl.NewMetadata(metadata))
}
if len(conf.Settings) > 0 {
usersettings, err := modSettings(conf)
if err != nil {
return err
}
settings.IndexSettings = usersettings
}
patterns := tpl.IndexTemplate.IndexPatterns
if len(conf.Patterns) > 0 {
patterns = conf.Patterns
}
maps.Settings(settings)
modify.Template(maps)
modify.IndexPatterns(patterns...)
_, err = modify.Do(context.Background())
if err != nil {
return fmt.Errorf("failed to modify index template: %w", esErrorString(err))
}
if conf.Rollover {
if err := rolloverAliasIndexTemplate(conf, name); err != nil {
return err
}
}
return nil
}
func IndexTemplateDelete(conf *cfg.Config, name string) error {
_, err := conf.DefaultCluster.ES().Indices.DeleteIndexTemplate(name).
Do(context.Background())
if err != nil {
return fmt.Errorf("failed to delete index template: %w", esErrorString(err))
}
return nil
}
// Based on given index template find the associated index patterns,
// find indices matching those, find their associated aliases and
// rollover all we find. Only run when conf.Rollover==true
func rolloverAliasIndexTemplate(conf *cfg.Config, name string) error {
res, err := conf.DefaultCluster.ES().Indices.GetIndexTemplate().
Name(name).
Do(context.Background())
if err != nil {
return fmt.Errorf("failed to get index template: %w", esErrorString(err))
}
slog.Debug("res", "index template", res)
if len(res.IndexTemplates) != 1 {
return errors.New("multiple or no index template matched the pattern")
}
patterns := res.IndexTemplates[0].IndexTemplate.IndexPatterns
aliases := map[string]int{}
// find all aliases matching the patterns
for _, pattern := range patterns {
res, err := conf.DefaultCluster.ES().Indices.ResolveIndex(pattern).
Do(context.Background())
if err != nil {
return fmt.Errorf("failed to resolve index pattern: %w", esErrorString(err))
}
for _, index := range res.Indices {
for _, alias := range index.Aliases {
aliases[alias] = 1
}
}
}
if len(aliases) == 0 {
return nil
}
table := printer.NewTable(conf, 4, 0)
table.Addheaders("rollover alias", "status", "ack", "new index")
// apply rollover to all matching aliases, if any
for alias := range aliases {
res, err := RolloverAlias(conf, alias)
if err != nil {
return err
}
table.AddRow(alias, res.RolledOver, res.Acknowledged, res.NewIndex)
if err := table.Print(); err != nil {
return err
}
}
return nil
}
func modMappings(mappings []string) (types.TypeMappingVariant, error) {
typemaps := esdsl.NewTypeMapping()
for _, mapping := range mappings {
parts := strings.Split(mapping, ":")
if len(parts) != 2 {
return nil, fmt.Errorf(
"invalid mapping %s, expect (type: integer, text, date, keyword)",
mapping)
}
switch parts[1] {
case "text":
typemaps.AddProperty(parts[0], esdsl.NewTextProperty())
case "integer":
typemaps.AddProperty(parts[0], esdsl.NewIntegerNumberProperty())
case "date":
typemaps.AddProperty(parts[0], esdsl.NewDateProperty())
case "keyword":
typemaps.AddProperty(parts[0], esdsl.NewKeywordProperty())
}
}
return typemaps, nil
}
func modMeta(conf *cfg.Config, meta types.Metadata) (map[string]json.RawMessage, error) {
metadata := map[string]json.RawMessage{}
maps.Copy(metadata, meta)
for _, meta := range conf.Meta {
parts := strings.Split(meta, ":")
if len(parts) != 2 {
return nil, errors.New("meta data must be in the form key:value")
}
msg, err := json.Marshal(parts[1])
if err != nil {
return nil, fmt.Errorf("failed to json marshal metadata %s: %w", meta, err)
}
metadata[parts[0]] = msg
}
return metadata, nil
}
func modSettings(conf *cfg.Config) (map[string]json.RawMessage, error) {
usersettings := map[string]json.RawMessage{}
for _, meta := range conf.Settings {
parts := strings.Split(meta, ":")
if len(parts) != 2 {
return nil, errors.New("settings data must be in the form key:value")
}
msg, err := json.Marshal(parts[1])
if err != nil {
return nil, fmt.Errorf("failed to json marshal metadata %s: %w", meta, err)
}
usersettings[parts[0]] = msg
}
return usersettings, nil
}