[#1223] lens/tui: Add TUI app for blobovnicza
Signed-off-by: Aleksey Savchuk <a.savchuk@yadro.com>
This commit is contained in:
parent
371d97f61a
commit
7768a482b5
4 changed files with 277 additions and 1 deletions
|
@ -19,7 +19,7 @@ var Root = &cobra.Command{
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
Root.AddCommand(listCMD, inspectCMD)
|
Root.AddCommand(listCMD, inspectCMD, tuiCMD)
|
||||||
}
|
}
|
||||||
|
|
||||||
func openBlobovnicza(cmd *cobra.Command) *blobovnicza.Blobovnicza {
|
func openBlobovnicza(cmd *cobra.Command) *blobovnicza.Blobovnicza {
|
||||||
|
|
79
cmd/frostfs-lens/internal/blobovnicza/tui.go
Normal file
79
cmd/frostfs-lens/internal/blobovnicza/tui.go
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
package blobovnicza
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
common "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-lens/internal"
|
||||||
|
schema "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-lens/internal/schema/blobovnicza"
|
||||||
|
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-lens/internal/tui"
|
||||||
|
"github.com/rivo/tview"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"go.etcd.io/bbolt"
|
||||||
|
)
|
||||||
|
|
||||||
|
var tuiCMD = &cobra.Command{
|
||||||
|
Use: "explore",
|
||||||
|
Short: "Blobovnicza exploration with a terminal UI",
|
||||||
|
Long: `Launch a terminal UI to explore blobovnicza and search for data.
|
||||||
|
|
||||||
|
Available search filters:
|
||||||
|
- cid CID
|
||||||
|
- oid OID
|
||||||
|
- addr CID/OID
|
||||||
|
`,
|
||||||
|
Run: tuiFunc,
|
||||||
|
}
|
||||||
|
|
||||||
|
var initialPrompt string
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
common.AddComponentPathFlag(tuiCMD, &vPath)
|
||||||
|
|
||||||
|
tuiCMD.Flags().StringVar(
|
||||||
|
&initialPrompt,
|
||||||
|
"filter",
|
||||||
|
"",
|
||||||
|
"Filter prompt to start with, format 'tag:value [+ tag:value]...'",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func tuiFunc(cmd *cobra.Command, _ []string) {
|
||||||
|
common.ExitOnErr(cmd, runTUI(cmd))
|
||||||
|
}
|
||||||
|
|
||||||
|
func runTUI(cmd *cobra.Command) error {
|
||||||
|
db, err := openDB(false)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("couldn't open database: %w", err)
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(cmd.Context())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
app := tview.NewApplication()
|
||||||
|
ui := tui.NewUI(ctx, app, db, schema.BlobovniczaParser, nil)
|
||||||
|
|
||||||
|
_ = ui.AddFilter("cid", tui.CIDParser, "CID")
|
||||||
|
_ = ui.AddFilter("oid", tui.OIDParser, "OID")
|
||||||
|
_ = ui.AddCompositeFilter("addr", tui.AddressParser, "CID/OID")
|
||||||
|
|
||||||
|
err = ui.WithPrompt(initialPrompt)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid filter prompt: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
app.SetRoot(ui, true).SetFocus(ui)
|
||||||
|
return app.Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
func openDB(writable bool) (*bbolt.DB, error) {
|
||||||
|
db, err := bbolt.Open(vPath, 0o600, &bbolt.Options{
|
||||||
|
ReadOnly: !writable,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return db, nil
|
||||||
|
}
|
96
cmd/frostfs-lens/internal/schema/blobovnicza/parsers.go
Normal file
96
cmd/frostfs-lens/internal/schema/blobovnicza/parsers.go
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
package blobovnicza
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-lens/internal/schema/common"
|
||||||
|
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
|
||||||
|
oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id"
|
||||||
|
"github.com/mr-tron/base58"
|
||||||
|
)
|
||||||
|
|
||||||
|
var BlobovniczaParser = common.WithFallback(
|
||||||
|
common.Any(
|
||||||
|
MetaBucketParser,
|
||||||
|
BucketParser,
|
||||||
|
),
|
||||||
|
common.RawParser.ToFallbackParser(),
|
||||||
|
)
|
||||||
|
|
||||||
|
func MetaBucketParser(key, value []byte) (common.SchemaEntry, common.Parser, error) {
|
||||||
|
if value != nil {
|
||||||
|
return nil, nil, errors.New("not a bucket")
|
||||||
|
}
|
||||||
|
|
||||||
|
if string(key) != "META" {
|
||||||
|
return nil, nil, errors.New("invalid bucket name")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &MetaBucket{}, MetaRecordParser, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func MetaRecordParser(key, value []byte) (common.SchemaEntry, common.Parser, error) {
|
||||||
|
var r MetaRecord
|
||||||
|
|
||||||
|
if len(key) == 0 {
|
||||||
|
return nil, nil, errors.New("invalid key")
|
||||||
|
}
|
||||||
|
|
||||||
|
r.label = string(key)
|
||||||
|
r.count = binary.LittleEndian.Uint64(value)
|
||||||
|
|
||||||
|
return &r, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func BucketParser(key, value []byte) (common.SchemaEntry, common.Parser, error) {
|
||||||
|
if value != nil {
|
||||||
|
return nil, nil, errors.New("not a bucket")
|
||||||
|
}
|
||||||
|
|
||||||
|
size, n := binary.Varint(key)
|
||||||
|
if n <= 0 {
|
||||||
|
return nil, nil, errors.New("invalid size")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Bucket{size: size}, RecordParser, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func RecordParser(key, value []byte) (common.SchemaEntry, common.Parser, error) {
|
||||||
|
parts := strings.Split(string(key), "/")
|
||||||
|
|
||||||
|
if len(parts) != 2 {
|
||||||
|
return nil, nil, errors.New("invalid key, expected address string <CID>/<OID>")
|
||||||
|
}
|
||||||
|
|
||||||
|
cnrRaw, err := base58.Decode(parts[0])
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, errors.New("can't decode CID string")
|
||||||
|
}
|
||||||
|
objRaw, err := base58.Decode(parts[1])
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, errors.New("can't decode OID string")
|
||||||
|
}
|
||||||
|
|
||||||
|
cnr := cid.ID{}
|
||||||
|
if err := cnr.Decode(cnrRaw); err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("can't decode CID: %w", err)
|
||||||
|
}
|
||||||
|
obj := oid.ID{}
|
||||||
|
if err := obj.Decode(objRaw); err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("can't decode OID: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var r Record
|
||||||
|
|
||||||
|
r.addr.SetContainer(cnr)
|
||||||
|
r.addr.SetObject(obj)
|
||||||
|
|
||||||
|
if err := r.object.Unmarshal(value); err != nil {
|
||||||
|
return nil, nil, errors.New("can't unmarshal object")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &r, nil, nil
|
||||||
|
}
|
101
cmd/frostfs-lens/internal/schema/blobovnicza/types.go
Normal file
101
cmd/frostfs-lens/internal/schema/blobovnicza/types.go
Normal file
|
@ -0,0 +1,101 @@
|
||||||
|
package blobovnicza
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-lens/internal/schema/common"
|
||||||
|
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
|
||||||
|
objectSDK "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object"
|
||||||
|
oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id"
|
||||||
|
"github.com/davecgh/go-spew/spew"
|
||||||
|
"github.com/gdamore/tcell/v2"
|
||||||
|
"github.com/rivo/tview"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
MetaBucket struct{}
|
||||||
|
|
||||||
|
MetaRecord struct {
|
||||||
|
label string
|
||||||
|
count uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
Bucket struct {
|
||||||
|
size int64
|
||||||
|
}
|
||||||
|
|
||||||
|
Record struct {
|
||||||
|
addr oid.Address
|
||||||
|
object objectSDK.Object
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func (b *MetaBucket) String() string {
|
||||||
|
return common.FormatSimple("META", tcell.ColorLime)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *MetaBucket) DetailedString() string {
|
||||||
|
return spew.Sdump(*b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *MetaBucket) Filter(string, any) common.FilterResult {
|
||||||
|
return common.No
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *MetaRecord) String() string {
|
||||||
|
return fmt.Sprintf("%-11s %c %d", r.label, tview.Borders.Vertical, r.count)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *MetaRecord) DetailedString() string {
|
||||||
|
return spew.Sdump(*r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *MetaRecord) Filter(string, any) common.FilterResult {
|
||||||
|
return common.No
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bucket) String() string {
|
||||||
|
return common.FormatSimple(strconv.FormatInt(b.size, 10), tcell.ColorLime)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bucket) DetailedString() string {
|
||||||
|
return spew.Sdump(*b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bucket) Filter(typ string, _ any) common.FilterResult {
|
||||||
|
switch typ {
|
||||||
|
case "cid":
|
||||||
|
return common.Maybe
|
||||||
|
case "oid":
|
||||||
|
return common.Maybe
|
||||||
|
default:
|
||||||
|
return common.No
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Record) String() string {
|
||||||
|
return fmt.Sprintf(
|
||||||
|
"CID %s OID %s %c Object {...}",
|
||||||
|
common.FormatSimple(fmt.Sprintf("%-44s", r.addr.Container()), tcell.ColorAqua),
|
||||||
|
common.FormatSimple(fmt.Sprintf("%-44s", r.addr.Object()), tcell.ColorAqua),
|
||||||
|
tview.Borders.Vertical,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Record) DetailedString() string {
|
||||||
|
return spew.Sdump(*r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Record) Filter(typ string, val any) common.FilterResult {
|
||||||
|
switch typ {
|
||||||
|
case "cid":
|
||||||
|
id := val.(cid.ID)
|
||||||
|
return common.IfThenElse(r.addr.Container().Equals(id), common.Yes, common.No)
|
||||||
|
case "oid":
|
||||||
|
id := val.(oid.ID)
|
||||||
|
return common.IfThenElse(r.addr.Object().Equals(id), common.Yes, common.No)
|
||||||
|
default:
|
||||||
|
return common.No
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue