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
5 changes: 5 additions & 0 deletions tools/fxconfig/internal/app/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ type DeployNamespaceInput struct {
Endorse bool
Submit bool
Wait bool
DryRun bool
}

// Validate validates namespace configuration.
Expand Down Expand Up @@ -75,6 +76,10 @@ func (d *AdminApp) DeployNamespace(
return nil, UnknownStatus, err
}

if input.DryRun {
return out, UnknownStatus, nil
}

// note that we enforce submit if wait is set
if !input.Submit && !input.Wait {
return out, UnknownStatus, nil
Expand Down
24 changes: 24 additions & 0 deletions tools/fxconfig/internal/app/deploy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -226,3 +226,27 @@ func TestDeployNamespace_EndorseAndSubmitWithWaitError(t *testing.T) {
_, _, err := a.DeployNamespace(t.Context(), input)
require.Error(t, err)
}

func TestDeployNamespace_DryRunSkipsSubmission(t *testing.T) {
t.Parallel()

a := &AdminApp{
Validators: fakeValidationContext(),
MspProvider: makeMSPProvider(&testSigningIdentity{}, nil),
OrdererProvider: makeOrdererProvider(nil, errors.New("orderer should not be used")),
NotificationProvider: makeNotificationProvider(nil, errors.New("notification should not be used")),
}
input := validDeployInput()
input.Endorse = true
input.Submit = true
input.Wait = true
input.DryRun = true

out, status, err := a.DeployNamespace(t.Context(), input)
require.NoError(t, err)
require.NotNil(t, out)
require.NotEmpty(t, out.TxID)
require.NotNil(t, out.Tx)
require.Equal(t, UnknownStatus, status)
require.NotEmpty(t, out.Tx.Endorsements, "dry-run should still allow endorsement before skipping submission")
}
3 changes: 3 additions & 0 deletions tools/fxconfig/internal/cli/v1/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ type namespaceDeployFlags struct {
endorse bool
submit bool
wait bool
dryRun bool
}

func (f *namespaceDeployFlags) bind(cmd *cobra.Command) {
Expand All @@ -46,6 +47,8 @@ func (f *namespaceDeployFlags) bind(cmd *cobra.Command) {
"Submit transaction to ordering service (requires --endorse)")
cmd.Flags().BoolVar(&f.wait, "wait", false,
"Wait for transaction finalization (implies --submit)")
cmd.Flags().BoolVar(&f.dryRun, "dry-run", false,
"Validate and preview changes without submitting")
}

// waitFlag represents a flag to wait for transaction finalization.
Expand Down
24 changes: 24 additions & 0 deletions tools/fxconfig/internal/cli/v1/namespace.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ SPDX-License-Identifier: Apache-2.0
package v1

import (
"fmt"

"github.com/spf13/cobra"
)

Expand Down Expand Up @@ -34,3 +36,25 @@ endorsement policies. Each namespace has:

return cmd
}

type dryRunPreview struct {
operation string
namespace string
txID string
}

func printDryRun(cmd *cobra.Command, ctx *CLIContext, preview dryRunPreview) {
message := fmt.Sprintf(
"=== DRY RUN ===\n\nNamespace operation prepared successfully.\n\n"+
"Namespace: %s\nOperation: %s\nTxID: %s\n\nTransaction was NOT submitted.\n",
preview.namespace,
preview.operation,
preview.txID,
)
if ctx.Printer != nil {
ctx.Printer.Print(message)
return
}

_, _ = fmt.Fprint(cmd.OutOrStdout(), message)
}
16 changes: 16 additions & 0 deletions tools/fxconfig/internal/cli/v1/namespace_create.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,15 @@ Transaction Lifecycle Flags:
--endorse Collect endorsement from local MSP
--submit Submit transaction to ordering service
--wait Wait for transaction finalization (implies --submit)
--dry-run Validate and preview changes without submitting

Examples:
# Create namespace with single org policy (save to file)
fxconfig namespace create hello --policy="OR('Org1MSP.member')" --output=tx.json

# Preview a namespace creation without submitting
fxconfig namespace create payments --config config.yaml --dry-run

# Create and immediately deploy (endorse + submit + wait)
fxconfig namespace create hello --policy="OR('Org1MSP.member')" --endorse --submit --wait

Expand All @@ -71,6 +75,7 @@ Examples:
Endorse: namespace.endorse,
Submit: namespace.submit,
Wait: namespace.wait,
DryRun: namespace.dryRun,
}

res, status, err := ctx.App.DeployNamespace(cmd.Context(), &input)
Expand All @@ -85,6 +90,17 @@ Examples:
return nil
}

if input.DryRun {
printDryRun(cmd, ctx, dryRunPreview{
operation: "create",
namespace: args[0],
txID: res.TxID,
})
if output == "" {
return nil
}
}

o, err := ctx.IOTransactionCodec.Encode(res.TxID, res.Tx)
if err != nil {
return err
Expand Down
36 changes: 36 additions & 0 deletions tools/fxconfig/internal/cli/v1/namespace_create_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ func TestNewCreateCommand(t *testing.T) {

policy := cmd.Flag("policy")
require.NotNil(t, policy, "policy flag should exist")

dryRun := cmd.Flag("dry-run")
require.NotNil(t, dryRun, "dry-run flag should exist")
}

func TestNewCreateCommandRun_TxReturned(t *testing.T) {
Expand Down Expand Up @@ -85,6 +88,39 @@ func TestNewCreateCommandRun_NoTx(t *testing.T) {
mockApp.AssertExpectations(t)
}

func TestNewCreateCommandRun_DryRun(t *testing.T) {
t.Parallel()

mockApp := &testApp{}
deployOut := &app.DeployNamespaceOutput{
TxID: "tx-dry-run",
Tx: &applicationpb.Tx{},
}
mockApp.On("DeployNamespace", mock.Anything, mock.MatchedBy(func(input *app.DeployNamespaceInput) bool {
return input != nil && input.DryRun && input.NsID == "my-namespace"
})).Return(deployOut, app.UnknownStatus, nil)

var printerOut, printerErr bytes.Buffer
printer := cliio.NewCLIPrinter(&printerOut, &printerErr, cliio.FormatTable)
cmd := newNsCreateCommand(&CLIContext{
App: mockApp,
Printer: printer,
})
cmd.SetOut(&printerOut)
require.NoError(t, cmd.Flags().Set("policy", "OR('Org1MSP.member')"))
require.NoError(t, cmd.Flags().Set("dry-run", "true"))

err := cmd.RunE(cmd, []string{"my-namespace"})

require.NoError(t, err)
require.Contains(t, printerOut.String(), "=== DRY RUN ===")
require.Contains(t, printerOut.String(), "Namespace: my-namespace")
require.Contains(t, printerOut.String(), "Operation: create")
require.Contains(t, printerOut.String(), "TxID: tx-dry-run")
require.Contains(t, printerOut.String(), "Transaction was NOT submitted.")
mockApp.AssertExpectations(t)
}

type testApp struct {
mock.Mock
}
Expand Down
18 changes: 18 additions & 0 deletions tools/fxconfig/internal/cli/v1/namespace_update.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,19 @@ Use 'fxconfig namespace list' to find the current version number.
Version numbers increment with each successful update. If the version you
specify doesn't match the current version, the update will fail.

Dry-run validates the update and previews the generated transaction without
submitting it to the ordering service.

Examples:
# Update namespace policy (check version first with 'list')
fxconfig namespace update hello \
--policy="OR('Org2MSP.member')" \
--version=0 \
--endorse --submit --wait

# Preview a namespace update without submitting
fxconfig namespace update payments --config updated.yaml --dry-run

# Change from single-org to multi-org policy
fxconfig namespace update hello \
--policy="AND('Org1MSP.member', 'Org2MSP.member')" \
Expand All @@ -70,6 +76,7 @@ Examples:
Endorse: namespace.endorse,
Submit: namespace.submit,
Wait: namespace.wait,
DryRun: namespace.dryRun,
}

res, status, err := ctx.App.DeployNamespace(cmd.Context(), &input)
Expand All @@ -84,6 +91,17 @@ Examples:
return nil
}

if input.DryRun {
printDryRun(cmd, ctx, dryRunPreview{
operation: "update",
namespace: args[0],
txID: res.TxID,
})
if output == "" {
return nil
}
}

o, err := ctx.IOTransactionCodec.Encode(res.TxID, res.Tx)
if err != nil {
return err
Expand Down
36 changes: 36 additions & 0 deletions tools/fxconfig/internal/cli/v1/namespace_update_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ func TestNewUpdateCommand(t *testing.T) {

policy := cmd.Flag("policy")
require.NotNil(t, policy, "policy flag should exist")

dryRun := cmd.Flag("dry-run")
require.NotNil(t, dryRun, "dry-run flag should exist")
}

func TestNsUpdateCommandRun_TxReturned(t *testing.T) {
Expand Down Expand Up @@ -85,3 +88,36 @@ func TestNsUpdateCommandRun_NoTx(t *testing.T) {
require.Contains(t, printerOut.String(), "Transaction status: STATUS_UNSPECIFIED")
mockApp.AssertExpectations(t)
}

func TestNsUpdateCommandRun_DryRun(t *testing.T) {
t.Parallel()

mockApp := &testApp{}
deployOut := &app.DeployNamespaceOutput{
TxID: "tx-dry-run-update",
Tx: &applicationpb.Tx{},
}
mockApp.On("DeployNamespace", mock.Anything, mock.MatchedBy(func(input *app.DeployNamespaceInput) bool {
return input != nil && input.DryRun && input.NsID == "my-namespace" && input.Version == 1
})).Return(deployOut, app.UnknownStatus, nil)

var outBuf bytes.Buffer
cmd := newNsUpdateCommand(&CLIContext{
App: mockApp,
Printer: cliio.NewCLIPrinter(&outBuf, &outBuf, cliio.FormatTable),
})
cmd.SetOut(&outBuf)
require.NoError(t, cmd.Flags().Set("policy", "OR('Org1MSP.member')"))
require.NoError(t, cmd.Flags().Set("version", "1"))
require.NoError(t, cmd.Flags().Set("dry-run", "true"))

err := cmd.RunE(cmd, []string{"my-namespace"})

require.NoError(t, err)
require.Contains(t, outBuf.String(), "=== DRY RUN ===")
require.Contains(t, outBuf.String(), "Namespace: my-namespace")
require.Contains(t, outBuf.String(), "Operation: update")
require.Contains(t, outBuf.String(), "TxID: tx-dry-run-update")
require.Contains(t, outBuf.String(), "Transaction was NOT submitted.")
mockApp.AssertExpectations(t)
}