diff --git a/cmd/swarm-rafttool/dump.go b/cmd/swarm-rafttool/dump.go index 481a0ba6c4..a395e26ad5 100644 --- a/cmd/swarm-rafttool/dump.go +++ b/cmd/swarm-rafttool/dump.go @@ -1,9 +1,12 @@ package main import ( + "bytes" "context" + "encoding/json" "errors" "fmt" + "io" "os" "path/filepath" @@ -82,19 +85,52 @@ func dumpWAL(swarmdir, unlockKey string, start, end uint64, redact bool) error { return err } - for _, ent := range walData.Entries { + prefix := "" + if format == "json" && len(walData.Entries) > 1 { + fmt.Println("[") + fmt.Print(" ") + prefix = " " + } + for idx, ent := range walData.Entries { if (start == 0 || ent.Index >= start) && (end == 0 || ent.Index <= end) { - fmt.Printf("Entry Index=%d, Term=%d, Type=%s:\n", ent.Index, ent.Term, ent.Type.String()) + if format == "text" { + fmt.Printf("Entry Index=%d, Term=%d, Type=%s:\n", ent.Index, ent.Term, ent.Type.String()) + } switch ent.Type { case raftpb.EntryConfChange: + cc := &raftpb.ConfChange{} err := proto.Unmarshal(ent.Data, cc) if err != nil { return err } - - fmt.Println("Conf change type:", cc.Type.String()) - fmt.Printf("Node ID: %x\n\n", cc.NodeID) + switch format { + case "text": + fmt.Println("Conf change type:", cc.Type.String()) + fmt.Printf("Node ID: %x\n\n", cc.NodeID) + fmt.Println() + case "json": + bytesBuffer := new(bytes.Buffer) + b, err := json.MarshalIndent(struct { + Term uint64 + Index uint64 + Type raftpb.EntryType + Data *raftpb.ConfChange + }{ + Term: ent.Term, + Index: ent.Index, + Type: ent.Type, + Data: cc, + }, prefix, " ") + if err != nil { + return err + } + bytesBuffer.Write(b) + if idx < len(walData.Entries)-1 { + bytesBuffer.WriteString(",\n ") + } + io.Copy(os.Stdout, bytesBuffer) + } case raftpb.EntryNormal: r := &api.InternalRaftRequest{} @@ -135,13 +171,41 @@ func dumpWAL(swarmdir, unlockKey string, start, end uint64, redact bool) error { } } - if err := proto.MarshalText(os.Stdout, r); err != nil { - return err + switch format { + case "text": + if err := proto.MarshalText(os.Stdout, r); err != nil { + return err + } + fmt.Println() + case "json": + bytesBuffer := new(bytes.Buffer) + b, err := json.MarshalIndent(struct { + Term uint64 + Index uint64 + Type raftpb.EntryType + Data *api.InternalRaftRequest + }{ + Term: ent.Term, + Index: ent.Index, + Type: ent.Type, + Data: r, + }, prefix, " ") + if err != nil { + return err + } + bytesBuffer.Write(b) + if idx < len(walData.Entries)-1 { + bytesBuffer.WriteString(",\n ") + } + io.Copy(os.Stdout, bytesBuffer) } - fmt.Println() } } } + if format == "json" && len(walData.Entries) > 1 { + fmt.Println() + fmt.Println("]") + } return nil } @@ -164,18 +228,6 @@ func dumpSnapshot(swarmdir, unlockKey string, redact bool) error { return fmt.Errorf("unrecognized snapshot version %d", s.Version) } - fmt.Println("Active members:") - for _, member := range s.Membership.Members { - fmt.Printf(" NodeID=%s, RaftID=%x, Addr=%s\n", member.NodeID, member.RaftID, member.Addr) - } - fmt.Println() - - fmt.Println("Removed members:") - for _, member := range s.Membership.Removed { - fmt.Printf(" RaftID=%x\n", member) - } - fmt.Println() - if redact { for _, cluster := range s.Store.Clusters { if cluster != nil { @@ -218,11 +270,32 @@ func dumpSnapshot(swarmdir, unlockKey string, redact bool) error { } } - fmt.Println("Objects:") - if err := proto.MarshalText(os.Stdout, &s.Store); err != nil { - return err + switch format { + case "text": + fmt.Println("Active members:") + for _, member := range s.Membership.Members { + fmt.Printf(" NodeID=%s, RaftID=%x, Addr=%s\n", member.NodeID, member.RaftID, member.Addr) + } + fmt.Println() + + fmt.Println("Removed members:") + for _, member := range s.Membership.Removed { + fmt.Printf(" RaftID=%x\n", member) + } + fmt.Println() + + fmt.Println("Objects:") + if err := proto.MarshalText(os.Stdout, &s.Store); err != nil { + return err + } + fmt.Println() + case "json": + enc := json.NewEncoder(os.Stdout) + enc.SetIndent("", " ") + if err := enc.Encode(&s); err != nil { + return err + } } - fmt.Println() return nil } @@ -451,11 +524,35 @@ func dumpObject(swarmdir, unlockKey, objType string, selector objSelector) error return fmt.Errorf("no matching objects found") } - for _, object := range objects { - if err := proto.MarshalText(os.Stdout, object); err != nil { - return err + prefix := "" + if format == "json" && len(objects) > 1 { + fmt.Println("[") + fmt.Print(" ") + prefix = " " + } + for idx, object := range objects { + switch format { + case "text": + if err := proto.MarshalText(os.Stdout, object); err != nil { + return err + } + fmt.Println() + case "json": + bytesBuffer := new(bytes.Buffer) + b, err := json.MarshalIndent(object, prefix, " ") + if err != nil { + return err + } + bytesBuffer.Write(b) + if idx < len(objects)-1 { + bytesBuffer.WriteString(",\n ") + } + io.Copy(os.Stdout, bytesBuffer) } + } + if format == "json" && len(objects) > 1 { fmt.Println() + fmt.Println("]") } return nil diff --git a/cmd/swarm-rafttool/main.go b/cmd/swarm-rafttool/main.go index a5d2963c6e..dc27fe604c 100644 --- a/cmd/swarm-rafttool/main.go +++ b/cmd/swarm-rafttool/main.go @@ -10,6 +10,8 @@ import ( ) var ( + format string + mainCmd = &cobra.Command{ Use: os.Args[0], Short: "Tool to translate and decrypt the raft logs of a swarm manager", @@ -45,6 +47,9 @@ var ( dumpWALCmd = &cobra.Command{ Use: "dump-wal", Short: "Display entries from the Raft log", + PreRunE: func(cmd *cobra.Command, args []string) error { + return validateFormatFlag(format) + }, RunE: func(cmd *cobra.Command, args []string) error { stateDir, err := cmd.Flags().GetString("state-dir") if err != nil { @@ -78,6 +83,9 @@ var ( dumpSnapshotCmd = &cobra.Command{ Use: "dump-snapshot", Short: "Display entries from the latest Raft snapshot", + PreRunE: func(cmd *cobra.Command, args []string) error { + return validateFormatFlag(format) + }, RunE: func(cmd *cobra.Command, args []string) error { stateDir, err := cmd.Flags().GetString("state-dir") if err != nil { @@ -101,6 +109,9 @@ var ( dumpObjectCmd = &cobra.Command{ Use: "dump-object [type]", Short: "Display an object from the Raft snapshot/WAL", + PreRunE: func(cmd *cobra.Command, args []string) error { + return validateFormatFlag(format) + }, RunE: func(cmd *cobra.Command, args []string) error { if len(args) != 1 { return errors.New("dump-object subcommand takes exactly 1 argument") @@ -159,6 +170,15 @@ var ( } ) +func validateFormatFlag(format string) error { + switch format { + case "text", "json": + return nil + default: + return fmt.Errorf("invalid 'format' %s", format) + } +} + func init() { mainCmd.PersistentFlags().StringP("state-dir", "d", defaults.StateDir, "State directory") mainCmd.PersistentFlags().String("unlock-key", "", "Unlock key, if raft logs are encrypted") @@ -172,13 +192,16 @@ func init() { ) dumpSnapshotCmd.Flags().Bool("redact", false, "Redact the values of secrets, configs, and environment variables") + dumpSnapshotCmd.Flags().StringVar(&format, "format", "text", `Output format (text|json)`) dumpWALCmd.Flags().Uint64("start", 0, "Start of index range to dump") dumpWALCmd.Flags().Uint64("end", 0, "End of index range to dump") dumpWALCmd.Flags().Bool("redact", false, "Redact the values of secrets, configs, and environment variables") + dumpWALCmd.Flags().StringVar(&format, "format", "text", `Output format (text|json)`) dumpObjectCmd.Flags().String("id", "", "Look up object by ID") dumpObjectCmd.Flags().String("name", "", "Look up object by name") + dumpObjectCmd.Flags().StringVar(&format, "format", "text", `Output format (text|json)`) } func main() {