diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d3a92bb..51e7aaa1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,10 +11,12 @@ Changelog for NeoFS Node - Background workers and object service now use separate client caches (#2048) - `replicator.pool_size` config field to tune replicator pool size (#2049) - Fix NNS hash parsing in morph client (#2063) +- `neofs-cli neofs-cli acl basic/extended print` commands (#2012) ### Changed - `object lock` command reads CID and OID the same way other commands do (#1971) - `LOCK` object are stored on every container node (#1502) +- `neofs-cli container get-eacl` print ACL table in json format only with arg `--json' (#2012) ### Fixed - Open FSTree in sync mode by default (#1992) diff --git a/cmd/neofs-cli/modules/acl/basic/print.go b/cmd/neofs-cli/modules/acl/basic/print.go new file mode 100644 index 00000000..68df86cb --- /dev/null +++ b/cmd/neofs-cli/modules/acl/basic/print.go @@ -0,0 +1,28 @@ +package basic + +import ( + "github.com/nspcc-dev/neofs-node/cmd/neofs-cli/internal/common" + "github.com/nspcc-dev/neofs-node/cmd/neofs-cli/modules/util" + "github.com/nspcc-dev/neofs-sdk-go/container/acl" + "github.com/spf13/cobra" +) + +var printACLCmd = &cobra.Command{ + Use: "print", + Short: "Pretty print basic ACL from the HEX representation", + Example: `neofs-cli acl basic print 0x1C8C8CCC`, + Long: `Pretty print basic ACL from the HEX representation. +Few roles have exclusive default access to set of operation, even if particular bit deny it. +Container have access to the operations of the data replication mechanism: + Get, Head, Put, Search, Hash. +InnerRing members are allowed to data audit ops only: + Get, Head, Hash, Search.`, + Run: printACL, + Args: cobra.ExactArgs(1), +} + +func printACL(cmd *cobra.Command, args []string) { + var bacl acl.Basic + common.ExitOnErr(cmd, "unable to parse basic acl: %w", bacl.DecodeString(args[0])) + util.PrettyPrintTableBACL(cmd, &bacl) +} diff --git a/cmd/neofs-cli/modules/acl/basic/root.go b/cmd/neofs-cli/modules/acl/basic/root.go new file mode 100644 index 00000000..04222002 --- /dev/null +++ b/cmd/neofs-cli/modules/acl/basic/root.go @@ -0,0 +1,14 @@ +package basic + +import ( + "github.com/spf13/cobra" +) + +var Cmd = &cobra.Command{ + Use: "basic", + Short: "Operations with Basic Access Control Lists", +} + +func init() { + Cmd.AddCommand(printACLCmd) +} diff --git a/cmd/neofs-cli/modules/acl/extended/create.go b/cmd/neofs-cli/modules/acl/extended/create.go index 7add3ec5..53d74ecb 100644 --- a/cmd/neofs-cli/modules/acl/extended/create.go +++ b/cmd/neofs-cli/modules/acl/extended/create.go @@ -2,16 +2,13 @@ package extended import ( "bytes" - "crypto/ecdsa" "encoding/json" - "errors" - "fmt" "os" "strings" - "github.com/flynn-archive/go-shlex" - "github.com/nspcc-dev/neo-go/pkg/crypto/keys" + "github.com/nspcc-dev/neofs-node/cmd/neofs-cli/internal/common" "github.com/nspcc-dev/neofs-node/cmd/neofs-cli/internal/commonflags" + "github.com/nspcc-dev/neofs-node/cmd/neofs-cli/modules/util" cid "github.com/nspcc-dev/neofs-sdk-go/container/id" "github.com/nspcc-dev/neofs-sdk-go/eacl" "github.com/spf13/cobra" @@ -52,7 +49,7 @@ neofs-cli acl extended create --cid EutHBsdT1YCzHxjCfQHnLPL1vFrkSyLSio4vkphfnEk func init() { createCmd.Flags().StringArrayP("rule", "r", nil, "Extended ACL table record to apply") - createCmd.Flags().StringP("file", "f", "", "Read list of extended ACL table records from from text file") + createCmd.Flags().StringP("file", "f", "", "Read list of extended ACL table records from text file") createCmd.Flags().StringP("out", "o", "", "Save JSON formatted extended ACL table in file") createCmd.Flags().StringP(commonflags.CIDFlag, "", "", commonflags.CIDFlagUsage) @@ -87,20 +84,7 @@ func createEACL(cmd *cobra.Command, _ []string) { } tb := eacl.NewTable() - - for _, ruleStr := range rules { - r, err := shlex.Split(ruleStr) - if err != nil { - cmd.PrintErrf("can't parse rule '%s': %v\n", ruleStr, err) - os.Exit(1) - } - - err = parseTable(tb, r) - if err != nil { - cmd.PrintErrf("can't create extended ACL record from rule '%s': %v\n", ruleStr, err) - os.Exit(1) - } - } + common.ExitOnErr(cmd, "unable to parse provided rules: %w", util.ParseEACLRules(tb, rules)) tb.SetCID(containerID) @@ -141,144 +125,3 @@ func getRulesFromFile(filename string) ([]string, error) { return strings.Split(strings.TrimSpace(string(data)), "\n"), nil } - -// parseTable parses eACL table from the following form: -// [ ...] [ ...] -// -// Examples: -// allow get req:X-Header=123 obj:Attr=value others:0xkey1,key2 system:key3 user:key4 -// -//nolint:godot -func parseTable(tb *eacl.Table, args []string) error { - if len(args) < 2 { - return errors.New("at least 2 arguments must be provided") - } - - var action eacl.Action - if !action.FromString(strings.ToUpper(args[0])) { - return errors.New("invalid action (expected 'allow' or 'deny')") - } - - ops, err := parseOperations(args[1]) - if err != nil { - return err - } - - r, err := parseRecord(args[2:]) - if err != nil { - return err - } - - r.SetAction(action) - - for _, op := range ops { - r := *r - r.SetOperation(op) - tb.AddRecord(&r) - } - - return nil -} - -func parseRecord(args []string) (*eacl.Record, error) { - r := new(eacl.Record) - for i := range args { - ss := strings.SplitN(args[i], ":", 2) - - switch prefix := strings.ToLower(ss[0]); prefix { - case "req", "obj": // filters - if len(ss) != 2 { - return nil, fmt.Errorf("invalid filter or target: %s", args[i]) - } - - i := strings.Index(ss[1], "=") - if i < 0 { - return nil, fmt.Errorf("invalid filter key-value pair: %s", ss[1]) - } - - var key, value string - var op eacl.Match - - if 0 < i && ss[1][i-1] == '!' { - key = ss[1][:i-1] - op = eacl.MatchStringNotEqual - } else { - key = ss[1][:i] - op = eacl.MatchStringEqual - } - - value = ss[1][i+1:] - - typ := eacl.HeaderFromRequest - if ss[0] == "obj" { - typ = eacl.HeaderFromObject - } - - r.AddFilter(typ, op, key, value) - case "others", "system", "user", "pubkey": // targets - var err error - - var pubs []ecdsa.PublicKey - if len(ss) == 2 { - pubs, err = parseKeyList(ss[1]) - if err != nil { - return nil, err - } - } - - var role eacl.Role - if prefix != "pubkey" { - role, err = roleFromString(prefix) - if err != nil { - return nil, err - } - } - - eacl.AddFormedTarget(r, role, pubs...) - - default: - return nil, fmt.Errorf("invalid prefix: %s", ss[0]) - } - } - - return r, nil -} - -func roleFromString(s string) (eacl.Role, error) { - var r eacl.Role - if !r.FromString(strings.ToUpper(s)) { - return r, fmt.Errorf("unexpected role %s", s) - } - - return r, nil -} - -// parseKeyList parses list of hex-encoded public keys separated by comma. -func parseKeyList(s string) ([]ecdsa.PublicKey, error) { - ss := strings.Split(s, ",") - pubs := make([]ecdsa.PublicKey, len(ss)) - for i := range ss { - st := strings.TrimPrefix(ss[i], "0x") - pub, err := keys.NewPublicKeyFromString(st) - if err != nil { - return nil, fmt.Errorf("invalid public key '%s': %w", ss[i], err) - } - - pubs[i] = ecdsa.PublicKey(*pub) - } - - return pubs, nil -} - -func parseOperations(s string) ([]eacl.Operation, error) { - ss := strings.Split(s, ",") - ops := make([]eacl.Operation, len(ss)) - - for i := range ss { - if !ops[i].FromString(strings.ToUpper(ss[i])) { - return nil, fmt.Errorf("invalid operation: %s", ss[i]) - } - } - - return ops, nil -} diff --git a/cmd/neofs-cli/modules/acl/extended/create_test.go b/cmd/neofs-cli/modules/acl/extended/create_test.go index 19350440..16d4cc02 100644 --- a/cmd/neofs-cli/modules/acl/extended/create_test.go +++ b/cmd/neofs-cli/modules/acl/extended/create_test.go @@ -1,9 +1,9 @@ package extended import ( - "strings" "testing" + "github.com/nspcc-dev/neofs-node/cmd/neofs-cli/modules/util" "github.com/nspcc-dev/neofs-sdk-go/eacl" "github.com/stretchr/testify/require" ) @@ -63,8 +63,7 @@ func TestParseTable(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { - ss := strings.Split(test.rule, " ") - err := parseTable(eaclTable, ss) + err := util.ParseEACLRule(eaclTable, test.rule) ok := len(test.jsonRecord) > 0 require.Equal(t, ok, err == nil, err) if ok { diff --git a/cmd/neofs-cli/modules/acl/extended/print.go b/cmd/neofs-cli/modules/acl/extended/print.go new file mode 100644 index 00000000..6a0694e8 --- /dev/null +++ b/cmd/neofs-cli/modules/acl/extended/print.go @@ -0,0 +1,38 @@ +package extended + +import ( + "os" + "strings" + + "github.com/nspcc-dev/neofs-node/cmd/neofs-cli/internal/common" + "github.com/nspcc-dev/neofs-node/cmd/neofs-cli/modules/util" + "github.com/nspcc-dev/neofs-sdk-go/eacl" + "github.com/spf13/cobra" +) + +var printEACLCmd = &cobra.Command{ + Use: "print", + Short: "Pretty print extended ACL from the file(in text or json format) or for given container.", + Run: printEACL, +} + +func init() { + flags := printEACLCmd.Flags() + flags.StringP("file", "f", "", + "Read list of extended ACL table records from text or json file") + _ = printEACLCmd.MarkFlagRequired("file") +} + +func printEACL(cmd *cobra.Command, _ []string) { + file, _ := cmd.Flags().GetString("file") + eaclTable := new(eacl.Table) + data, err := os.ReadFile(file) + common.ExitOnErr(cmd, "can't read file with EACL: %w", err) + if strings.HasSuffix(file, ".json") { + common.ExitOnErr(cmd, "unable to parse json: %w", eaclTable.UnmarshalJSON(data)) + } else { + rules := strings.Split(strings.TrimSpace(string(data)), "\n") + common.ExitOnErr(cmd, "can't parse file with EACL: %w", util.ParseEACLRules(eaclTable, rules)) + } + util.PrettyPrintTableEACL(cmd, eaclTable) +} diff --git a/cmd/neofs-cli/modules/acl/extended/root.go b/cmd/neofs-cli/modules/acl/extended/root.go index 67365254..8ec2915d 100644 --- a/cmd/neofs-cli/modules/acl/extended/root.go +++ b/cmd/neofs-cli/modules/acl/extended/root.go @@ -1,6 +1,8 @@ package extended -import "github.com/spf13/cobra" +import ( + "github.com/spf13/cobra" +) var Cmd = &cobra.Command{ Use: "extended", @@ -9,4 +11,5 @@ var Cmd = &cobra.Command{ func init() { Cmd.AddCommand(createCmd) + Cmd.AddCommand(printEACLCmd) } diff --git a/cmd/neofs-cli/modules/acl/root.go b/cmd/neofs-cli/modules/acl/root.go index 0fc3ecea..83867cf6 100644 --- a/cmd/neofs-cli/modules/acl/root.go +++ b/cmd/neofs-cli/modules/acl/root.go @@ -1,6 +1,7 @@ package acl import ( + "github.com/nspcc-dev/neofs-node/cmd/neofs-cli/modules/acl/basic" "github.com/nspcc-dev/neofs-node/cmd/neofs-cli/modules/acl/extended" "github.com/spf13/cobra" ) @@ -12,4 +13,5 @@ var Cmd = &cobra.Command{ func init() { Cmd.AddCommand(extended.Cmd) + Cmd.AddCommand(basic.Cmd) } diff --git a/cmd/neofs-cli/modules/container/get.go b/cmd/neofs-cli/modules/container/get.go index 7959c662..846b8a8c 100644 --- a/cmd/neofs-cli/modules/container/get.go +++ b/cmd/neofs-cli/modules/container/get.go @@ -10,6 +10,7 @@ import ( "github.com/nspcc-dev/neofs-node/cmd/neofs-cli/internal/common" "github.com/nspcc-dev/neofs-node/cmd/neofs-cli/internal/commonflags" "github.com/nspcc-dev/neofs-node/cmd/neofs-cli/internal/key" + "github.com/nspcc-dev/neofs-node/cmd/neofs-cli/modules/util" "github.com/nspcc-dev/neofs-sdk-go/container" "github.com/nspcc-dev/neofs-sdk-go/container/acl" cid "github.com/nspcc-dev/neofs-sdk-go/container/id" @@ -141,6 +142,7 @@ func prettyPrintBasicACL(cmd *cobra.Command, basicACL acl.Basic) { } cmd.Println() + util.PrettyPrintTableBACL(cmd, &basicACL) } func getContainer(cmd *cobra.Command) (container.Container, *ecdsa.PrivateKey) { diff --git a/cmd/neofs-cli/modules/util/acl.go b/cmd/neofs-cli/modules/util/acl.go new file mode 100644 index 00000000..db9790c4 --- /dev/null +++ b/cmd/neofs-cli/modules/util/acl.go @@ -0,0 +1,330 @@ +package util + +import ( + "bytes" + "crypto/ecdsa" + "encoding/hex" + "errors" + "fmt" + "strings" + "text/tabwriter" + + "github.com/flynn-archive/go-shlex" + "github.com/nspcc-dev/neo-go/pkg/crypto/keys" + "github.com/nspcc-dev/neofs-sdk-go/container/acl" + "github.com/nspcc-dev/neofs-sdk-go/eacl" + "github.com/olekukonko/tablewriter" + "github.com/spf13/cobra" +) + +// PrettyPrintTableBACL print basic ACL in table format. +func PrettyPrintTableBACL(cmd *cobra.Command, bacl *acl.Basic) { + // Header + w := tabwriter.NewWriter(cmd.OutOrStdout(), 1, 4, 4, ' ', 0) + fmt.Fprintln(w, "\tRangeHASH\tRange\tSearch\tDelete\tPut\tHead\tGet") + // Bits + bits := []string{ + boolToString(bacl.Sticky()) + " " + boolToString(bacl.Extendable()), + getRoleBitsForOperation(bacl, acl.OpObjectHash), getRoleBitsForOperation(bacl, acl.OpObjectRange), + getRoleBitsForOperation(bacl, acl.OpObjectSearch), getRoleBitsForOperation(bacl, acl.OpObjectDelete), + getRoleBitsForOperation(bacl, acl.OpObjectPut), getRoleBitsForOperation(bacl, acl.OpObjectHead), + getRoleBitsForOperation(bacl, acl.OpObjectGet), + } + fmt.Fprintln(w, strings.Join(bits, "\t")) + // Footer + footer := []string{"X F"} + for i := 0; i < 7; i++ { + footer = append(footer, "U S O B") + } + fmt.Fprintln(w, strings.Join(footer, "\t")) + + w.Flush() + + cmd.Println(" X-Sticky F-Final U-User S-System O-Others B-Bearer") +} + +func getRoleBitsForOperation(bacl *acl.Basic, op acl.Op) string { + return boolToString(bacl.IsOpAllowed(op, acl.RoleOwner)) + " " + + boolToString(bacl.IsOpAllowed(op, acl.RoleContainer)) + " " + + boolToString(bacl.IsOpAllowed(op, acl.RoleOthers)) + " " + + boolToString(bacl.IsOpAllowed(op, acl.RoleInnerRing)) +} + +func boolToString(b bool) string { + if b { + return "1" + } + return "0" +} + +// PrettyPrintTableEACL print extended ACL in table format. +func PrettyPrintTableEACL(cmd *cobra.Command, table *eacl.Table) { + out := tablewriter.NewWriter(cmd.OutOrStdout()) + out.SetHeader([]string{"Operation", "Action", "Filters", "Targets"}) + out.SetAlignment(tablewriter.ALIGN_CENTER) + out.SetRowLine(true) + + out.SetAutoWrapText(false) + + for _, r := range table.Records() { + out.Append([]string{ + r.Operation().String(), + r.Action().String(), + eaclFiltersToString(r.Filters()), + eaclTargetsToString(r.Targets()), + }) + } + + out.Render() +} + +func eaclTargetsToString(ts []eacl.Target) string { + b := bytes.NewBuffer(nil) + for _, t := range ts { + keysExists := len(t.BinaryKeys()) > 0 + switch t.Role() { + case eacl.RoleUser: + b.WriteString("User") + if keysExists { + b.WriteString(": ") + } + case eacl.RoleSystem: + b.WriteString("System") + if keysExists { + b.WriteString(": ") + } + case eacl.RoleOthers: + b.WriteString("Others") + if keysExists { + b.WriteString(": ") + } + default: + b.WriteString("Unknown") + if keysExists { + b.WriteString(": ") + } + } + + for i, pub := range t.BinaryKeys() { + if i != 0 { + b.WriteString(" ") + } + b.WriteString(hex.EncodeToString(pub)) + b.WriteString("\n") + } + } + + return b.String() +} + +func eaclFiltersToString(fs []eacl.Filter) string { + b := bytes.NewBuffer(nil) + tw := tabwriter.NewWriter(b, 0, 0, 1, ' ', 0) + + for _, f := range fs { + switch f.From() { + case eacl.HeaderFromObject: + _, _ = tw.Write([]byte("O:\t")) + case eacl.HeaderFromRequest: + _, _ = tw.Write([]byte("R:\t")) + case eacl.HeaderFromService: + _, _ = tw.Write([]byte("S:\t")) + default: + _, _ = tw.Write([]byte(" \t")) + } + + _, _ = tw.Write([]byte(f.Key())) + + switch f.Matcher() { + case eacl.MatchStringEqual: + _, _ = tw.Write([]byte("\t==\t")) + case eacl.MatchStringNotEqual: + _, _ = tw.Write([]byte("\t!=\t")) + case eacl.MatchUnknown: + } + + _, _ = tw.Write([]byte(f.Value() + "\t")) + _, _ = tw.Write([]byte("\n")) + } + + _ = tw.Flush() + + // To have nice output with tabwriter, we must append newline + // after the last line. Here we strip it to delete empty line + // in the final output. + s := b.String() + if len(s) > 0 { + s = s[:len(s)-1] + } + + return s +} + +// ParseEACLRules parses eACL table. +// Uses ParseEACLRule. +// +//nolint:godot +func ParseEACLRules(table *eacl.Table, rules []string) error { + if len(rules) == 0 { + return errors.New("no extended ACL rules has been provided") + } + + for _, ruleStr := range rules { + err := ParseEACLRule(table, ruleStr) + if err != nil { + return fmt.Errorf("can't create extended acl record from rule '%s': %v", ruleStr, err) + } + } + return nil +} + +// ParseEACLRule parses eACL table from the following form: +// [ ...] [ ...] +// +// Examples: +// allow get req:X-Header=123 obj:Attr=value others:0xkey1,key2 system:key3 user:key4 +// +//nolint:godot +func ParseEACLRule(table *eacl.Table, rule string) error { + r, err := shlex.Split(rule) + if err != nil { + return fmt.Errorf("can't parse rule '%s': %v", rule, err) + } + return parseEACLTable(table, r) +} + +func parseEACLTable(tb *eacl.Table, args []string) error { + if len(args) < 2 { + return errors.New("at least 2 arguments must be provided") + } + + var action eacl.Action + if !action.FromString(strings.ToUpper(args[0])) { + return errors.New("invalid action (expected 'allow' or 'deny')") + } + + ops, err := eaclOperationsFromString(args[1]) + if err != nil { + return err + } + + r, err := parseEACLRecord(args[2:]) + if err != nil { + return err + } + + r.SetAction(action) + + for _, op := range ops { + r := *r + r.SetOperation(op) + tb.AddRecord(&r) + } + + return nil +} + +func parseEACLRecord(args []string) (*eacl.Record, error) { + r := new(eacl.Record) + for i := range args { + ss := strings.SplitN(args[i], ":", 2) + + switch prefix := strings.ToLower(ss[0]); prefix { + case "req", "obj": // filters + if len(ss) != 2 { + return nil, fmt.Errorf("invalid filter or target: %s", args[i]) + } + + i := strings.Index(ss[1], "=") + if i < 0 { + return nil, fmt.Errorf("invalid filter key-value pair: %s", ss[1]) + } + + var key, value string + var op eacl.Match + + if 0 < i && ss[1][i-1] == '!' { + key = ss[1][:i-1] + op = eacl.MatchStringNotEqual + } else { + key = ss[1][:i] + op = eacl.MatchStringEqual + } + + value = ss[1][i+1:] + + typ := eacl.HeaderFromRequest + if ss[0] == "obj" { + typ = eacl.HeaderFromObject + } + + r.AddFilter(typ, op, key, value) + case "others", "system", "user", "pubkey": // targets + var err error + + var pubs []ecdsa.PublicKey + if len(ss) == 2 { + pubs, err = parseKeyList(ss[1]) + if err != nil { + return nil, err + } + } + + var role eacl.Role + if prefix != "pubkey" { + role, err = eaclRoleFromString(prefix) + if err != nil { + return nil, err + } + } + + eacl.AddFormedTarget(r, role, pubs...) + + default: + return nil, fmt.Errorf("invalid prefix: %s", ss[0]) + } + } + + return r, nil +} + +// eaclRoleFromString parses eacl.Role from string. +func eaclRoleFromString(s string) (eacl.Role, error) { + var r eacl.Role + if !r.FromString(strings.ToUpper(s)) { + return r, fmt.Errorf("unexpected role %s", s) + } + + return r, nil +} + +// parseKeyList parses list of hex-encoded public keys separated by comma. +func parseKeyList(s string) ([]ecdsa.PublicKey, error) { + ss := strings.Split(s, ",") + pubs := make([]ecdsa.PublicKey, len(ss)) + for i := range ss { + st := strings.TrimPrefix(ss[i], "0x") + pub, err := keys.NewPublicKeyFromString(st) + if err != nil { + return nil, fmt.Errorf("invalid public key '%s': %w", ss[i], err) + } + + pubs[i] = ecdsa.PublicKey(*pub) + } + + return pubs, nil +} + +// eaclOperationsFromString parses list of eacl.Operation separated by comma. +func eaclOperationsFromString(s string) ([]eacl.Operation, error) { + ss := strings.Split(s, ",") + ops := make([]eacl.Operation, len(ss)) + + for i := range ss { + if !ops[i].FromString(strings.ToUpper(ss[i])) { + return nil, fmt.Errorf("invalid operation: %s", ss[i]) + } + } + + return ops, nil +} diff --git a/go.mod b/go.mod index 94107d67..5a74a85d 100644 --- a/go.mod +++ b/go.mod @@ -21,6 +21,7 @@ require ( github.com/nspcc-dev/neofs-contract v0.16.0 github.com/nspcc-dev/neofs-sdk-go v1.0.0-rc.7 github.com/nspcc-dev/tzhash v1.6.1 + github.com/olekukonko/tablewriter v0.0.5 github.com/panjf2000/ants/v2 v2.4.0 github.com/paulmach/orb v0.2.2 github.com/prometheus/client_golang v1.13.0 @@ -57,7 +58,7 @@ require ( github.com/inconshreveable/mousetrap v1.0.0 // indirect github.com/ipfs/go-cid v0.0.7 // indirect github.com/magiconair/properties v1.8.6 // indirect - github.com/mattn/go-runewidth v0.0.4 // indirect + github.com/mattn/go-runewidth v0.0.9 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect github.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1 // indirect github.com/minio/sha256-simd v0.1.1-0.20190913151208-6de447530771 // indirect diff --git a/go.sum b/go.sum index 0987165b..ceaed1ac 100644 --- a/go.sum +++ b/go.sum @@ -374,8 +374,9 @@ github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOA github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= -github.com/mattn/go-runewidth v0.0.4 h1:2BvfKmzob6Bmd4YsL0zygOqfdFnK7GR4QL06Do4/p7Y= github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= @@ -474,6 +475,8 @@ github.com/nspcc-dev/tzhash v1.6.1 h1:8dUrWFpjkmoHF+7GxuGUmarj9LLHWFcuyF3CTrqq9J github.com/nspcc-dev/tzhash v1.6.1/go.mod h1:BoflzCVp+DO/f1mvbcsJQWoFzidIFBhWFZMglbUW648= github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= +github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=