Files
esctl/pkg/es/ilm.go

358 lines
9.0 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"
"strings"
"codeberg.org/scip/esctl/pkg/cfg"
"codeberg.org/scip/esctl/pkg/printer"
"github.com/alecthomas/repr"
"github.com/charmbracelet/lipgloss"
"github.com/elastic/go-elasticsearch/v9/typedapi/esdsl"
"github.com/elastic/go-elasticsearch/v9/typedapi/ilm/putlifecycle"
"github.com/elastic/go-elasticsearch/v9/typedapi/types"
)
func IlmRetry(conf *cfg.Config, index string) error {
_, err := conf.DefaultCluster.ES().Ilm.Retry(index).
Do(context.Background())
if err != nil {
return fmt.Errorf("failed to retry ilm: %s", esErrorString(err))
}
return nil
}
func IlmStatus(conf *cfg.Config) error {
res, err := conf.DefaultCluster.ES().Ilm.GetStatus().
Do(context.Background())
if err != nil {
return fmt.Errorf("failed to get ilm status: %s", esErrorString(err))
}
fmt.Println(res.OperationMode.Name)
return nil
}
func IlmNames(conf *cfg.Config) ([]string, error) {
res, err := conf.DefaultCluster.ES().Ilm.GetLifecycle().
Do(context.Background())
if err != nil {
return nil, fmt.Errorf("failed to get ilm policies: %s", esErrorString(err))
}
names := make([]string, len(res))
idx := 0
for name := range res {
names[idx] = name
}
return names, nil
}
func IlmList(conf *cfg.Config, pattern string) error {
ilm := conf.DefaultCluster.ES().Ilm.GetLifecycle()
if pattern != "" {
ilm.FilterPath(pattern)
}
res, err := ilm.Do(context.Background())
if err != nil {
return fmt.Errorf("failed to get ilm policies: %s", esErrorString(err))
}
if conf.Debug {
repr.Println(res)
}
table := printer.NewTable(conf, 5, 0)
table.Addheaders("ilm policy", "hot", "warm", "frozen", "delete")
for name, ilm := range res {
table.AddRow(name,
ilmPhaseString(ilm.Policy.Phases.Hot, true),
ilmPhaseString(ilm.Policy.Phases.Warm, true),
ilmPhaseString(ilm.Policy.Phases.Frozen, true),
ilmPhaseString(ilm.Policy.Phases.Delete, true),
)
}
table.Sort()
return table.Print()
}
func IlmShow(conf *cfg.Config, policy string) error {
res, err := conf.DefaultCluster.ES().Ilm.GetLifecycle().
Policy(policy).
Do(context.Background())
if err != nil {
return fmt.Errorf("failed to get ilm status: %s", esErrorString(err))
}
if conf.Debug {
repr.Println(res)
}
ilm, exists := res[policy]
if !exists {
return errors.New("no ilm policy retrieved")
}
table := printer.NewTable(conf, 2, 5)
table.Addheaders("ilm policy setting", "value")
table.Entries = [][]string{
{"policy", policy},
{"hot phase", ilmPhaseString(ilm.Policy.Phases.Hot, false)},
{"warm phase", ilmPhaseString(ilm.Policy.Phases.Warm, false)},
{"frozen phase", ilmPhaseString(ilm.Policy.Phases.Frozen, false)},
{"delete phase", ilmPhaseString(ilm.Policy.Phases.Delete, false)},
}
return table.Print()
}
func ilmPhaseString(phase *types.Phase, short bool) string {
if phase == nil {
return ""
}
out := []string{fmt.Sprintf("move-after:%s", phase.MinAge)}
if short {
return strings.Join(out, ",")
}
if phase.Actions.Rollover != nil {
if phase.Actions.Rollover.MaxAge != "" {
out = append(out, fmt.Sprintf("rollover:%s", phase.Actions.Rollover.MaxAge))
if phase.Actions.Rollover.MaxPrimaryShardSize != nil {
out = append(out, fmt.Sprintf("shardsize:%s", phase.Actions.Rollover.MaxPrimaryShardSize))
}
}
}
if phase.Actions.Delete != nil {
out = append(out, fmt.Sprintf("delete-searchable-snapshot:%t", *phase.Actions.Delete.DeleteSearchableSnapshot))
}
if phase.Actions.SearchableSnapshot != nil {
out = append(out, fmt.Sprintf("searchable-snapshot:%s", phase.Actions.SearchableSnapshot.SnapshotRepository))
}
return strings.Join(out, ",")
}
func IlmExplain(conf *cfg.Config, index string) error {
res, err := conf.DefaultCluster.ES().Ilm.ExplainLifecycle(index).
Do(context.Background())
if err != nil {
return fmt.Errorf("failed to get ilm state: %s", esErrorString(err))
}
slog.Debug("ilm status", "ilm", res)
explain, exists := res.Indices[index]
if !exists {
return errors.New("no ilm status retrieved")
}
ilm := explain.(*types.LifecycleExplainManaged)
table := printer.NewTable(conf, 2, 0)
table.Addheaders("ilm status field", "value")
info := ""
err = json.Unmarshal(ilm.StepInfo["reason"], &info)
if err != nil {
return fmt.Errorf("failed to unmarshal step info: %w", err)
}
table.Entries = [][]string{
{"index", ilm.Index},
{"ilm policy", *ilm.Policy},
{"action", *ilm.Action},
{"age", fmt.Sprintf("%s", ilm.Age)},
{"managed", fmt.Sprintf("%t", ilm.Managed)},
{"phase", *ilm.Phase},
{"phase execution", ilmPhaseString(ilm.PhaseExecution.PhaseDefinition, false)},
{"step", *ilm.Step},
{"failed step", *ilm.FailedStep},
{"failed step retry count", fmt.Sprintf("%d", ilm.FailedStepRetryCount)},
}
if err := table.Print(); err != nil {
return err
}
if info != "" {
var bold = lipgloss.NewStyle().Bold(true)
fmt.Printf("\n%s:\n%s\n",
bold.Render("Step Reason"),
info)
}
return nil
}
func IlmCreate(conf *cfg.Config, policyname string) error {
ilm := conf.DefaultCluster.ES().Ilm.PutLifecycle(policyname)
cfg := conf.Ilm
var phases types.PhasesVariant = esdsl.NewPhases()
if cfg.HaveHot() {
hot := types.Phase{}
var actions types.IlmActionsVariant = esdsl.NewIlmActions()
rollover := &types.RolloverAction{}
haveroll := false
if cfg.HotMinAge != "" {
hot.MinAge = cfg.HotMinAge
}
if cfg.HotRolloverMaxAge != "" {
rollover.MaxAge = cfg.HotRolloverMaxAge
haveroll = true
}
if cfg.HotRolloverMaxDocs != 0 {
rollover.MaxDocs = &cfg.HotRolloverMaxDocs
haveroll = true
}
if cfg.HotRolloverMaxPrimaryShardSize != "" {
rollover.MaxPrimaryShardSize = &cfg.HotRolloverMaxPrimaryShardSize
}
if haveroll {
actions.IlmActionsCaster().Rollover = rollover
}
hot.Actions = actions.IlmActionsCaster()
phases.PhasesCaster().Hot = &hot
}
if cfg.HaveWarm() {
warm := types.Phase{}
var actions types.IlmActionsVariant = esdsl.NewIlmActions()
if cfg.WarmForceMerge != 0 {
actions.IlmActionsCaster().Forcemerge = &types.ForceMergeAction{MaxNumSegments: cfg.WarmForceMerge}
}
if cfg.WarmMinAge != "" {
warm.MinAge = cfg.WarmMinAge
}
if cfg.WarmPriority != 0 {
actions.IlmActionsCaster().SetPriority = &types.SetPriorityAction{Priority: &cfg.WarmPriority}
}
if cfg.WarmShrinkShards != 0 {
actions.IlmActionsCaster().Shrink = &types.ShrinkAction{NumberOfShards: &cfg.WarmShrinkShards}
}
warm.Actions = actions.IlmActionsCaster()
phases.PhasesCaster().Warm = &warm
}
if cfg.HaveCold() {
cold := types.Phase{}
var actions types.IlmActionsVariant = esdsl.NewIlmActions()
if cfg.ColdForceMerge != 0 {
actions.IlmActionsCaster().Forcemerge = &types.ForceMergeAction{MaxNumSegments: cfg.ColdForceMerge}
}
if cfg.ColdMinAge != "" {
cold.MinAge = cfg.ColdMinAge
}
if cfg.ColdPriority != 0 {
actions.IlmActionsCaster().SetPriority = &types.SetPriorityAction{Priority: &cfg.ColdPriority}
}
if cfg.ColdSearchableSnapshotRepo != "" {
actions.IlmActionsCaster().SearchableSnapshot =
&types.SearchableSnapshotAction{SnapshotRepository: cfg.ColdSearchableSnapshotRepo}
}
cold.Actions = actions.IlmActionsCaster()
phases.PhasesCaster().Cold = &cold
}
if cfg.HaveFrozen() {
froze := types.Phase{}
var actions types.IlmActionsVariant = esdsl.NewIlmActions()
if cfg.FrozenMinAge != "" {
froze.MinAge = cfg.FrozenMinAge
}
if cfg.FrozenSearchableSnapshotRepo != "" {
actions.IlmActionsCaster().SearchableSnapshot =
&types.SearchableSnapshotAction{SnapshotRepository: cfg.FrozenSearchableSnapshotRepo}
}
froze.Actions = actions.IlmActionsCaster()
phases.PhasesCaster().Frozen = &froze
}
if cfg.HaveDelete() {
del := types.Phase{}
delete := types.DeleteAction{}
var actions types.IlmActionsVariant = esdsl.NewIlmActions()
if cfg.DeleteMinAge != "" {
del.MinAge = cfg.DeleteMinAge
}
if cfg.DeleteSearchableSnapshots {
delete.DeleteSearchableSnapshot = &cfg.DeleteSearchableSnapshots
}
actions.IlmActionsCaster().Delete = &delete
del.Actions = actions.IlmActionsCaster()
phases.PhasesCaster().Delete = &del
}
put := &putlifecycle.Request{}
policy := &types.IlmPolicy{}
policy.IlmPolicyCaster().Phases = *phases.PhasesCaster()
put.Policy = policy
ilm.Request(put)
_, err := ilm.Do(context.Background())
if err != nil {
return fmt.Errorf("failed create ilm policy: %s", esErrorString(err))
}
return nil
}