mirror of
https://codeberg.org/scip/esctl.git
synced 2026-08-24 13:04:20 +02:00
486 lines
11 KiB
Go
486 lines
11 KiB
Go
/*
|
|
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"
|
|
"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 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: %w", 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] = []any{tpl.Name, desc, 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: %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 <name:type> (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
|
|
}
|