From e6aba48571fa3ca09f656c47b0d9ff0ee23bf5cb Mon Sep 17 00:00:00 2001 From: pradhyum6144 Date: Mon, 4 May 2026 00:57:37 +0530 Subject: [PATCH] feat(fxconfig): add --format flag to namespace list with JSON/YAML output MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds structured output support to 'fxconfig namespace list' via a new --format flag (json|yaml|table). The table format preserves the existing output for backward compatibility. For json and yaml formats, the command serialises a typed slice whose fields include a decoded policyString alongside the raw policy bytes. Policy decoding is implemented in the CLI layer: - ThresholdRule → base64-encoded public key - MspRule → proto text representation of SignaturePolicyEnvelope - Unknown/invalid → hex fallback The commented-out parsePolicy stub in app/list.go (which had the wrong unmarshal target type — SignaturePolicy instead of SignaturePolicyEnvelope) is removed; the correct implementation now lives in the CLI layer where display concerns belong. Tests added: - JSON output validates structure and field presence - YAML output validates name and version fields - Invalid format returns a descriptive error Signed-off-by: pradhyum6144 --- tools/fxconfig/internal/app/list.go | 26 +---- .../internal/cli/v1/namespace_list.go | 100 ++++++++++++++++-- .../internal/cli/v1/namespace_list_test.go | 75 ++++++++++++- 3 files changed, 161 insertions(+), 40 deletions(-) diff --git a/tools/fxconfig/internal/app/list.go b/tools/fxconfig/internal/app/list.go index fe7d70f..7b2be99 100644 --- a/tools/fxconfig/internal/app/list.go +++ b/tools/fxconfig/internal/app/list.go @@ -12,8 +12,7 @@ import ( ) // ListNamespaces queries the committer service for installed namespaces. -// It connects to the query service, retrieves all namespace policies, and formats -// the Output showing namespace names, versions, and policy data in hexadecimal. +// It connects to the query service and retrieves all namespace policies. func (d *AdminApp) ListNamespaces(ctx context.Context) ([]NamespaceQueryResult, error) { // get query service instance qc, err := d.QueryProvider.Get() @@ -47,26 +46,3 @@ type NamespaceQueryResult struct { Version int `json:"version" yaml:"version"` Policy []byte `json:"policy" yaml:"policy"` } - -// parsePolicy extracts and formats policy information from serialized bytes. -// Returns base64-encoded public key for threshold policies or string representation for MSP policies. -// func parsePolicy(b []byte) string { -// var p applicationpb.NamespacePolicy -// if err := proto.Unmarshal(b, &p); err != nil { -// panic(err) -// } -// -// switch r := p.Rule.(type) { -// case *applicationpb.NamespacePolicy_ThresholdRule: -// return base64.StdEncoding.EncodeToString(r.ThresholdRule.GetPublicKey()) -// case *applicationpb.NamespacePolicy_MspRule: -// var en common.SignaturePolicy -// if err := proto.Unmarshal(r.MspRule, &en); err != nil { -// panic(err) -// } -// // TODO: some pretty print would be beautiful -// return en.String() -// default: -// return "error parsing policy" -// } -// } diff --git a/tools/fxconfig/internal/cli/v1/namespace_list.go b/tools/fxconfig/internal/cli/v1/namespace_list.go index b42a893..2d31d03 100644 --- a/tools/fxconfig/internal/cli/v1/namespace_list.go +++ b/tools/fxconfig/internal/cli/v1/namespace_list.go @@ -7,15 +7,82 @@ SPDX-License-Identifier: Apache-2.0 package v1 import ( + "encoding/base64" "fmt" + "strings" + cb "github.com/hyperledger/fabric-protos-go-apiv2/common" "github.com/spf13/cobra" + "google.golang.org/protobuf/proto" + + "github.com/hyperledger/fabric-x-common/api/applicationpb" + "github.com/hyperledger/fabric-x/tools/fxconfig/internal/app" + "github.com/hyperledger/fabric-x/tools/fxconfig/internal/cli/v1/cliio" ) +// namespaceListEntry is the display representation of a namespace query result. +type namespaceListEntry struct { + Name string `json:"name" yaml:"name"` + Version int `json:"version" yaml:"version"` + Policy []byte `json:"policy" yaml:"policy"` + PolicyString string `json:"policyString" yaml:"policyString"` +} + +// namespaceListOutput is a slice of namespaceListEntry with table rendering support. +type namespaceListOutput []namespaceListEntry + +// String renders the namespace list as a human-readable table. +func (r namespaceListOutput) String() string { + var sb strings.Builder + sb.WriteString(fmt.Sprintf("Installed namespaces (%d total):\n", len(r))) + for i, p := range r { + sb.WriteString(fmt.Sprintf("%d) %v: version %d policy: %x\n", i, p.Name, p.Version, p.Policy)) + } + return sb.String() +} + +func toListOutput(results []app.NamespaceQueryResult) namespaceListOutput { + out := make(namespaceListOutput, len(results)) + for i, r := range results { + out[i] = namespaceListEntry{ + Name: r.NsID, + Version: r.Version, + Policy: r.Policy, + PolicyString: parsePolicy(r.Policy), + } + } + return out +} + +// parsePolicy decodes namespace policy bytes into a human-readable string. +// For threshold policies, returns the base64-encoded public key. +// For MSP policies, returns the proto text representation of the signature policy envelope. +// Falls back to a hex-encoded string if decoding fails. +func parsePolicy(b []byte) string { + var ns applicationpb.NamespacePolicy + if err := proto.Unmarshal(b, &ns); err != nil { + return fmt.Sprintf("%x", b) + } + + switch r := ns.Rule.(type) { + case *applicationpb.NamespacePolicy_ThresholdRule: + return base64.StdEncoding.EncodeToString(r.ThresholdRule.GetPublicKey()) + case *applicationpb.NamespacePolicy_MspRule: + var spe cb.SignaturePolicyEnvelope + if err := proto.Unmarshal(r.MspRule, &spe); err != nil { + return fmt.Sprintf("%x", r.MspRule) + } + return spe.String() + default: + return fmt.Sprintf("%x", b) + } +} + // newNsListCommand creates a command for listing installed namespaces. // It connects to the query service and displays namespace names, versions, and policies. -// The listFunc is injected to enable testing with mock implementations. func newNsListCommand(ctx *CLIContext) *cobra.Command { + var format string + cmd := &cobra.Command{ Use: "list", Short: "List installed Namespaces", @@ -24,13 +91,18 @@ func newNsListCommand(ctx *CLIContext) *cobra.Command { For each namespace, displays: • Name (namespace identifier) • Version (current version number) - • Policy (endorsement policy in hexadecimal format) + • Policy (endorsement policy) Use this command to: • Verify namespace deployment • Check current version before updates • Audit endorsement policies +Output Formats: + • table Human-readable text (default) + • json Machine-readable JSON array + • yaml Machine-readable YAML list + Examples: # List all namespaces fxconfig namespace list @@ -38,24 +110,30 @@ Examples: # List with custom config fxconfig namespace list --config /path/to/config.yaml - # List and save output to file - fxconfig namespace list > namespaces.txt`, + # Machine-readable JSON output for scripting + fxconfig namespace list --format json + + # YAML output + fxconfig namespace list --format yaml`, RunE: func(cmd *cobra.Command, _ []string) error { + f := cliio.Format(format) + if f != cliio.FormatTable && f != cliio.FormatJSON && f != cliio.FormatYAML { + return fmt.Errorf("invalid --format %q: must be one of json, yaml, table", format) + } + result, err := ctx.App.ListNamespaces(cmd.Context()) if err != nil { return err } - // print namespace policy information to the Output writer. - // Each namespace is displayed with its index, name, version, and policy in hexadecimal format. - ctx.Printer.Print(fmt.Sprintf("Installed namespaces (%d total):\n", len(result))) - for i, p := range result { - ctx.Printer.Print(fmt.Sprintf("%d) %v: version %d policy: %x\n", i, p.NsID, p.Version, p.Policy)) - } - + output := toListOutput(result) + printer := cliio.NewCLIPrinter(cmd.OutOrStdout(), cmd.ErrOrStderr(), f) + printer.Print(output) return nil }, } + cmd.Flags().StringVar(&format, "format", "table", "Output format (json|yaml|table)") + return cmd } diff --git a/tools/fxconfig/internal/cli/v1/namespace_list_test.go b/tools/fxconfig/internal/cli/v1/namespace_list_test.go index 6d7518c..fc7a05f 100644 --- a/tools/fxconfig/internal/cli/v1/namespace_list_test.go +++ b/tools/fxconfig/internal/cli/v1/namespace_list_test.go @@ -9,13 +9,13 @@ package v1 import ( "bytes" "context" + "encoding/json" "testing" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "github.com/hyperledger/fabric-x/tools/fxconfig/internal/app" - "github.com/hyperledger/fabric-x/tools/fxconfig/internal/cli/v1/cliio" ) func TestNewListCommand(t *testing.T) { @@ -29,6 +29,7 @@ func TestNewListCommand(t *testing.T) { require.Equal(t, "list", cmd.Use, "command use should be 'list'") require.NotEmpty(t, cmd.Short, "command should have a short description") require.NotNil(t, cmd.RunE, "command should have a RunE function") + require.NotNil(t, cmd.Flags().Lookup("format"), "command should have a --format flag") } func TestNewListCommandRun(t *testing.T) { @@ -41,9 +42,9 @@ func TestNewListCommandRun(t *testing.T) { } mockApp.On("ListNamespaces", mock.Anything).Return(namespaces, nil) - var out, errOut bytes.Buffer - printer := cliio.NewCLIPrinter(&out, &errOut, cliio.FormatTable) - cmd := newNsListCommand(&CLIContext{App: mockApp, Printer: printer}) + var out bytes.Buffer + cmd := newNsListCommand(&CLIContext{App: mockApp}) + cmd.SetOut(&out) err := cmd.RunE(cmd, nil) @@ -55,6 +56,72 @@ func TestNewListCommandRun(t *testing.T) { mockApp.AssertExpectations(t) } +func TestNewListCommandRunJSON(t *testing.T) { + t.Parallel() + + mockApp := &testApp{} + namespaces := []app.NamespaceQueryResult{ + {NsID: "ns1", Version: 1, Policy: []byte{0xde, 0xad}}, + {NsID: "ns2", Version: 2, Policy: []byte{0xbe, 0xef}}, + } + mockApp.On("ListNamespaces", mock.Anything).Return(namespaces, nil) + + var out bytes.Buffer + cmd := newNsListCommand(&CLIContext{App: mockApp}) + cmd.SetOut(&out) + require.NoError(t, cmd.Flags().Set("format", "json")) + + err := cmd.RunE(cmd, nil) + + require.NoError(t, err) + + var results []map[string]any + require.NoError(t, json.Unmarshal(out.Bytes(), &results)) + require.Len(t, results, 2) + require.Equal(t, "ns1", results[0]["name"]) + require.Equal(t, "ns2", results[1]["name"]) + require.InDelta(t, float64(1), results[0]["version"], 0) + require.InDelta(t, float64(2), results[1]["version"], 0) + require.Contains(t, results[0], "policyString") + mockApp.AssertExpectations(t) +} + +func TestNewListCommandRunYAML(t *testing.T) { + t.Parallel() + + mockApp := &testApp{} + namespaces := []app.NamespaceQueryResult{ + {NsID: "myns", Version: 3, Policy: []byte{0xca, 0xfe}}, + } + mockApp.On("ListNamespaces", mock.Anything).Return(namespaces, nil) + + var out bytes.Buffer + cmd := newNsListCommand(&CLIContext{App: mockApp}) + cmd.SetOut(&out) + require.NoError(t, cmd.Flags().Set("format", "yaml")) + + err := cmd.RunE(cmd, nil) + + require.NoError(t, err) + output := out.String() + require.Contains(t, output, "myns") + require.Contains(t, output, "version: 3") + mockApp.AssertExpectations(t) +} + +func TestNewListCommandRunInvalidFormat(t *testing.T) { + t.Parallel() + + mockApp := &testApp{} + cmd := newNsListCommand(&CLIContext{App: mockApp}) + require.NoError(t, cmd.Flags().Set("format", "xml")) + + err := cmd.RunE(cmd, nil) + + require.Error(t, err) + require.Contains(t, err.Error(), "invalid --format") +} + func TestNewListCommandRun_AppError(t *testing.T) { t.Parallel()