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))
+ }
+}