/* Copyright IBM Corp All Rights Reserved. SPDX-License-Identifier: Apache-2.0 */ package channelparticipation import ( "bytes" "encoding/json" "fmt" "io" "mime/multipart" "net/http" "time" "github.com/golang/protobuf/proto" "github.com/hyperledger/fabric-protos-go/common" "github.com/hyperledger/fabric/integration/nwo" ginkgo "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "github.com/onsi/gomega/gbytes" "github.com/onsi/gomega/gstruct" "github.com/onsi/gomega/types" ginkgomon "github.com/tedsuo/ifrit/ginkgomon_v2" ) func Join(n *nwo.Network, o *nwo.Orderer, channel string, block *common.Block, expectedChannelInfo ChannelInfo) { blockBytes, err := proto.Marshal(block) Expect(err).NotTo(HaveOccurred()) protocol := "http" if n.TLSEnabled { protocol = "https" } url := fmt.Sprintf("%s://127.0.0.1:%d/participation/v1/channels", protocol, n.OrdererPort(o, nwo.AdminPort)) req := GenerateJoinRequest(url, channel, blockBytes) authClient, unauthClient := nwo.OrdererOperationalClients(n, o) client := unauthClient if n.TLSEnabled { client = authClient } body := doBody(client, req) c := &ChannelInfo{} err = json.Unmarshal(body, c) Expect(err).NotTo(HaveOccurred()) Expect(*c).To(Equal(expectedChannelInfo)) } func GenerateJoinRequest(url, channel string, blockBytes []byte) *http.Request { joinBody := new(bytes.Buffer) writer := multipart.NewWriter(joinBody) part, err := writer.CreateFormFile("config-block", fmt.Sprintf("%s.block", channel)) Expect(err).NotTo(HaveOccurred()) part.Write(blockBytes) err = writer.Close() Expect(err).NotTo(HaveOccurred()) req, err := http.NewRequest(http.MethodPost, url, joinBody) Expect(err).NotTo(HaveOccurred()) req.Header.Set("Content-Type", writer.FormDataContentType()) return req } func doBody(client *http.Client, req *http.Request) []byte { resp, err := client.Do(req) Expect(err).NotTo(HaveOccurred()) Expect(resp.StatusCode).To(Equal(http.StatusCreated)) bodyBytes, err := io.ReadAll(resp.Body) Expect(err).NotTo(HaveOccurred()) resp.Body.Close() return bodyBytes } type ChannelList struct { // Deprecated: system channel no longer supported SystemChannel *ChannelInfoShort `json:"systemChannel"` Channels []ChannelInfoShort `json:"channels"` } type ChannelInfoShort struct { Name string `json:"name"` URL string `json:"url"` } func List(n *nwo.Network, o *nwo.Orderer) ChannelList { authClient, _ := nwo.OrdererOperationalClients(n, o) protocol := "http" if n.TLSEnabled { protocol = "https" } listChannelsURL := fmt.Sprintf("%s://127.0.0.1:%d/participation/v1/channels", protocol, n.OrdererPort(o, nwo.AdminPort)) body := getBody(authClient, listChannelsURL)() list := &ChannelList{} err := json.Unmarshal([]byte(body), list) Expect(err).NotTo(HaveOccurred()) return *list } func getBody(client *http.Client, url string) func() string { return func() string { resp, err := client.Get(url) Expect(err).NotTo(HaveOccurred()) bodyBytes, err := io.ReadAll(resp.Body) Expect(err).NotTo(HaveOccurred()) resp.Body.Close() return string(bodyBytes) } } type ChannelInfo struct { Name string `json:"name"` URL string `json:"url"` Status string `json:"status"` ConsensusRelation string `json:"consensusRelation"` Height uint64 `json:"height"` } func ListOne(n *nwo.Network, o *nwo.Orderer, channel string) ChannelInfo { authClient, _ := nwo.OrdererOperationalClients(n, o) protocol := "http" if n.TLSEnabled { protocol = "https" } listChannelURL := fmt.Sprintf("%s://127.0.0.1:%d/participation/v1/channels/%s", protocol, n.OrdererPort(o, nwo.AdminPort), channel) body := getBody(authClient, listChannelURL)() c := &ChannelInfo{} err := json.Unmarshal([]byte(body), c) Expect(err).NotTo(HaveOccurred()) return *c } func Remove(n *nwo.Network, o *nwo.Orderer, channel string) { authClient, _ := nwo.OrdererOperationalClients(n, o) protocol := "http" if n.TLSEnabled { protocol = "https" } url := fmt.Sprintf("%s://127.0.0.1:%d/participation/v1/channels/%s", protocol, n.OrdererPort(o, nwo.AdminPort), channel) req, err := http.NewRequest(http.MethodDelete, url, nil) Expect(err).NotTo(HaveOccurred()) resp, err := authClient.Do(req) Expect(err).NotTo(HaveOccurred()) Expect(resp.StatusCode).To(Equal(http.StatusNoContent)) } func ChannelListMatcher(list ChannelList, expectedChannels []string) { Expect(list).To(gstruct.MatchFields(gstruct.IgnoreExtras, gstruct.Fields{ "Channels": channelsMatcher(expectedChannels), "SystemChannel": BeNil(), })) } func channelsMatcher(channels []string) types.GomegaMatcher { if len(channels) == 0 { return BeEmpty() } matchers := make([]types.GomegaMatcher, len(channels)) for i, channel := range channels { matchers[i] = channelInfoShortMatcher(channel) } return ConsistOf(matchers) } func channelInfoShortMatcher(channel string) types.GomegaMatcher { return gstruct.MatchFields(gstruct.IgnoreExtras, gstruct.Fields{ "Name": Equal(channel), "URL": Equal(fmt.Sprintf("/participation/v1/channels/%s", channel)), }) } // JoinOrdererJoinPeersAppChannel Joins an orderer to a channel for which the genesis block was created by the network // bootstrap. It assumes a channel with one orderer. It waits for a leader (single orderer, always node=1), and then // joins all the peers to the channel. func JoinOrdererJoinPeersAppChannel(network *nwo.Network, channelID string, orderer *nwo.Orderer, ordererRunner *ginkgomon.Runner) { appGenesisBlock := network.LoadAppChannelGenesisBlock(channelID) expectedChannelInfo := ChannelInfo{ Name: channelID, URL: fmt.Sprintf("/participation/v1/channels/%s", channelID), Status: "active", ConsensusRelation: "consenter", Height: 1, } Join(network, orderer, channelID, appGenesisBlock, expectedChannelInfo) ginkgo.By(fmt.Sprintf("waiting for leader on channel %s", channelID)) Eventually(ordererRunner.Err(), network.EventuallyTimeout, time.Second).Should( gbytes.Say(fmt.Sprintf("Raft leader changed: 0 -> 1 channel=%s node=1", channelID))) ginkgo.By(fmt.Sprintf("joining peers to the channel %s", channelID)) peers := network.PeersWithChannel(channelID) network.JoinChannel(channelID, orderer, peers...) } // JoinOrdererAppChannel Joins an orderer to a channel for which the genesis block was created by the network // bootstrap. It assumes a channel with one orderer. It waits for a leader (single orderer, always node=1). func JoinOrdererAppChannel(network *nwo.Network, channelID string, orderer *nwo.Orderer, ordererRunner *ginkgomon.Runner) { appGenesisBlock := network.LoadAppChannelGenesisBlock(channelID) expectedChannelInfo := ChannelInfo{ Name: channelID, URL: fmt.Sprintf("/participation/v1/channels/%s", channelID), Status: "active", ConsensusRelation: "consenter", Height: 1, } Join(network, orderer, channelID, appGenesisBlock, expectedChannelInfo) ginkgo.By(fmt.Sprintf("waiting for leader on channel %s", channelID)) Eventually(ordererRunner.Err(), network.EventuallyTimeout, time.Second).Should( gbytes.Say(fmt.Sprintf("Raft leader changed: 0 -> 1 channel=%s node=1", channelID))) } // JoinOrderersAppChannelCluster Joins a set of orderers to a channel for which the genesis block was created by the network // bootstrap. It assumes a channel with one or more orderers (a cluster). func JoinOrderersAppChannelCluster(network *nwo.Network, channelID string, orderers ...*nwo.Orderer) { appGenesisBlock := network.LoadAppChannelGenesisBlock(channelID) for _, orderer := range orderers { expectedChannelInfo := ChannelInfo{ Name: channelID, URL: fmt.Sprintf("/participation/v1/channels/%s", channelID), Status: "active", ConsensusRelation: "consenter", Height: 1, } Join(network, orderer, channelID, appGenesisBlock, expectedChannelInfo) } }