diff --git a/cmd/index.go b/cmd/index.go index e5a83ad..d2eecca 100644 --- a/cmd/index.go +++ b/cmd/index.go @@ -33,6 +33,7 @@ func Index(conf *cfg.Config) *cli.Command { Commands: []*cli.Command{ IndexList(conf), + IndexShow(conf), }, } } @@ -50,6 +51,18 @@ func IndexList(conf *cfg.Config) *cli.Command { Destination: &conf.MaxItems, Aliases: []string{"m"}, }, + &cli.BoolFlag{ + Name: "partials", + Usage: "include partial indicies", + Destination: &conf.Partials, + Aliases: []string{"p"}, + }, + &cli.BoolFlag{ + Name: "reds", + Usage: "include only red failed indicies", + Destination: &conf.Failed, + Aliases: []string{"r"}, + }, }, Action: func(ctx context.Context, cmd *cli.Command) error { @@ -61,3 +74,19 @@ func IndexList(conf *cfg.Config) *cli.Command { }, } } + +func IndexShow(conf *cfg.Config) *cli.Command { + return &cli.Command{ + Name: "show", + Aliases: []string{"sh"}, + Usage: "show details about an index", + + Action: func(ctx context.Context, cmd *cli.Command) error { + if err := es.IndexShow(conf, cmd.Args().Get(0)); err != nil { + return err + } + + return nil + }, + } +} diff --git a/cmd/root.go b/cmd/root.go index 04b9f52..96d2eb1 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -63,6 +63,7 @@ func Main() int { Status(conf), Search(conf), Index(conf), + Snapshot(conf), }, Before: func(ctx context.Context, cmd *cli.Command) (context.Context, error) { diff --git a/cmd/snapshot.go b/cmd/snapshot.go new file mode 100644 index 0000000..db3c039 --- /dev/null +++ b/cmd/snapshot.go @@ -0,0 +1,81 @@ +/* +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 cmd + +import ( + "context" + + "codeberg.org/scip/esctl/pkg/cfg" + "codeberg.org/scip/esctl/pkg/es" + + "github.com/urfave/cli/v3" +) + +func Snapshot(conf *cfg.Config) *cli.Command { + return &cli.Command{ + Name: "snapshot", + Aliases: []string{"snap"}, + Usage: "manage snapshots", + + Commands: []*cli.Command{ + SnapshotList(conf), + SnapshotShow(conf), + }, + } +} + +func SnapshotList(conf *cfg.Config) *cli.Command { + return &cli.Command{ + Name: "list", + Aliases: []string{"ls"}, + Usage: "list snapshots", + + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "orphaned", + Usage: "include only orphaned snapshots", + Destination: &conf.Failed, + Aliases: []string{"o"}, + }, + }, + + Action: func(ctx context.Context, cmd *cli.Command) error { + if err := es.SnapshotList(conf); err != nil { + return err + } + + return nil + }, + } +} + +func SnapshotShow(conf *cfg.Config) *cli.Command { + return &cli.Command{ + Name: "show", + Aliases: []string{"sh"}, + Usage: "show details about a snapshot", + UsageText: "show [options] ", + + Action: func(ctx context.Context, cmd *cli.Command) error { + if err := es.SnapshotShow(conf, cmd.Args().Get(0)); err != nil { + return err + } + + return nil + }, + } +} diff --git a/pkg/cfg/config.go b/pkg/cfg/config.go index 19fac7a..097f99b 100644 --- a/pkg/cfg/config.go +++ b/pkg/cfg/config.go @@ -37,6 +37,7 @@ type Config struct { From, To, MaxItems int Index string Filter []string + Failed, Partials bool } func Init() (*Config, error) { diff --git a/pkg/es/aux.go b/pkg/es/aux.go index cb4e16c..041e312 100644 --- a/pkg/es/aux.go +++ b/pkg/es/aux.go @@ -33,7 +33,7 @@ func Health(conf *cfg.Config) error { slog.Debug("ES result", "cluster health", res) - table := NewTable(5) + table := NewTable(2, 5) table.headers = []string{bold("SETTING"), bold("STATUS")} diff --git a/pkg/es/index.go b/pkg/es/index.go index 4f64b22..21ccc2b 100644 --- a/pkg/es/index.go +++ b/pkg/es/index.go @@ -18,16 +18,23 @@ package es import ( "context" - "log" + "fmt" "log/slog" "codeberg.org/scip/esctl/pkg/cfg" + "github.com/elastic/go-elasticsearch/v9/typedapi/types/enums/healthstatus" ) func IndexList(conf *cfg.Config) error { - res, err := conf.ES.Cat.Indices().Do(context.Background()) + cat := conf.ES.Cat.Indices() + + if conf.Failed { + cat = cat.Health(healthstatus.Red) + } + + res, err := cat.Do(context.Background()) if err != nil { - log.Fatalf("Error getting indicies: %s", err) + return fmt.Errorf("Error getting indicies: %s", err) } slog.Debug("ES result", "indicies", res) @@ -40,9 +47,8 @@ func IndexList(conf *cfg.Config) error { } } - table := NewTable(size) - - table.headers = []string{bold("NAME"), bold("SIZE"), bold("DOCSCOUNT")} + table := NewTable(3, size) + table.Addheaders("name", "size", "docscount") for idx, index := range res { name := Colorize(*index.Health, *index.Index) @@ -59,3 +65,16 @@ func IndexList(conf *cfg.Config) error { return nil } + +func IndexShow(conf *cfg.Config, index string) error { + res, err := conf.ES.Indices.Get(index).Do(context.Background()) + if err != nil { + return fmt.Errorf("failed to get index: %s", err) + } + + slog.Debug("ES result", "index", res) + + // FIXME: add table printer like SnapshotShow + + return nil +} diff --git a/pkg/es/snapshot.go b/pkg/es/snapshot.go new file mode 100644 index 0000000..ddedb65 --- /dev/null +++ b/pkg/es/snapshot.go @@ -0,0 +1,151 @@ +/* +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" + "errors" + "fmt" + "log/slog" + "regexp" + "strings" + + "codeberg.org/scip/esctl/pkg/cfg" +) + +var ( + findIndex = regexp.MustCompile(`^\d{4}\.\d{2}\.\d{2}\-(.*)\-timeseries`) +) + +type Snapshot struct { + Name string + Forindex string + Orphaned string + Status string + Start string + IndexCount int +} + +func SnapshotList(conf *cfg.Config) error { + // get partial indicies + ires, err := conf.ES.Cat.Indices().Do(context.Background()) + if err != nil { + return fmt.Errorf("Error getting indicies: %s", err) + } + + indicies := map[string]int{} + + for _, index := range ires { + name := strings.ReplaceAll(*index.Index, "partial-", "") + indicies[name] = 1 + } + + // get snapshots + sres, err := conf.ES.Cat.Snapshots().Do(context.Background()) + if err != nil { + return fmt.Errorf("Error getting snapshots: %s", err) + } + + slog.Debug("ES result", "indicies", sres) + + snapshots := []*Snapshot{} // original snapshot names + + for _, snapshot := range sres { + snap := &Snapshot{ + Name: *snapshot.Id, + Status: *snapshot.Status, + Start: fmt.Sprintf("%s", snapshot.StartTime), + Forindex: indexFromSnapshot(*snapshot.Id), + Orphaned: "no", + } + + _, exists := indicies[snap.Forindex] + if !exists { + snap.Orphaned = "orphaned" + } + + if (conf.Failed && snap.Orphaned == "orphaned") || !conf.Failed { + snapshots = append(snapshots, snap) + } + } + + table := NewTable(5, len(snapshots)) + table.Addheaders("name", "index", "start", "orphaned", "status") + + for idx, snap := range snapshots { + table.entries[idx] = []string{ + snap.Name, + snap.Forindex, + snap.Start, + snap.Orphaned, + snap.Status, + } + } + + table.Sort() + table.PrintMarkdown() + + return nil +} + +func SnapshotShow(conf *cfg.Config, snapshot string) error { + res, err := conf.ES.Snapshot.Get("*", snapshot).Do(context.Background()) + if err != nil { + return fmt.Errorf("failed to get snapshot: %s", err) + } + + slog.Debug("ES result", "snapshot", res) + + if res.Total == 0 { + return errors.New("no snapshot retrieved") + } + + table := NewTable(2, 17) + table.Addheaders("field", "value") + + snap := res.Snapshots[0] + + table.entries = [][]string{ + {"snapshot", snapshot}, + {"uuid", snap.Uuid}, + {"repository", *snap.Repository}, + {"version_id", fmt.Sprintf("%d", snap.VersionId)}, + {"version", *snap.Version}, + {"include_global_state", fmt.Sprintf("%t", *snap.IncludeGlobalState)}, + {"state", *snap.State}, + {"start_time", fmt.Sprintf("%s", snap.StartTime)}, + {"end_time", fmt.Sprintf("%s", snap.EndTime)}, + {"duration_in_millis", fmt.Sprintf("%d", *snap.DurationInMillis)}, + {"shards-total", fmt.Sprintf("%d", snap.Shards.Total)}, + {"shards-failed", fmt.Sprintf("%d", snap.Shards.Failed)}, + {"shards-successful", fmt.Sprintf("%d", snap.Shards.Successful)}, + } + + table.PrintMarkdown() + + return nil +} + +func indexFromSnapshot(snapshot string) string { + matches := findIndex.FindStringSubmatch(snapshot) + + if len(matches) != 2 { + return "" + } + + return matches[1] +} diff --git a/pkg/es/table.go b/pkg/es/table.go index 28fe3a5..3bedef4 100644 --- a/pkg/es/table.go +++ b/pkg/es/table.go @@ -31,11 +31,11 @@ type Table struct { entries [][]string } -func NewTable(size int) *Table { +func NewTable(columns, rows int) *Table { table := Table{} - table.headers = make([]string, size) - table.entries = make([][]string, size) + table.headers = make([]string, columns) + table.entries = make([][]string, rows) return &table } @@ -106,3 +106,9 @@ func (data *Table) Sort() { return data.entries[i][0] < data.entries[j][0] }) } + +func (data *Table) Addheaders(headers ...string) { + for idx, header := range headers { + data.headers[idx] = bold(strings.ToUpper(header)) + } +}