diff --git a/docs/source/commands/peerlifecycle.md b/docs/source/commands/peerlifecycle.md index 0f119215b6e..bfd3e794597 100644 --- a/docs/source/commands/peerlifecycle.md +++ b/docs/source/commands/peerlifecycle.md @@ -311,6 +311,7 @@ Flags: -E, --endorsement-plugin string The name of the endorsement plugin to be used for this chaincode -h, --help help for checkcommitreadiness --init-required Whether the chaincode requires invoking 'init' + --inspect If inspect is enabled, output additional information to identify discrepancies when an organization's approval is false -n, --name string Name of the chaincode -O, --output string The output format for query results. Default is human-readable plain-text. json is currently the only supported format. --peerAddresses stringArray The addresses of the peers to connect to diff --git a/internal/peer/lifecycle/chaincode/chaincode.go b/internal/peer/lifecycle/chaincode/chaincode.go index 19126685825..5375a3653de 100644 --- a/internal/peer/lifecycle/chaincode/chaincode.go +++ b/internal/peer/lifecycle/chaincode/chaincode.go @@ -70,6 +70,7 @@ var ( sequence int initRequired bool output string + inspectionEnabled bool outputDirectory string ) @@ -119,6 +120,7 @@ func ResetFlags() { flags.IntVarP(&sequence, "sequence", "", 0, "The sequence number of the chaincode definition for the channel") flags.BoolVarP(&initRequired, "init-required", "", false, "Whether the chaincode requires invoking 'init'") flags.StringVarP(&output, "output", "O", "", "The output format for query results. Default is human-readable plain-text. json is currently the only supported format.") + flags.BoolVarP(&inspectionEnabled, "inspect", "", false, "If inspect is enabled, output additional information to identify discrepancies when an organization's approval is false") flags.StringVarP(&outputDirectory, "output-directory", "", "", "The output directory to use when writing a chaincode install package to disk. Default is the current working directory.") } diff --git a/internal/peer/lifecycle/chaincode/checkcommitreadiness.go b/internal/peer/lifecycle/chaincode/checkcommitreadiness.go index 5af31f46da3..4bd723c825b 100644 --- a/internal/peer/lifecycle/chaincode/checkcommitreadiness.go +++ b/internal/peer/lifecycle/chaincode/checkcommitreadiness.go @@ -57,6 +57,7 @@ type CommitReadinessCheckInput struct { PeerAddresses []string TxID string OutputFormat string + InspectionEnabled bool } // Validate the input for a CheckCommitReadiness proposal @@ -137,6 +138,7 @@ func CheckCommitReadinessCmd(c *CommitReadinessChecker, cryptoProvider bccsp.BCC "tlsRootCertFiles", "connectionProfile", "output", + "inspect", } attachFlags(chaincodeCheckCommitReadinessCmd, flagList) @@ -185,11 +187,52 @@ func (c *CommitReadinessChecker) ReadinessCheck() error { } if strings.ToLower(c.Input.OutputFormat) == "json" { + // Unmarshal the proposal response to add descriptions to mismatch items + readinessResult := &lb.CheckCommitReadinessResult{} + err := proto.Unmarshal(proposalResponse.Response.Payload, readinessResult) + if err != nil { + return errors.Wrap(err, "failed to unmarshal readiness result") + } + + if c.Input.InspectionEnabled { + for org, mismatches := range readinessResult.Mismatches { + for i, item := range mismatches.Items { + mismatches.Items[i] = c.mismatchItemWithDescription(item) + } + readinessResult.Mismatches[org] = mismatches + } + } else { + // If InspectionEnabled flag is OFF, clear the Mismatches + readinessResult.Mismatches = nil + } + + // Marshal back to proposalResponse + updatedPayload, err := proto.Marshal(readinessResult) + if err != nil { + return errors.Wrap(err, "failed to marshal updated readiness result") + } + proposalResponse.Response.Payload = updatedPayload + return printResponseAsJSON(proposalResponse, &lb.CheckCommitReadinessResult{}, c.Writer) } return c.printResponse(proposalResponse) } +// mismatchItemWithDescription returns the item with its corresponding description +func (c *CommitReadinessChecker) mismatchItemWithDescription(item string) string { + descriptions := map[string]string{ + "ChaincodeParameters": "ChaincodeParameters (Check the Sequence, ChaincodeName)", + "EndorsementInfo": "EndorsementInfo (Check the Version, InitRequired, EndorsementPlugin)", + "ValidationInfo": "ValidationInfo (Check the ValidationParameter, ValidationPlugin)", + "Collections": "Collections (Check the Collections)", + } + + if description, ok := descriptions[item]; ok { + return description + } + return item +} + // printResponse prints the information included in the response // from the server as human readable plain-text. func (c *CommitReadinessChecker) printResponse(proposalResponse *pb.ProposalResponse) error { @@ -207,7 +250,12 @@ func (c *CommitReadinessChecker) printResponse(proposalResponse *pb.ProposalResp fmt.Fprintf(c.Writer, "Chaincode definition for chaincode '%s', version '%s', sequence '%d' on channel '%s' approval status by org:\n", c.Input.Name, c.Input.Version, c.Input.Sequence, c.Input.ChannelID) for _, org := range orgs { - fmt.Fprintf(c.Writer, "%s: %t\n", org, result.Approvals[org]) + fmt.Fprintf(c.Writer, "%s: %t", org, result.Approvals[org]) + + if mismatch, ok := result.Mismatches[org]; ok && c.Input.InspectionEnabled && len(mismatch.Items) > 0 { + fmt.Fprintf(c.Writer, " (mismatch: [%s])", strings.Join(result.Mismatches[org].Items, ", ")) + } + fmt.Fprintln(c.Writer) } return nil @@ -238,6 +286,7 @@ func (c *CommitReadinessChecker) createInput() (*CommitReadinessCheckInput, erro CollectionConfigPackage: ccp, PeerAddresses: peerAddresses, OutputFormat: output, + InspectionEnabled: inspectionEnabled, } return input, nil diff --git a/internal/peer/lifecycle/chaincode/checkcommitreadiness_test.go b/internal/peer/lifecycle/chaincode/checkcommitreadiness_test.go index be9119adfa7..c792e9ecf67 100644 --- a/internal/peer/lifecycle/chaincode/checkcommitreadiness_test.go +++ b/internal/peer/lifecycle/chaincode/checkcommitreadiness_test.go @@ -8,6 +8,7 @@ package chaincode_test import ( "encoding/json" + "fmt" "github.com/golang/protobuf/proto" pb "github.com/hyperledger/fabric-protos-go/peer" @@ -41,6 +42,11 @@ var _ = Describe("CheckCommitReadiness", func() { "well...ok": true, "absolutely-not": false, }, + Mismatches: map[string]*lb.CheckCommitReadinessResult_Mismatches{ + "absolutely-not": { + Items: []string{"ChaincodeParameters", "EndorsementInfo", "ValidationInfo", "Collections"}, + }, + }, } mockResultBytes, err := proto.Marshal(mockResult) Expect(err).NotTo(HaveOccurred()) @@ -81,6 +87,21 @@ var _ = Describe("CheckCommitReadiness", func() { Eventually(commitReadinessChecker.Writer).Should(gbytes.Say("well...ok: true")) }) + Context("when inspect is enabled", func() { + BeforeEach(func() { + commitReadinessChecker.Input.InspectionEnabled = true + }) + + It("checks whether a chaincode definition is ready to commit and writes the output as human readable plain-text with mismatch details", func() { + err := commitReadinessChecker.ReadinessCheck() + Expect(err).NotTo(HaveOccurred()) + Eventually(commitReadinessChecker.Writer).Should(gbytes.Say("Chaincode definition for chaincode 'testcc', version '1.0', sequence '1' on channel 'testchannel' approval status by org")) + Eventually(commitReadinessChecker.Writer).Should(gbytes.Say("absolutely-not: false \\(mismatch: \\[ChaincodeParameters, EndorsementInfo, ValidationInfo, Collections\\]\\)")) + Eventually(commitReadinessChecker.Writer).Should(gbytes.Say("seemsfinetome: true")) + Eventually(commitReadinessChecker.Writer).Should(gbytes.Say("well...ok: true")) + }) + }) + Context("when JSON-formatted output is requested", func() { BeforeEach(func() { commitReadinessChecker.Input.OutputFormat = "json" @@ -100,6 +121,37 @@ var _ = Describe("CheckCommitReadiness", func() { Expect(err).NotTo(HaveOccurred()) Eventually(commitReadinessChecker.Writer).Should(gbytes.Say(string(json))) }) + + Context("when inspect is enabled", func() { + BeforeEach(func() { + commitReadinessChecker.Input.InspectionEnabled = true + }) + + It("checks whether a chaincode definition is ready to commit and writes the output as JSON with mismatch details", func() { + err := commitReadinessChecker.ReadinessCheck() + Expect(err).NotTo(HaveOccurred()) + expectedOutput := &lb.CheckCommitReadinessResult{ + Approvals: map[string]bool{ + "absolutely-not": false, + "well...ok": true, + "seemsfinetome": true, + }, + Mismatches: map[string]*lb.CheckCommitReadinessResult_Mismatches{ + "absolutely-not": { + Items: []string{ + "ChaincodeParameters (Check the Sequence, ChaincodeName)", + "EndorsementInfo (Check the Version, InitRequired, EndorsementPlugin)", + "ValidationInfo (Check the ValidationParameter, ValidationPlugin)", + "Collections (Check the Collections)", + }, + }, + }, + } + json, err := json.MarshalIndent(expectedOutput, "", "\t") + Expect(err).NotTo(HaveOccurred()) + Eventually(commitReadinessChecker.Writer).Should(gbytes.Say(fmt.Sprintf(`\Q%s\E`, string(json)))) + }) + }) }) Context("when the channel name is not provided", func() {