Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 1 addition & 25 deletions tools/fxconfig/internal/app/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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"
// }
// }
100 changes: 89 additions & 11 deletions tools/fxconfig/internal/cli/v1/namespace_list.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -24,38 +91,49 @@ 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

# 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
}
75 changes: 71 additions & 4 deletions tools/fxconfig/internal/cli/v1/namespace_list_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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) {
Expand All @@ -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)

Expand All @@ -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()

Expand Down