/* 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" "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 IndexTemplateList(conf *cfg.Config) error { res, err := conf.DefaultCluster.ES().Indices.GetIndexTemplate(). Do(context.Background()) if err != nil { return fmt.Errorf("failed to get index templates: %s", esErrorString(err)) } slog.Debug("res", "index templates", res) table := printer.NewTable(conf, 5, len(res.IndexTemplates)) table.Addheaders("name", "description", "priority") for idx, tpl := range res.IndexTemplates { desc, err := json.Marshal(tpl.IndexTemplate.Meta_["description"]) if err != nil { return fmt.Errorf("failed to unmarshal meta json data: %w", err) } table.Entries[idx] = []string{ tpl.Name, string(desc), fmt.Sprintf("%d", tpl.IndexTemplate.Priority), } } 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: %s", 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 = [][]string{ {"name", tpl.Name}, {"description", string(desc)}, {"index patterns", strings.Join(tpl.IndexTemplate.IndexPatterns, ",")}, {"composed of", strings.Join(tpl.IndexTemplate.ComposedOf, ",")}, {"data stream enabled", fmt.Sprintf("%t", hasds)}, {"aliases", strings.Join(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 nil } 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.Entries = append(table.Entries, []string{ 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 = settings.NumberOfShards(strconv.Itoa(conf.Shards)) } if conf.Replicas > 0 { settings = settings.NumberOfReplicas(strconv.Itoa(conf.Replicas)) } 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: %s", 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: %s", 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 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: %s", 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: %s", 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: %s", 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: %s", 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.Entries = append(table.Entries, []string{ alias, fmt.Sprintf("%t", res.RolledOver), fmt.Sprintf("%t", 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{} for key, value := range meta { metadata[key] = value } 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 }