/* 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" "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 != 0 { 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.HaveFrozen() { froze := phases.PhasesCaster().Frozen if cfg.FrozenMinAge != "" { froze.MinAge = cfg.FrozenMinAge } } if cfg.HaveDelete() { del := phases.PhasesCaster().Delete 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 }