From 30fb81a34178d17cb4bf1f5a11148d97d9118b84 Mon Sep 17 00:00:00 2001 From: jkneubuh <86427252+jkneubuh@users.noreply.github.com> Date: Wed, 6 Oct 2021 11:50:39 -0400 Subject: [PATCH] Run the fabric test network on Kubernetes (#498) * Run the fabric test network on Kubernetes Signed-off-by: Josh Kneubuhl * Re-LINT Signed-off-by: Josh Kneubuhl --- test-network-k8s/.gitignore | 5 + test-network-k8s/README.md | 78 ++ .../connection.json | 5 + .../asset-transfer-basic-debug/metadata.json | 4 + .../asset-transfer-basic/connection.json | 5 + .../asset-transfer-basic/metadata.json | 4 + test-network-k8s/config/org0/configtx.yaml | 415 ++++++++++ .../org0/fabric-ecert-ca-server-config.yaml | 506 ++++++++++++ .../org0/fabric-tls-ca-server-config.yaml | 496 ++++++++++++ test-network-k8s/config/org0/orderer.yaml | 420 ++++++++++ test-network-k8s/config/org1/core.yaml | 759 ++++++++++++++++++ .../org1/fabric-ecert-ca-server-config.yaml | 506 ++++++++++++ .../org1/fabric-tls-ca-server-config.yaml | 496 ++++++++++++ test-network-k8s/config/org2/core.yaml | 759 ++++++++++++++++++ .../org2/fabric-ecert-ca-server-config.yaml | 506 ++++++++++++ .../org2/fabric-tls-ca-server-config.yaml | 496 ++++++++++++ test-network-k8s/docs/APPLICATIONS.md | 106 +++ test-network-k8s/docs/CA.md | 281 +++++++ test-network-k8s/docs/CHAINCODE.md | 278 +++++++ test-network-k8s/docs/CHANNELS.md | 206 +++++ test-network-k8s/docs/KUBERNETES.md | 161 ++++ test-network-k8s/docs/README.md | 49 ++ test-network-k8s/docs/TEST_NETWORK.md | 221 +++++ test-network-k8s/docs/images/test-network.png | Bin 0 -> 225031 bytes .../kube/application-deployment.yaml | 48 ++ test-network-k8s/kube/fabric-rest-sample.yaml | 258 ++++++ test-network-k8s/kube/ingress-nginx.yaml | 687 ++++++++++++++++ .../kube/job-scrub-fabric-volumes.yaml | 43 + test-network-k8s/kube/ns-test-network.yaml | 10 + .../kube/org0/org0-admin-cli.yaml | 61 ++ test-network-k8s/kube/org0/org0-ecert-ca.yaml | 69 ++ test-network-k8s/kube/org0/org0-orderer1.yaml | 85 ++ test-network-k8s/kube/org0/org0-orderer2.yaml | 85 ++ test-network-k8s/kube/org0/org0-orderer3.yaml | 85 ++ test-network-k8s/kube/org0/org0-tls-ca.yaml | 65 ++ .../kube/org1/org1-admin-cli.yaml | 65 ++ .../kube/org1/org1-cc-template.yaml | 45 ++ test-network-k8s/kube/org1/org1-ecert-ca.yaml | 69 ++ test-network-k8s/kube/org1/org1-peer1.yaml | 103 +++ test-network-k8s/kube/org1/org1-peer2.yaml | 89 ++ test-network-k8s/kube/org1/org1-tls-ca.yaml | 65 ++ .../kube/org2/org2-admin-cli.yaml | 65 ++ test-network-k8s/kube/org2/org2-cc.yaml | 6 + test-network-k8s/kube/org2/org2-ecert-ca.yaml | 69 ++ test-network-k8s/kube/org2/org2-peer1.yaml | 104 +++ test-network-k8s/kube/org2/org2-peer2.yaml | 89 ++ test-network-k8s/kube/org2/org2-tls-ca.yaml | 65 ++ test-network-k8s/kube/pv-fabric-org0.yaml | 18 + test-network-k8s/kube/pv-fabric-org1.yaml | 18 + test-network-k8s/kube/pv-fabric-org2.yaml | 18 + test-network-k8s/kube/pvc-fabric-org0.yaml | 17 + test-network-k8s/kube/pvc-fabric-org1.yaml | 17 + test-network-k8s/kube/pvc-fabric-org2.yaml | 17 + test-network-k8s/network | 157 ++++ .../scripts/application_connection.sh | 114 +++ test-network-k8s/scripts/appuser.id.template | 9 + test-network-k8s/scripts/ccp-template.json | 49 ++ test-network-k8s/scripts/chaincode.sh | 172 ++++ test-network-k8s/scripts/channel.sh | 201 +++++ test-network-k8s/scripts/fabric_CAs.sh | 137 ++++ test-network-k8s/scripts/fabric_config.sh | 42 + test-network-k8s/scripts/kind.sh | 134 ++++ test-network-k8s/scripts/prereqs.sh | 28 + test-network-k8s/scripts/rest_sample.sh | 91 +++ test-network-k8s/scripts/set_anchor_peer.sh | 110 +++ test-network-k8s/scripts/test_network.sh | 259 ++++++ test-network-k8s/scripts/utils.sh | 66 ++ 67 files changed, 10766 insertions(+) create mode 100644 test-network-k8s/.gitignore create mode 100644 test-network-k8s/README.md create mode 100644 test-network-k8s/chaincode/asset-transfer-basic-debug/connection.json create mode 100644 test-network-k8s/chaincode/asset-transfer-basic-debug/metadata.json create mode 100644 test-network-k8s/chaincode/asset-transfer-basic/connection.json create mode 100644 test-network-k8s/chaincode/asset-transfer-basic/metadata.json create mode 100644 test-network-k8s/config/org0/configtx.yaml create mode 100644 test-network-k8s/config/org0/fabric-ecert-ca-server-config.yaml create mode 100644 test-network-k8s/config/org0/fabric-tls-ca-server-config.yaml create mode 100644 test-network-k8s/config/org0/orderer.yaml create mode 100644 test-network-k8s/config/org1/core.yaml create mode 100644 test-network-k8s/config/org1/fabric-ecert-ca-server-config.yaml create mode 100644 test-network-k8s/config/org1/fabric-tls-ca-server-config.yaml create mode 100644 test-network-k8s/config/org2/core.yaml create mode 100644 test-network-k8s/config/org2/fabric-ecert-ca-server-config.yaml create mode 100644 test-network-k8s/config/org2/fabric-tls-ca-server-config.yaml create mode 100644 test-network-k8s/docs/APPLICATIONS.md create mode 100644 test-network-k8s/docs/CA.md create mode 100644 test-network-k8s/docs/CHAINCODE.md create mode 100644 test-network-k8s/docs/CHANNELS.md create mode 100644 test-network-k8s/docs/KUBERNETES.md create mode 100644 test-network-k8s/docs/README.md create mode 100644 test-network-k8s/docs/TEST_NETWORK.md create mode 100644 test-network-k8s/docs/images/test-network.png create mode 100644 test-network-k8s/kube/application-deployment.yaml create mode 100644 test-network-k8s/kube/fabric-rest-sample.yaml create mode 100644 test-network-k8s/kube/ingress-nginx.yaml create mode 100644 test-network-k8s/kube/job-scrub-fabric-volumes.yaml create mode 100644 test-network-k8s/kube/ns-test-network.yaml create mode 100644 test-network-k8s/kube/org0/org0-admin-cli.yaml create mode 100644 test-network-k8s/kube/org0/org0-ecert-ca.yaml create mode 100644 test-network-k8s/kube/org0/org0-orderer1.yaml create mode 100644 test-network-k8s/kube/org0/org0-orderer2.yaml create mode 100644 test-network-k8s/kube/org0/org0-orderer3.yaml create mode 100644 test-network-k8s/kube/org0/org0-tls-ca.yaml create mode 100644 test-network-k8s/kube/org1/org1-admin-cli.yaml create mode 100644 test-network-k8s/kube/org1/org1-cc-template.yaml create mode 100644 test-network-k8s/kube/org1/org1-ecert-ca.yaml create mode 100644 test-network-k8s/kube/org1/org1-peer1.yaml create mode 100644 test-network-k8s/kube/org1/org1-peer2.yaml create mode 100644 test-network-k8s/kube/org1/org1-tls-ca.yaml create mode 100644 test-network-k8s/kube/org2/org2-admin-cli.yaml create mode 100644 test-network-k8s/kube/org2/org2-cc.yaml create mode 100644 test-network-k8s/kube/org2/org2-ecert-ca.yaml create mode 100644 test-network-k8s/kube/org2/org2-peer1.yaml create mode 100644 test-network-k8s/kube/org2/org2-peer2.yaml create mode 100644 test-network-k8s/kube/org2/org2-tls-ca.yaml create mode 100644 test-network-k8s/kube/pv-fabric-org0.yaml create mode 100644 test-network-k8s/kube/pv-fabric-org1.yaml create mode 100644 test-network-k8s/kube/pv-fabric-org2.yaml create mode 100644 test-network-k8s/kube/pvc-fabric-org0.yaml create mode 100644 test-network-k8s/kube/pvc-fabric-org1.yaml create mode 100644 test-network-k8s/kube/pvc-fabric-org2.yaml create mode 100755 test-network-k8s/network create mode 100755 test-network-k8s/scripts/application_connection.sh create mode 100644 test-network-k8s/scripts/appuser.id.template create mode 100755 test-network-k8s/scripts/ccp-template.json create mode 100755 test-network-k8s/scripts/chaincode.sh create mode 100755 test-network-k8s/scripts/channel.sh create mode 100755 test-network-k8s/scripts/fabric_CAs.sh create mode 100755 test-network-k8s/scripts/fabric_config.sh create mode 100755 test-network-k8s/scripts/kind.sh create mode 100755 test-network-k8s/scripts/prereqs.sh create mode 100755 test-network-k8s/scripts/rest_sample.sh create mode 100755 test-network-k8s/scripts/set_anchor_peer.sh create mode 100755 test-network-k8s/scripts/test_network.sh create mode 100644 test-network-k8s/scripts/utils.sh diff --git a/test-network-k8s/.gitignore b/test-network-k8s/.gitignore new file mode 100644 index 00000000..b4afee58 --- /dev/null +++ b/test-network-k8s/.gitignore @@ -0,0 +1,5 @@ +.idea/ +network.log +network-debug.log +build/ +.env diff --git a/test-network-k8s/README.md b/test-network-k8s/README.md new file mode 100644 index 00000000..577c0394 --- /dev/null +++ b/test-network-k8s/README.md @@ -0,0 +1,78 @@ +# Kubernetes Test Network + +This project re-establishes the Hyperledger [test-network](../test-network) as a _cloud native_ application. + +### Objectives: + +- Provide a simple, _one click_ activity for running the Fabric test network. +- Provide a reference guide for deploying _production-style_ networks on Kubernetes. +- Provide a _cloud ready_ platform for developing chaincode, Gateway, and blockchain apps. +- Provide a Kube supplement to the Fabric [CA Operations and Deployment](https://hyperledger-fabric-ca.readthedocs.io/en/latest/deployguide/ca-deploy.html) guides. +- Support a transition to [Chaincode as a Service](https://hyperledger-fabric.readthedocs.io/en/latest/cc_service.html). +- Support a transition from the Internal, Docker daemon to [External Chaincode](https://hyperledger-fabric.readthedocs.io/en/latest/cc_launcher.html) builders. +- Run on any Kube. + +_Fabric, Ahoy!_ + + +## Prerequisites + +- [Docker](https://www.docker.com) +- [kubectl](https://kubernetes.io/docs/tasks/tools/) +- [kind](https://kind.sigs.k8s.io/docs/user/quick-start/#installation) + + +## Quickstart + +Create a local Kubernetes cluster: +```shell +./network kind +``` + +Launch the network, create a channel, and deploy the [basic-asset-transfer](../asset-transfer-basic) smart contract: +```shell +./network up +./network channel create +./network chaincode deploy +``` + +Invoke and query chaincode: +```shell +./network chaincode invoke '{"Args":["CreateAsset","1","blue","35","tom","1000"]}' +./network chaincode query '{"Args":["ReadAsset","1"]}' +``` + +Access the blockchain with a [REST API](https://github.com/hyperledgendary/fabric-rest-sample/tree/main/asset-transfer-basic/rest-api-typescript#rest-api): +``` +./network rest-easy +``` + +Tear down the test network: +```shell +./network down +``` + +Tear down the cluster: +```shell +./network unkind +``` + + +## [Detailed Guides](docs/README.md) + +- [`./network`](docs/NETWORK.md) +- [Working with Kubernetes](docs/KUBERNETES.md) +- [Certificate Authorities](docs/CA.md) +- [Launching the Test Network](docs/TEST_NETWORK.md) +- [Working with Channels](docs/CHANNELS.md) +- [Working with Chaincode](docs/CHAINCODE.md) +- [Working with Applications](docs/APPLICATIONS.md) + + +## Areas for Improvement / TODOs + +- [ ] Test the recipe with OCP, AWS, gcp, Azure, etc. (These should ONLY differ w.r.t. pvc and ingress) +- [ ] Implement @celder mechanism for bootstrapping dual-headed CAs w/o poisoning the root CA on expiry. +- [ ] Address any of the 20+ todo: notes in network.sh +- [ ] Implement mutual TLS across peers, orderers, and clients. +- [ ] Caliper? diff --git a/test-network-k8s/chaincode/asset-transfer-basic-debug/connection.json b/test-network-k8s/chaincode/asset-transfer-basic-debug/connection.json new file mode 100644 index 00000000..030c8ee1 --- /dev/null +++ b/test-network-k8s/chaincode/asset-transfer-basic-debug/connection.json @@ -0,0 +1,5 @@ +{ + "address": "host.docker.internal:9999", + "dial_timeout": "10s", + "tls_required": false +} diff --git a/test-network-k8s/chaincode/asset-transfer-basic-debug/metadata.json b/test-network-k8s/chaincode/asset-transfer-basic-debug/metadata.json new file mode 100644 index 00000000..bb7056c0 --- /dev/null +++ b/test-network-k8s/chaincode/asset-transfer-basic-debug/metadata.json @@ -0,0 +1,4 @@ +{ + "type": "external", + "label": "basic_1.0" +} \ No newline at end of file diff --git a/test-network-k8s/chaincode/asset-transfer-basic/connection.json b/test-network-k8s/chaincode/asset-transfer-basic/connection.json new file mode 100644 index 00000000..598ba74f --- /dev/null +++ b/test-network-k8s/chaincode/asset-transfer-basic/connection.json @@ -0,0 +1,5 @@ +{ + "address": "org1-cc-asset-transfer-basic:9999", + "dial_timeout": "10s", + "tls_required": false +} \ No newline at end of file diff --git a/test-network-k8s/chaincode/asset-transfer-basic/metadata.json b/test-network-k8s/chaincode/asset-transfer-basic/metadata.json new file mode 100644 index 00000000..bb7056c0 --- /dev/null +++ b/test-network-k8s/chaincode/asset-transfer-basic/metadata.json @@ -0,0 +1,4 @@ +{ + "type": "external", + "label": "basic_1.0" +} \ No newline at end of file diff --git a/test-network-k8s/config/org0/configtx.yaml b/test-network-k8s/config/org0/configtx.yaml new file mode 100644 index 00000000..337c83c2 --- /dev/null +++ b/test-network-k8s/config/org0/configtx.yaml @@ -0,0 +1,415 @@ +# Copyright IBM Corp. All Rights Reserved. +# +# SPDX-License-Identifier: Apache-2.0 +# + +--- +################################################################################ +# +# Section: Organizations +# +# - This section defines the different organizational identities which will +# be referenced later in the configuration. +# +################################################################################ +Organizations: + + # SampleOrg defines an MSP using the sampleconfig. It should never be used + # in production but may be used as a template for other definitions + - &OrdererOrg + # DefaultOrg defines the organization which is used in the sampleconfig + # of the fabric.git development environment + Name: OrdererOrg + + # ID to load the MSP definition as + ID: OrdererMSP + + # MSPDir is the filesystem path which contains the MSP configuration + MSPDir: /var/hyperledger/fabric/organizations/ordererOrganizations/org0.example.com/msp + + # Policies defines the set of policies at this level of the config tree + # For organization policies, their canonical path is usually + # /Channel/// + Policies: + Readers: + Type: Signature + Rule: "OR('OrdererMSP.member')" + Writers: + Type: Signature + Rule: "OR('OrdererMSP.member')" + Admins: + Type: Signature + Rule: "OR('OrdererMSP.admin')" + + OrdererEndpoints: + - org0-orderer1:6050 + - org0-orderer2:6050 + - org0-orderer3:6050 + + - &Org1 + # DefaultOrg defines the organization which is used in the sampleconfig + # of the fabric.git development environment + Name: Org1MSP + + # ID to load the MSP definition as + ID: Org1MSP + + MSPDir: /var/hyperledger/fabric/organizations/peerOrganizations/org1.example.com/msp + + # Policies defines the set of policies at this level of the config tree + # For organization policies, their canonical path is usually + # /Channel/// + Policies: + Readers: + Type: Signature + Rule: "OR('Org1MSP.admin', 'Org1MSP.peer', 'Org1MSP.client')" + Writers: + Type: Signature + Rule: "OR('Org1MSP.admin', 'Org1MSP.client')" + Admins: + Type: Signature + Rule: "OR('Org1MSP.admin')" + Endorsement: + Type: Signature + Rule: "OR('Org1MSP.peer')" + + # leave this flag set to true. + AnchorPeers: + # AnchorPeers defines the location of peers which can be used + # for cross org gossip communication. Note, this value is only + # encoded in the genesis block in the Application section context + - Host: org1-peer1 + Port: 7051 + + - &Org2 + # DefaultOrg defines the organization which is used in the sampleconfig + # of the fabric.git development environment + Name: Org2MSP + + # ID to load the MSP definition as + ID: Org2MSP + + MSPDir: /var/hyperledger/fabric/organizations/peerOrganizations/org2.example.com/msp + + # Policies defines the set of policies at this level of the config tree + # For organization policies, their canonical path is usually + # /Channel/// + Policies: + Readers: + Type: Signature + Rule: "OR('Org2MSP.admin', 'Org2MSP.peer', 'Org2MSP.client')" + Writers: + Type: Signature + Rule: "OR('Org2MSP.admin', 'Org2MSP.client')" + Admins: + Type: Signature + Rule: "OR('Org2MSP.admin')" + Endorsement: + Type: Signature + Rule: "OR('Org2MSP.peer')" + + AnchorPeers: + # AnchorPeers defines the location of peers which can be used + # for cross org gossip communication. Note, this value is only + # encoded in the genesis block in the Application section context + - Host: org2-peer1 + Port: 7051 + +################################################################################ +# +# SECTION: Capabilities +# +# - This section defines the capabilities of fabric network. This is a new +# concept as of v1.1.0 and should not be utilized in mixed networks with +# v1.0.x peers and orderers. Capabilities define features which must be +# present in a fabric binary for that binary to safely participate in the +# fabric network. For instance, if a new MSP type is added, newer binaries +# might recognize and validate the signatures from this type, while older +# binaries without this support would be unable to validate those +# transactions. This could lead to different versions of the fabric binaries +# having different world states. Instead, defining a capability for a channel +# informs those binaries without this capability that they must cease +# processing transactions until they have been upgraded. For v1.0.x if any +# capabilities are defined (including a map with all capabilities turned off) +# then the v1.0.x peer will deliberately crash. +# +################################################################################ +Capabilities: + # Channel capabilities apply to both the orderers and the peers and must be + # supported by both. + # Set the value of the capability to true to require it. + Channel: &ChannelCapabilities + # V2_0 capability ensures that orderers and peers behave according + # to v2.0 channel capabilities. Orderers and peers from + # prior releases would behave in an incompatible way, and are therefore + # not able to participate in channels at v2.0 capability. + # Prior to enabling V2.0 channel capabilities, ensure that all + # orderers and peers on a channel are at v2.0.0 or later. + V2_0: true + + # Orderer capabilities apply only to the orderers, and may be safely + # used with prior release peers. + # Set the value of the capability to true to require it. + Orderer: &OrdererCapabilities + # V2_0 orderer capability ensures that orderers behave according + # to v2.0 orderer capabilities. Orderers from + # prior releases would behave in an incompatible way, and are therefore + # not able to participate in channels at v2.0 orderer capability. + # Prior to enabling V2.0 orderer capabilities, ensure that all + # orderers on channel are at v2.0.0 or later. + V2_0: true + + # Application capabilities apply only to the peer network, and may be safely + # used with prior release orderers. + # Set the value of the capability to true to require it. + Application: &ApplicationCapabilities + # V2_0 application capability ensures that peers behave according + # to v2.0 application capabilities. Peers from + # prior releases would behave in an incompatible way, and are therefore + # not able to participate in channels at v2.0 application capability. + # Prior to enabling V2.0 application capabilities, ensure that all + # peers on channel are at v2.0.0 or later. + V2_0: true + +################################################################################ +# +# SECTION: Application +# +# - This section defines the values to encode into a config transaction or +# genesis block for application related parameters +# +################################################################################ +Application: &ApplicationDefaults + + # Organizations is the list of orgs which are defined as participants on + # the application side of the network + Organizations: + + # Policies defines the set of policies at this level of the config tree + # For Application policies, their canonical path is + # /Channel/Application/ + Policies: + Readers: + Type: ImplicitMeta + Rule: "ANY Readers" + Writers: + Type: ImplicitMeta + Rule: "ANY Writers" + Admins: + Type: ImplicitMeta + Rule: "MAJORITY Admins" + LifecycleEndorsement: + Type: Signature + Rule: "OR('Org1MSP.peer','Org2MSP.peer')" + Endorsement: + Type: Signature + Rule: "OR('Org1MSP.peer','Org2MSP.peer')" + + Capabilities: + <<: *ApplicationCapabilities +################################################################################ +# +# SECTION: Orderer +# +# - This section defines the values to encode into a config transaction or +# genesis block for orderer related parameters +# +################################################################################ +Orderer: &OrdererDefaults + + # Orderer Type: The orderer implementation to start + OrdererType: etcdraft + + EtcdRaft: + Consenters: + - Host: org0-orderer1 + Port: 6050 + ClientTLSCert: /var/hyperledger/fabric/organizations/ordererOrganizations/org0.example.com/orderers/org0-orderer1.org0.example.com/tls/signcerts/cert.pem + ServerTLSCert: /var/hyperledger/fabric/organizations/ordererOrganizations/org0.example.com/orderers/org0-orderer1.org0.example.com/tls/signcerts/cert.pem + - Host: org0-orderer2 + Port: 6050 + ClientTLSCert: /var/hyperledger/fabric/organizations/ordererOrganizations/org0.example.com/orderers/org0-orderer2.org0.example.com/tls/signcerts/cert.pem + ServerTLSCert: /var/hyperledger/fabric/organizations/ordererOrganizations/org0.example.com/orderers/org0-orderer2.org0.example.com/tls/signcerts/cert.pem + - Host: org0-orderer3 + Port: 6050 + ClientTLSCert: /var/hyperledger/fabric/organizations/ordererOrganizations/org0.example.com/orderers/org0-orderer3.org0.example.com/tls/signcerts/cert.pem + ServerTLSCert: /var/hyperledger/fabric/organizations/ordererOrganizations/org0.example.com/orderers/org0-orderer3.org0.example.com/tls/signcerts/cert.pem + + + # Options to be specified for all the etcd/raft nodes. The values here + # are the defaults for all new channels and can be modified on a + # per-channel basis via configuration updates. + Options: + # TickInterval is the time interval between two Node.Tick invocations. + #TickInterval: 500ms default + TickInterval: 2500ms + + # ElectionTick is the number of Node.Tick invocations that must pass + # between elections. That is, if a follower does not receive any + # message from the leader of current term before ElectionTick has + # elapsed, it will become candidate and start an election. + # ElectionTick must be greater than HeartbeatTick. + # ElectionTick: 10 default + ElectionTick: 5 + + # HeartbeatTick is the number of Node.Tick invocations that must + # pass between heartbeats. That is, a leader sends heartbeat + # messages to maintain its leadership every HeartbeatTick ticks. + HeartbeatTick: 1 + + # MaxInflightBlocks limits the max number of in-flight append messages + # during optimistic replication phase. + MaxInflightBlocks: 5 + + # SnapshotIntervalSize defines number of bytes per which a snapshot is taken + SnapshotIntervalSize: 16 MB + + # Batch Timeout: The amount of time to wait before creating a batch + BatchTimeout: 2s + + # Batch Size: Controls the number of messages batched into a block + BatchSize: + + # Max Message Count: The maximum number of messages to permit in a batch + MaxMessageCount: 10 + + # Absolute Max Bytes: The absolute maximum number of bytes allowed for + # the serialized messages in a batch. + AbsoluteMaxBytes: 99 MB + + # Preferred Max Bytes: The preferred maximum number of bytes allowed for + # the serialized messages in a batch. A message larger than the preferred + # max bytes will result in a batch larger than preferred max bytes. + PreferredMaxBytes: 512 KB + + # Organizations is the list of orgs which are defined as participants on + # the orderer side of the network + Organizations: + + # Policies defines the set of policies at this level of the config tree + # For Orderer policies, their canonical path is + # /Channel/Orderer/ + Policies: + Readers: + Type: ImplicitMeta + Rule: "ANY Readers" + Writers: + Type: ImplicitMeta + Rule: "ANY Writers" + Admins: + Type: ImplicitMeta + Rule: "MAJORITY Admins" + # BlockValidation specifies what signatures must be included in the block + # from the orderer for the peer to validate it. + BlockValidation: + Type: ImplicitMeta + Rule: "ANY Writers" + +################################################################################ +# +# CHANNEL +# +# This section defines the values to encode into a config transaction or +# genesis block for channel related parameters. +# +################################################################################ +Channel: &ChannelDefaults + # Policies defines the set of policies at this level of the config tree + # For Channel policies, their canonical path is + # /Channel/ + Policies: + # Who may invoke the 'Deliver' API + Readers: + Type: ImplicitMeta + Rule: "ANY Readers" + # Who may invoke the 'Broadcast' API + Writers: + Type: ImplicitMeta + Rule: "ANY Writers" + # By default, who may modify elements at this config level + Admins: + Type: ImplicitMeta + Rule: "MAJORITY Admins" + + # Capabilities describes the channel level capabilities, see the + # dedicated Capabilities section elsewhere in this file for a full + # description + Capabilities: + <<: *ChannelCapabilities + +################################################################################ +# +# Profile +# +# - Different configuration profiles may be encoded here to be specified +# as parameters to the configtxgen tool +# +################################################################################ +Profiles: + + # test network profile with application (not system) channel. + TwoOrgsApplicationGenesis: + <<: *ChannelDefaults + Orderer: + <<: *OrdererDefaults + Organizations: + - *OrdererOrg + Capabilities: *OrdererCapabilities + Application: + <<: *ApplicationDefaults + Organizations: + - *Org1 + - *Org2 + Capabilities: *ApplicationCapabilities + + + # + # Unclear lineage for these profiles: nano-fab? + # + # TwoOrgsOrdererGenesis will construct a system channel as it has a Consortiums stanza, which is not + # compatible with osnadmin. + # + # @enyeart - which profile should be used for the kube test network? + # + TwoOrgsOrdererGenesis: + <<: *ChannelDefaults + Orderer: + <<: *OrdererDefaults + OrdererType: etcdraft + Organizations: + - *OrdererOrg + Capabilities: + <<: *OrdererCapabilities + Consortiums: + SampleConsortium: + Organizations: + - *Org1 + - *Org2 + TwoOrgsChannel: + Consortium: SampleConsortium + <<: *ChannelDefaults + Application: + <<: *ApplicationDefaults + Organizations: + - *Org1 + - *Org2 + Capabilities: + <<: *ApplicationCapabilities + Org1Channel: + Consortium: SampleConsortium + <<: *ChannelDefaults + Application: + <<: *ApplicationDefaults + Organizations: + - *Org1 + Capabilities: + <<: *ApplicationCapabilities + Org2Channel: + Consortium: SampleConsortium + <<: *ChannelDefaults + Application: + <<: *ApplicationDefaults + Organizations: + - *Org2 + Capabilities: + <<: *ApplicationCapabilities diff --git a/test-network-k8s/config/org0/fabric-ecert-ca-server-config.yaml b/test-network-k8s/config/org0/fabric-ecert-ca-server-config.yaml new file mode 100644 index 00000000..eff91c34 --- /dev/null +++ b/test-network-k8s/config/org0/fabric-ecert-ca-server-config.yaml @@ -0,0 +1,506 @@ +############################################################################# +# This is a configuration file for the fabric-ca-server command. +# +# COMMAND LINE ARGUMENTS AND ENVIRONMENT VARIABLES +# ------------------------------------------------ +# Each configuration element can be overridden via command line +# arguments or environment variables. The precedence for determining +# the value of each element is as follows: +# 1) command line argument +# Examples: +# a) --port 443 +# To set the listening port +# b) --ca.keyfile ../mykey.pem +# To set the "keyfile" element in the "ca" section below; +# note the '.' separator character. +# 2) environment variable +# Examples: +# a) FABRIC_CA_SERVER_PORT=443 +# To set the listening port +# b) FABRIC_CA_SERVER_CA_KEYFILE="../mykey.pem" +# To set the "keyfile" element in the "ca" section below; +# note the '_' separator character. +# 3) configuration file +# 4) default value (if there is one) +# All default values are shown beside each element below. +# +# FILE NAME ELEMENTS +# ------------------ +# The value of all fields whose name ends with "file" or "files" are +# name or names of other files. +# For example, see "tls.certfile" and "tls.clientauth.certfiles". +# The value of each of these fields can be a simple filename, a +# relative path, or an absolute path. If the value is not an +# absolute path, it is interpretted as being relative to the location +# of this configuration file. +# +############################################################################# + +# Version of config file +version: 1.5.2 + +# Server's listening port (default: 7054) +port: 443 + +# Cross-Origin Resource Sharing (CORS) +cors: + enabled: false + origins: + - "*" + +# Enables debug logging (default: false) +debug: false + +# Size limit of an acceptable CRL in bytes (default: 512000) +crlsizelimit: 512000 + +############################################################################# +# TLS section for the server's listening port +# +# The following types are supported for client authentication: NoClientCert, +# RequestClientCert, RequireAnyClientCert, VerifyClientCertIfGiven, +# and RequireAndVerifyClientCert. +# +# Certfiles is a list of root certificate authorities that the server uses +# when verifying client certificates. +############################################################################# +tls: + # Enable TLS (default: false) + enabled: true + # TLS for the server's listening port + certfile: + keyfile: + clientauth: + type: noclientcert + certfiles: + +############################################################################# +# The CA section contains information related to the Certificate Authority +# including the name of the CA, which should be unique for all members +# of a blockchain network. It also includes the key and certificate files +# used when issuing enrollment certificates (ECerts) and transaction +# certificates (TCerts). +# The chainfile (if it exists) contains the certificate chain which +# should be trusted for this CA, where the 1st in the chain is always the +# root CA certificate. +############################################################################# +ca: + # Name of this CA + name: org0-ecert-ca + # Key file (is only used to import a private key into BCCSP) + keyfile: + # Certificate file (default: ca-cert.pem) + certfile: + # Chain file + chainfile: + +############################################################################# +# The gencrl REST endpoint is used to generate a CRL that contains revoked +# certificates. This section contains configuration options that are used +# during gencrl request processing. +############################################################################# +crl: + # Specifies expiration for the generated CRL. The number of hours + # specified by this property is added to the UTC time, the resulting time + # is used to set the 'Next Update' date of the CRL. + expiry: 24h + +############################################################################# +# The registry section controls how the fabric-ca-server does two things: +# 1) authenticates enrollment requests which contain a username and password +# (also known as an enrollment ID and secret). +# 2) once authenticated, retrieves the identity's attribute names and +# values which the fabric-ca-server optionally puts into TCerts +# which it issues for transacting on the Hyperledger Fabric blockchain. +# These attributes are useful for making access control decisions in +# chaincode. +# There are two main configuration options: +# 1) The fabric-ca-server is the registry. +# This is true if "ldap.enabled" in the ldap section below is false. +# 2) An LDAP server is the registry, in which case the fabric-ca-server +# calls the LDAP server to perform these tasks. +# This is true if "ldap.enabled" in the ldap section below is true, +# which means this "registry" section is ignored. +############################################################################# +registry: + # Maximum number of times a password/secret can be reused for enrollment + # (default: -1, which means there is no limit) + maxenrollments: -1 + + # Contains identity information which is used when LDAP is disabled + identities: + - name: rcaadmin + pass: rcaadminpw + type: client + affiliation: "" + attrs: + hf.Registrar.Roles: "*" + hf.Registrar.DelegateRoles: "*" + hf.Revoker: true + hf.IntermediateCA: true + hf.GenCRL: true + hf.Registrar.Attributes: "*" + hf.AffiliationMgr: true + +############################################################################# +# Database section +# Supported types are: "sqlite3", "postgres", and "mysql". +# The datasource value depends on the type. +# If the type is "sqlite3", the datasource value is a file name to use +# as the database store. Since "sqlite3" is an embedded database, it +# may not be used if you want to run the fabric-ca-server in a cluster. +# To run the fabric-ca-server in a cluster, you must choose "postgres" +# or "mysql". +############################################################################# +db: + type: sqlite3 + datasource: fabric-ca-server.db + tls: + enabled: false + certfiles: + client: + certfile: + keyfile: + +############################################################################# +# LDAP section +# If LDAP is enabled, the fabric-ca-server calls LDAP to: +# 1) authenticate enrollment ID and secret (i.e. username and password) +# for enrollment requests; +# 2) To retrieve identity attributes +############################################################################# +ldap: + # Enables or disables the LDAP client (default: false) + # If this is set to true, the "registry" section is ignored. + enabled: false + # The URL of the LDAP server + url: ldap://:@:/ + # TLS configuration for the client connection to the LDAP server + tls: + certfiles: + client: + certfile: + keyfile: + # Attribute related configuration for mapping from LDAP entries to Fabric CA attributes + attribute: + # 'names' is an array of strings containing the LDAP attribute names which are + # requested from the LDAP server for an LDAP identity's entry + names: ['uid','member'] + # The 'converters' section is used to convert an LDAP entry to the value of + # a fabric CA attribute. + # For example, the following converts an LDAP 'uid' attribute + # whose value begins with 'revoker' to a fabric CA attribute + # named "hf.Revoker" with a value of "true" (because the boolean expression + # evaluates to true). + # converters: + # - name: hf.Revoker + # value: attr("uid") =~ "revoker*" + converters: + - name: + value: + # The 'maps' section contains named maps which may be referenced by the 'map' + # function in the 'converters' section to map LDAP responses to arbitrary values. + # For example, assume a user has an LDAP attribute named 'member' which has multiple + # values which are each a distinguished name (i.e. a DN). For simplicity, assume the + # values of the 'member' attribute are 'dn1', 'dn2', and 'dn3'. + # Further assume the following configuration. + # converters: + # - name: hf.Registrar.Roles + # value: map(attr("member"),"groups") + # maps: + # groups: + # - name: dn1 + # value: peer + # - name: dn2 + # value: client + # The value of the user's 'hf.Registrar.Roles' attribute is then computed to be + # "peer,client,dn3". This is because the value of 'attr("member")' is + # "dn1,dn2,dn3", and the call to 'map' with a 2nd argument of + # "group" replaces "dn1" with "peer" and "dn2" with "client". + maps: + groups: + - name: + value: + +############################################################################# +# Affiliations section. Fabric CA server can be bootstrapped with the +# affiliations specified in this section. Affiliations are specified as maps. +# For example: +# businessunit1: +# department1: +# - team1 +# businessunit2: +# - department2 +# - department3 +# +# Affiliations are hierarchical in nature. In the above example, +# department1 (used as businessunit1.department1) is the child of businessunit1. +# team1 (used as businessunit1.department1.team1) is the child of department1. +# department2 (used as businessunit2.department2) and department3 (businessunit2.department3) +# are children of businessunit2. +# Note: Affiliations are case sensitive except for the non-leaf affiliations +# (like businessunit1, department1, businessunit2) that are specified in the configuration file, +# which are always stored in lower case. +############################################################################# +affiliations: + org1: + - department1 + - department2 + org2: + - department1 + +############################################################################# +# Signing section +# +# The "default" subsection is used to sign enrollment certificates; +# the default expiration ("expiry" field) is "8760h", which is 1 year in hours. +# +# The "ca" profile subsection is used to sign intermediate CA certificates; +# the default expiration ("expiry" field) is "43800h" which is 5 years in hours. +# Note that "isca" is true, meaning that it issues a CA certificate. +# A maxpathlen of 0 means that the intermediate CA cannot issue other +# intermediate CA certificates, though it can still issue end entity certificates. +# (See RFC 5280, section 4.2.1.9) +# +# The "tls" profile subsection is used to sign TLS certificate requests; +# the default expiration ("expiry" field) is "8760h", which is 1 year in hours. +############################################################################# +signing: + default: + usage: + - digital signature + expiry: 8760h + profiles: + ca: + usage: + - cert sign + - crl sign + expiry: 43800h + caconstraint: + isca: true + maxpathlen: 0 + tls: + usage: + - signing + - key encipherment + - server auth + - client auth + - key agreement + expiry: 8760h + +########################################################################### +# Certificate Signing Request (CSR) section. +# This controls the creation of the root CA certificate. +# The expiration for the root CA certificate is configured with the +# "ca.expiry" field below, whose default value is "131400h" which is +# 15 years in hours. +# The pathlength field is used to limit CA certificate hierarchy as described +# in section 4.2.1.9 of RFC 5280. +# Examples: +# 1) No pathlength value means no limit is requested. +# 2) pathlength == 1 means a limit of 1 is requested which is the default for +# a root CA. This means the root CA can issue intermediate CA certificates, +# but these intermediate CAs may not in turn issue other CA certificates +# though they can still issue end entity certificates. +# 3) pathlength == 0 means a limit of 0 is requested; +# this is the default for an intermediate CA, which means it can not issue +# CA certificates though it can still issue end entity certificates. +########################################################################### +csr: + cn: fabric-ca-server + keyrequest: + algo: ecdsa + size: 256 + names: + - C: US + ST: "North Carolina" + L: + O: Hyperledger + OU: Fabric + hosts: + - localhost + - 127.0.0.1 + - org0-ecert-ca + - org0-ecert-ca.test-network.svc.cluster.local + ca: + expiry: 131400h + pathlength: 1 + +########################################################################### +# Each CA can issue both X509 enrollment certificate as well as Idemix +# Credential. This section specifies configuration for the issuer component +# that is responsible for issuing Idemix credentials. +########################################################################### +idemix: + # Specifies pool size for revocation handles. A revocation handle is an unique identifier of an + # Idemix credential. The issuer will create a pool revocation handles of this specified size. When + # a credential is requested, issuer will get handle from the pool and assign it to the credential. + # Issuer will repopulate the pool with new handles when the last handle in the pool is used. + # A revocation handle and credential revocation information (CRI) are used to create non revocation proof + # by the prover to prove to the verifier that her credential is not revoked. + rhpoolsize: 1000 + + # The Idemix credential issuance is a two step process. First step is to get a nonce from the issuer + # and second step is send credential request that is constructed using the nonce to the isuser to + # request a credential. This configuration property specifies expiration for the nonces. By default is + # nonces expire after 15 seconds. The value is expressed in the time.Duration format (see https://golang.org/pkg/time/#ParseDuration). + nonceexpiration: 15s + + # Specifies interval at which expired nonces are removed from datastore. Default value is 15 minutes. + # The value is expressed in the time.Duration format (see https://golang.org/pkg/time/#ParseDuration) + noncesweepinterval: 15m + +############################################################################# +# BCCSP (BlockChain Crypto Service Provider) section is used to select which +# crypto library implementation to use +############################################################################# +bccsp: + default: SW + sw: + hash: SHA2 + security: 256 + filekeystore: + # The directory used for the software file-based keystore + keystore: msp/keystore + +############################################################################# +# Multi CA section +# +# Each Fabric CA server contains one CA by default. This section is used +# to configure multiple CAs in a single server. +# +# 1) --cacount +# Automatically generate non-default CAs. The names of these +# additional CAs are "ca1", "ca2", ... "caN", where "N" is +# This is particularly useful in a development environment to quickly set up +# multiple CAs. Note that, this config option is not applicable to intermediate CA server +# i.e., Fabric CA server that is started with intermediate.parentserver.url config +# option (-u command line option) +# +# 2) --cafiles +# For each CA config file in the list, generate a separate signing CA. Each CA +# config file in this list MAY contain all of the same elements as are found in +# the server config file except port, debug, and tls sections. +# +# Examples: +# fabric-ca-server start -b admin:adminpw --cacount 2 +# +# fabric-ca-server start -b admin:adminpw --cafiles ca/ca1/fabric-ca-server-config.yaml +# --cafiles ca/ca2/fabric-ca-server-config.yaml +# +############################################################################# + +cacount: + +cafiles: + +############################################################################# +# Intermediate CA section +# +# The relationship between servers and CAs is as follows: +# 1) A single server process may contain or function as one or more CAs. +# This is configured by the "Multi CA section" above. +# 2) Each CA is either a root CA or an intermediate CA. +# 3) Each intermediate CA has a parent CA which is either a root CA or another intermediate CA. +# +# This section pertains to configuration of #2 and #3. +# If the "intermediate.parentserver.url" property is set, +# then this is an intermediate CA with the specified parent +# CA. +# +# parentserver section +# url - The URL of the parent server +# caname - Name of the CA to enroll within the server +# +# enrollment section used to enroll intermediate CA with parent CA +# profile - Name of the signing profile to use in issuing the certificate +# label - Label to use in HSM operations +# +# tls section for secure socket connection +# certfiles - PEM-encoded list of trusted root certificate files +# client: +# certfile - PEM-encoded certificate file for when client authentication +# is enabled on server +# keyfile - PEM-encoded key file for when client authentication +# is enabled on server +############################################################################# +intermediate: + parentserver: + url: + caname: + + enrollment: + hosts: + profile: + label: + + tls: + certfiles: + client: + certfile: + keyfile: + +############################################################################# +# CA configuration section +# +# Configure the number of incorrect password attempts are allowed for +# identities. By default, the value of 'passwordattempts' is 10, which +# means that 10 incorrect password attempts can be made before an identity get +# locked out. +############################################################################# +cfg: + identities: + passwordattempts: 10 + +############################################################################### +# +# Operations section +# +############################################################################### +operations: + # host and port for the operations server + listenAddress: 127.0.0.1:9443 + + # TLS configuration for the operations endpoint + tls: + # TLS enabled + enabled: false + + # path to PEM encoded server certificate for the operations server + cert: + file: + + # path to PEM encoded server key for the operations server + key: + file: + + # require client certificate authentication to access all resources + clientAuthRequired: false + + # paths to PEM encoded ca certificates to trust for client authentication + clientRootCAs: + files: [] + +############################################################################### +# +# Metrics section +# +############################################################################### +metrics: + # statsd, prometheus, or disabled + provider: disabled + + # statsd configuration + statsd: + # network type: tcp or udp + network: udp + + # statsd server address + address: 127.0.0.1:8125 + + # the interval at which locally cached counters and gauges are pushsed + # to statsd; timings are pushed immediately + writeInterval: 10s + + # prefix is prepended to all emitted statsd merics + prefix: server diff --git a/test-network-k8s/config/org0/fabric-tls-ca-server-config.yaml b/test-network-k8s/config/org0/fabric-tls-ca-server-config.yaml new file mode 100644 index 00000000..b574e72b --- /dev/null +++ b/test-network-k8s/config/org0/fabric-tls-ca-server-config.yaml @@ -0,0 +1,496 @@ +############################################################################# +# This is a configuration file for the fabric-ca-server command. +# +# COMMAND LINE ARGUMENTS AND ENVIRONMENT VARIABLES +# ------------------------------------------------ +# Each configuration element can be overridden via command line +# arguments or environment variables. The precedence for determining +# the value of each element is as follows: +# 1) command line argument +# Examples: +# a) --port 443 +# To set the listening port +# b) --ca.keyfile ../mykey.pem +# To set the "keyfile" element in the "ca" section below; +# note the '.' separator character. +# 2) environment variable +# Examples: +# a) FABRIC_CA_SERVER_PORT=443 +# To set the listening port +# b) FABRIC_CA_SERVER_CA_KEYFILE="../mykey.pem" +# To set the "keyfile" element in the "ca" section below; +# note the '_' separator character. +# 3) configuration file +# 4) default value (if there is one) +# All default values are shown beside each element below. +# +# FILE NAME ELEMENTS +# ------------------ +# The value of all fields whose name ends with "file" or "files" are +# name or names of other files. +# For example, see "tls.certfile" and "tls.clientauth.certfiles". +# The value of each of these fields can be a simple filename, a +# relative path, or an absolute path. If the value is not an +# absolute path, it is interpretted as being relative to the location +# of this configuration file. +# +############################################################################# + +# Version of config file +version: 1.5.2 + +# Server's listening port (default: 7054) +port: 443 + +# Cross-Origin Resource Sharing (CORS) +cors: + enabled: false + origins: + - "*" + +# Enables debug logging (default: false) +debug: false + +# Size limit of an acceptable CRL in bytes (default: 512000) +crlsizelimit: 512000 + +############################################################################# +# TLS section for the server's listening port +# +# The following types are supported for client authentication: NoClientCert, +# RequestClientCert, RequireAnyClientCert, VerifyClientCertIfGiven, +# and RequireAndVerifyClientCert. +# +# Certfiles is a list of root certificate authorities that the server uses +# when verifying client certificates. +############################################################################# +tls: + # Enable TLS (default: false) + enabled: true + # TLS for the server's listening port + certfile: + keyfile: + clientauth: + type: noclientcert + certfiles: + +############################################################################# +# The CA section contains information related to the Certificate Authority +# including the name of the CA, which should be unique for all members +# of a blockchain network. It also includes the key and certificate files +# used when issuing enrollment certificates (ECerts) and transaction +# certificates (TCerts). +# The chainfile (if it exists) contains the certificate chain which +# should be trusted for this CA, where the 1st in the chain is always the +# root CA certificate. +############################################################################# +ca: + # Name of this CA + name: org0-tls-ca + # Key file (is only used to import a private key into BCCSP) + keyfile: + # Certificate file (default: ca-cert.pem) + certfile: + # Chain file + chainfile: + +############################################################################# +# The gencrl REST endpoint is used to generate a CRL that contains revoked +# certificates. This section contains configuration options that are used +# during gencrl request processing. +############################################################################# +crl: + # Specifies expiration for the generated CRL. The number of hours + # specified by this property is added to the UTC time, the resulting time + # is used to set the 'Next Update' date of the CRL. + expiry: 24h + +############################################################################# +# The registry section controls how the fabric-ca-server does two things: +# 1) authenticates enrollment requests which contain a username and password +# (also known as an enrollment ID and secret). +# 2) once authenticated, retrieves the identity's attribute names and +# values which the fabric-ca-server optionally puts into TCerts +# which it issues for transacting on the Hyperledger Fabric blockchain. +# These attributes are useful for making access control decisions in +# chaincode. +# There are two main configuration options: +# 1) The fabric-ca-server is the registry. +# This is true if "ldap.enabled" in the ldap section below is false. +# 2) An LDAP server is the registry, in which case the fabric-ca-server +# calls the LDAP server to perform these tasks. +# This is true if "ldap.enabled" in the ldap section below is true, +# which means this "registry" section is ignored. +############################################################################# +registry: + # Maximum number of times a password/secret can be reused for enrollment + # (default: -1, which means there is no limit) + maxenrollments: -1 + + # Contains identity information which is used when LDAP is disabled + identities: + - name: tlsadmin + pass: tlsadminpw + type: client + affiliation: "" + attrs: + hf.Registrar.Roles: "*" + hf.Registrar.DelegateRoles: "*" + hf.Revoker: true + hf.IntermediateCA: true + hf.GenCRL: true + hf.Registrar.Attributes: "*" + hf.AffiliationMgr: true + +############################################################################# +# Database section +# Supported types are: "sqlite3", "postgres", and "mysql". +# The datasource value depends on the type. +# If the type is "sqlite3", the datasource value is a file name to use +# as the database store. Since "sqlite3" is an embedded database, it +# may not be used if you want to run the fabric-ca-server in a cluster. +# To run the fabric-ca-server in a cluster, you must choose "postgres" +# or "mysql". +############################################################################# +db: + type: sqlite3 + datasource: fabric-ca-server.db + tls: + enabled: false + certfiles: + client: + certfile: + keyfile: + +############################################################################# +# LDAP section +# If LDAP is enabled, the fabric-ca-server calls LDAP to: +# 1) authenticate enrollment ID and secret (i.e. username and password) +# for enrollment requests; +# 2) To retrieve identity attributes +############################################################################# +ldap: + # Enables or disables the LDAP client (default: false) + # If this is set to true, the "registry" section is ignored. + enabled: false + # The URL of the LDAP server + url: ldap://:@:/ + # TLS configuration for the client connection to the LDAP server + tls: + certfiles: + client: + certfile: + keyfile: + # Attribute related configuration for mapping from LDAP entries to Fabric CA attributes + attribute: + # 'names' is an array of strings containing the LDAP attribute names which are + # requested from the LDAP server for an LDAP identity's entry + names: ['uid','member'] + # The 'converters' section is used to convert an LDAP entry to the value of + # a fabric CA attribute. + # For example, the following converts an LDAP 'uid' attribute + # whose value begins with 'revoker' to a fabric CA attribute + # named "hf.Revoker" with a value of "true" (because the boolean expression + # evaluates to true). + # converters: + # - name: hf.Revoker + # value: attr("uid") =~ "revoker*" + converters: + - name: + value: + # The 'maps' section contains named maps which may be referenced by the 'map' + # function in the 'converters' section to map LDAP responses to arbitrary values. + # For example, assume a user has an LDAP attribute named 'member' which has multiple + # values which are each a distinguished name (i.e. a DN). For simplicity, assume the + # values of the 'member' attribute are 'dn1', 'dn2', and 'dn3'. + # Further assume the following configuration. + # converters: + # - name: hf.Registrar.Roles + # value: map(attr("member"),"groups") + # maps: + # groups: + # - name: dn1 + # value: peer + # - name: dn2 + # value: client + # The value of the user's 'hf.Registrar.Roles' attribute is then computed to be + # "peer,client,dn3". This is because the value of 'attr("member")' is + # "dn1,dn2,dn3", and the call to 'map' with a 2nd argument of + # "group" replaces "dn1" with "peer" and "dn2" with "client". + maps: + groups: + - name: + value: + +############################################################################# +# Affiliations section. Fabric CA server can be bootstrapped with the +# affiliations specified in this section. Affiliations are specified as maps. +# For example: +# businessunit1: +# department1: +# - team1 +# businessunit2: +# - department2 +# - department3 +# +# Affiliations are hierarchical in nature. In the above example, +# department1 (used as businessunit1.department1) is the child of businessunit1. +# team1 (used as businessunit1.department1.team1) is the child of department1. +# department2 (used as businessunit2.department2) and department3 (businessunit2.department3) +# are children of businessunit2. +# Note: Affiliations are case sensitive except for the non-leaf affiliations +# (like businessunit1, department1, businessunit2) that are specified in the configuration file, +# which are always stored in lower case. +############################################################################# +affiliations: + org1: + - department1 + - department2 + org2: + - department1 + +############################################################################# +# Signing section +# +# The "default" subsection is used to sign enrollment certificates; +# the default expiration ("expiry" field) is "8760h", which is 1 year in hours. +# +# The "ca" profile subsection is used to sign intermediate CA certificates; +# the default expiration ("expiry" field) is "43800h" which is 5 years in hours. +# Note that "isca" is true, meaning that it issues a CA certificate. +# A maxpathlen of 0 means that the intermediate CA cannot issue other +# intermediate CA certificates, though it can still issue end entity certificates. +# (See RFC 5280, section 4.2.1.9) +# +# The "tls" profile subsection is used to sign TLS certificate requests; +# the default expiration ("expiry" field) is "8760h", which is 1 year in hours. +############################################################################# +signing: + default: + authremote: {} + caconstraint: {} + expiry: 8760h + usage: + - signing + - key encipherment + - server auth + - client auth + - key agreement + profiles: null + +########################################################################### +# Certificate Signing Request (CSR) section. +# This controls the creation of the root CA certificate. +# The expiration for the root CA certificate is configured with the +# "ca.expiry" field below, whose default value is "131400h" which is +# 15 years in hours. +# The pathlength field is used to limit CA certificate hierarchy as described +# in section 4.2.1.9 of RFC 5280. +# Examples: +# 1) No pathlength value means no limit is requested. +# 2) pathlength == 1 means a limit of 1 is requested which is the default for +# a root CA. This means the root CA can issue intermediate CA certificates, +# but these intermediate CAs may not in turn issue other CA certificates +# though they can still issue end entity certificates. +# 3) pathlength == 0 means a limit of 0 is requested; +# this is the default for an intermediate CA, which means it can not issue +# CA certificates though it can still issue end entity certificates. +########################################################################### +csr: + cn: fabric-ca-server + keyrequest: + algo: ecdsa + size: 256 + names: + - C: US + ST: "North Carolina" + L: + O: Hyperledger + OU: Fabric + hosts: + - localhost + - 127.0.0.1 + - org0-tls-ca + - org0-tls-ca.test-network.svc.cluster.local + ca: + expiry: 131400h + pathlength: 1 + +########################################################################### +# Each CA can issue both X509 enrollment certificate as well as Idemix +# Credential. This section specifies configuration for the issuer component +# that is responsible for issuing Idemix credentials. +########################################################################### +idemix: + # Specifies pool size for revocation handles. A revocation handle is an unique identifier of an + # Idemix credential. The issuer will create a pool revocation handles of this specified size. When + # a credential is requested, issuer will get handle from the pool and assign it to the credential. + # Issuer will repopulate the pool with new handles when the last handle in the pool is used. + # A revocation handle and credential revocation information (CRI) are used to create non revocation proof + # by the prover to prove to the verifier that her credential is not revoked. + rhpoolsize: 1000 + + # The Idemix credential issuance is a two step process. First step is to get a nonce from the issuer + # and second step is send credential request that is constructed using the nonce to the isuser to + # request a credential. This configuration property specifies expiration for the nonces. By default is + # nonces expire after 15 seconds. The value is expressed in the time.Duration format (see https://golang.org/pkg/time/#ParseDuration). + nonceexpiration: 15s + + # Specifies interval at which expired nonces are removed from datastore. Default value is 15 minutes. + # The value is expressed in the time.Duration format (see https://golang.org/pkg/time/#ParseDuration) + noncesweepinterval: 15m + +############################################################################# +# BCCSP (BlockChain Crypto Service Provider) section is used to select which +# crypto library implementation to use +############################################################################# +bccsp: + default: SW + sw: + hash: SHA2 + security: 256 + filekeystore: + # The directory used for the software file-based keystore + keystore: msp/keystore + +############################################################################# +# Multi CA section +# +# Each Fabric CA server contains one CA by default. This section is used +# to configure multiple CAs in a single server. +# +# 1) --cacount +# Automatically generate non-default CAs. The names of these +# additional CAs are "ca1", "ca2", ... "caN", where "N" is +# This is particularly useful in a development environment to quickly set up +# multiple CAs. Note that, this config option is not applicable to intermediate CA server +# i.e., Fabric CA server that is started with intermediate.parentserver.url config +# option (-u command line option) +# +# 2) --cafiles +# For each CA config file in the list, generate a separate signing CA. Each CA +# config file in this list MAY contain all of the same elements as are found in +# the server config file except port, debug, and tls sections. +# +# Examples: +# fabric-ca-server start -b admin:adminpw --cacount 2 +# +# fabric-ca-server start -b admin:adminpw --cafiles ca/ca1/fabric-ca-server-config.yaml +# --cafiles ca/ca2/fabric-ca-server-config.yaml +# +############################################################################# + +cacount: + +cafiles: + +############################################################################# +# Intermediate CA section +# +# The relationship between servers and CAs is as follows: +# 1) A single server process may contain or function as one or more CAs. +# This is configured by the "Multi CA section" above. +# 2) Each CA is either a root CA or an intermediate CA. +# 3) Each intermediate CA has a parent CA which is either a root CA or another intermediate CA. +# +# This section pertains to configuration of #2 and #3. +# If the "intermediate.parentserver.url" property is set, +# then this is an intermediate CA with the specified parent +# CA. +# +# parentserver section +# url - The URL of the parent server +# caname - Name of the CA to enroll within the server +# +# enrollment section used to enroll intermediate CA with parent CA +# profile - Name of the signing profile to use in issuing the certificate +# label - Label to use in HSM operations +# +# tls section for secure socket connection +# certfiles - PEM-encoded list of trusted root certificate files +# client: +# certfile - PEM-encoded certificate file for when client authentication +# is enabled on server +# keyfile - PEM-encoded key file for when client authentication +# is enabled on server +############################################################################# +intermediate: + parentserver: + url: + caname: + + enrollment: + hosts: + profile: + label: + + tls: + certfiles: + client: + certfile: + keyfile: + +############################################################################# +# CA configuration section +# +# Configure the number of incorrect password attempts are allowed for +# identities. By default, the value of 'passwordattempts' is 10, which +# means that 10 incorrect password attempts can be made before an identity get +# locked out. +############################################################################# +cfg: + identities: + passwordattempts: 10 + +############################################################################### +# +# Operations section +# +############################################################################### +operations: + # host and port for the operations server + listenAddress: 127.0.0.1:9444 + + # TLS configuration for the operations endpoint + tls: + # TLS enabled + enabled: false + + # path to PEM encoded server certificate for the operations server + cert: + file: + + # path to PEM encoded server key for the operations server + key: + file: + + # require client certificate authentication to access all resources + clientAuthRequired: false + + # paths to PEM encoded ca certificates to trust for client authentication + clientRootCAs: + files: [] + +############################################################################### +# +# Metrics section +# +############################################################################### +metrics: + # statsd, prometheus, or disabled + provider: disabled + + # statsd configuration + statsd: + # network type: tcp or udp + network: udp + + # statsd server address + address: 127.0.0.1:8125 + + # the interval at which locally cached counters and gauges are pushsed + # to statsd; timings are pushed immediately + writeInterval: 10s + + # prefix is prepended to all emitted statsd merics + prefix: server diff --git a/test-network-k8s/config/org0/orderer.yaml b/test-network-k8s/config/org0/orderer.yaml new file mode 100644 index 00000000..c8e25a07 --- /dev/null +++ b/test-network-k8s/config/org0/orderer.yaml @@ -0,0 +1,420 @@ +# Copyright IBM Corp. All Rights Reserved. +# +# SPDX-License-Identifier: Apache-2.0 +# + + +--- +################################################################################ +# +# Orderer Configuration +# +# - This controls the type and configuration of the orderer. +# +################################################################################ +General: + # Listen address: The IP on which to bind to listen. + ListenAddress: 0.0.0.0 + + # Listen port: The port on which to bind to listen. + ListenPort: 6050 + + # TLS: TLS settings for the GRPC server. + TLS: + # Require server-side TLS + Enabled: false + # PrivateKey governs the file location of the private key of the TLS certificate. + PrivateKey: tls/server.key + # Certificate governs the file location of the server TLS certificate. + Certificate: tls/server.crt + # RootCAs contains a list of additional root certificates used for verifying certificates + # of other orderer nodes during outbound connections. + # It is not required to be set, but can be used to augment the set of TLS CA certificates + # available from the MSPs of each channel’s configuration. + RootCAs: + - tls/ca.crt + # Require client certificates / mutual TLS for inbound connections. + ClientAuthRequired: false + # If mutual TLS is enabled, ClientRootCAs contains a list of additional root certificates + # used for verifying certificates of client connections. + # It is not required to be set, but can be used to augment the set of TLS CA certificates + # available from the MSPs of each channel’s configuration. + ClientRootCAs: + # Keepalive settings for the GRPC server. + Keepalive: + # ServerMinInterval is the minimum permitted time between client pings. + # If clients send pings more frequently, the server will + # disconnect them. + ServerMinInterval: 60s + # ServerInterval is the time between pings to clients. + ServerInterval: 7200s + # ServerTimeout is the duration the server waits for a response from + # a client before closing the connection. + ServerTimeout: 20s + # Cluster settings for ordering service nodes that communicate with other ordering service nodes + # such as Raft based ordering service. + Cluster: + # SendBufferSize is the maximum number of messages in the egress buffer. + # Consensus messages are dropped if the buffer is full, and transaction + # messages are waiting for space to be freed. + SendBufferSize: 10 + + # ClientCertificate governs the file location of the client TLS certificate + # used to establish mutual TLS connections with other ordering service nodes. + # If not set, the server General.TLS.Certificate is re-used. + ClientCertificate: + # ClientPrivateKey governs the file location of the private key of the client TLS certificate. + # If not set, the server General.TLS.PrivateKey is re-used. + ClientPrivateKey: + + # The below 4 properties should be either set together, or be unset together. + # If they are set, then the orderer node uses a separate listener for intra-cluster + # communication. If they are unset, then the general orderer listener is used. + # This is useful if you want to use a different TLS server certificates on the + # client-facing and the intra-cluster listeners. + + # ListenPort defines the port on which the cluster listens to connections. + ListenPort: + # ListenAddress defines the IP on which to listen to intra-cluster communication. + ListenAddress: + # ServerCertificate defines the file location of the server TLS certificate used for intra-cluster + # communication. + ServerCertificate: + # ServerPrivateKey defines the file location of the private key of the TLS certificate. + ServerPrivateKey: + + # Bootstrap method: The method by which to obtain the bootstrap block + # system channel is specified. The option can be one of: + # "file" - path to a file containing the genesis block or config block of system channel + # "none" - allows an orderer to start without a system channel configuration + BootstrapMethod: none + + # Bootstrap file: The file containing the bootstrap block to use when + # initializing the orderer system channel and BootstrapMethod is set to + # "file". The bootstrap file can be the genesis block, and it can also be + # a config block for late bootstrap of some consensus methods like Raft. + # Generate a genesis block by updating $FABRIC_CFG_PATH/configtx.yaml and + # using configtxgen command with "-outputBlock" option. + # Defaults to file "genesisblock" (in $FABRIC_CFG_PATH directory) if not specified. + BootstrapFile: + + # LocalMSPDir is where to find the private crypto material needed by the + # orderer. It is set relative here as a default for dev environments but + # should be changed to the real location in production. + LocalMSPDir: msp + + # LocalMSPID is the identity to register the local MSP material with the MSP + # manager. IMPORTANT: The local MSP ID of an orderer needs to match the MSP + # ID of one of the organizations defined in the orderer system channel's + # /Channel/Orderer configuration. The sample organization defined in the + # sample configuration provided has an MSP ID of "SampleOrg". + LocalMSPID: SampleOrg + + # Enable an HTTP service for Go "pprof" profiling as documented at: + # https://golang.org/pkg/net/http/pprof + Profile: + Enabled: false + Address: 0.0.0.0:6060 + + # BCCSP configures the blockchain crypto service providers. + BCCSP: + # Default specifies the preferred blockchain crypto service provider + # to use. If the preferred provider is not available, the software + # based provider ("SW") will be used. + # Valid providers are: + # - SW: a software based crypto provider + # - PKCS11: a CA hardware security module crypto provider. + Default: SW + + # SW configures the software based blockchain crypto provider. + SW: + # TODO: The default Hash and Security level needs refactoring to be + # fully configurable. Changing these defaults requires coordination + # SHA2 is hardcoded in several places, not only BCCSP + Hash: SHA2 + Security: 256 + # Location of key store. If this is unset, a location will be + # chosen using: 'LocalMSPDir'/keystore + FileKeyStore: + KeyStore: + + # Settings for the PKCS#11 crypto provider (i.e. when DEFAULT: PKCS11) + PKCS11: + # Location of the PKCS11 module library + Library: + # Token Label + Label: + # User PIN + Pin: + Hash: + Security: + FileKeyStore: + KeyStore: + + # Authentication contains configuration parameters related to authenticating + # client messages + Authentication: + # the acceptable difference between the current server time and the + # client's time as specified in a client request message + TimeWindow: 15m + + +################################################################################ +# +# SECTION: File Ledger +# +# - This section applies to the configuration of the file ledger. +# +################################################################################ +FileLedger: + + # Location: The directory to store the blocks in. + Location: /var/hyperledger/production/orderer + +################################################################################ +# +# SECTION: Kafka +# +# - This section applies to the configuration of the Kafka-based orderer, and +# its interaction with the Kafka cluster. +# +################################################################################ +Kafka: + + # Retry: What do if a connection to the Kafka cluster cannot be established, + # or if a metadata request to the Kafka cluster needs to be repeated. + Retry: + # When a new channel is created, or when an existing channel is reloaded + # (in case of a just-restarted orderer), the orderer interacts with the + # Kafka cluster in the following ways: + # 1. It creates a Kafka producer (writer) for the Kafka partition that + # corresponds to the channel. + # 2. It uses that producer to post a no-op CONNECT message to that + # partition + # 3. It creates a Kafka consumer (reader) for that partition. + # If any of these steps fail, they will be re-attempted every + # for a total of , and then every + # for a total of until they succeed. + # Note that the orderer will be unable to write to or read from a + # channel until all of the steps above have been completed successfully. + ShortInterval: 5s + ShortTotal: 10m + LongInterval: 5m + LongTotal: 12h + # Affects the socket timeouts when waiting for an initial connection, a + # response, or a transmission. See Config.Net for more info: + # https://godoc.org/github.com/Shopify/sarama#Config + NetworkTimeouts: + DialTimeout: 10s + ReadTimeout: 10s + WriteTimeout: 10s + # Affects the metadata requests when the Kafka cluster is in the middle + # of a leader election.See Config.Metadata for more info: + # https://godoc.org/github.com/Shopify/sarama#Config + Metadata: + RetryBackoff: 250ms + RetryMax: 3 + # What to do if posting a message to the Kafka cluster fails. See + # Config.Producer for more info: + # https://godoc.org/github.com/Shopify/sarama#Config + Producer: + RetryBackoff: 100ms + RetryMax: 3 + # What to do if reading from the Kafka cluster fails. See + # Config.Consumer for more info: + # https://godoc.org/github.com/Shopify/sarama#Config + Consumer: + RetryBackoff: 2s + # Settings to use when creating Kafka topics. Only applies when + # Kafka.Version is v0.10.1.0 or higher + Topic: + # The number of Kafka brokers across which to replicate the topic + ReplicationFactor: 3 + # Verbose: Enable logging for interactions with the Kafka cluster. + Verbose: false + + # TLS: TLS settings for the orderer's connection to the Kafka cluster. + TLS: + + # Enabled: Use TLS when connecting to the Kafka cluster. + Enabled: false + + # PrivateKey: PEM-encoded private key the orderer will use for + # authentication. + PrivateKey: + # As an alternative to specifying the PrivateKey here, uncomment the + # following "File" key and specify the file name from which to load the + # value of PrivateKey. + #File: path/to/PrivateKey + + # Certificate: PEM-encoded signed public key certificate the orderer will + # use for authentication. + Certificate: + # As an alternative to specifying the Certificate here, uncomment the + # following "File" key and specify the file name from which to load the + # value of Certificate. + #File: path/to/Certificate + + # RootCAs: PEM-encoded trusted root certificates used to validate + # certificates from the Kafka cluster. + RootCAs: + # As an alternative to specifying the RootCAs here, uncomment the + # following "File" key and specify the file name from which to load the + # value of RootCAs. + #File: path/to/RootCAs + + # SASLPlain: Settings for using SASL/PLAIN authentication with Kafka brokers + SASLPlain: + # Enabled: Use SASL/PLAIN to authenticate with Kafka brokers + Enabled: false + # User: Required when Enabled is set to true + User: + # Password: Required when Enabled is set to true + Password: + + # Kafka protocol version used to communicate with the Kafka cluster brokers + # (defaults to 0.10.2.0 if not specified) + Version: + +################################################################################ +# +# Debug Configuration +# +# - This controls the debugging options for the orderer +# +################################################################################ +Debug: + + # BroadcastTraceDir when set will cause each request to the Broadcast service + # for this orderer to be written to a file in this directory + BroadcastTraceDir: + + # DeliverTraceDir when set will cause each request to the Deliver service + # for this orderer to be written to a file in this directory + DeliverTraceDir: + +################################################################################ +# +# Operations Configuration +# +# - This configures the operations server endpoint for the orderer +# +################################################################################ +Operations: + # host and port for the operations server + ListenAddress: 0.0.0.0:8443 + + # TLS configuration for the operations endpoint + TLS: + # TLS enabled + Enabled: false + + # Certificate is the location of the PEM encoded TLS certificate + Certificate: + + # PrivateKey points to the location of the PEM-encoded key + PrivateKey: + + # Most operations service endpoints require client authentication when TLS + # is enabled. ClientAuthRequired requires client certificate authentication + # at the TLS layer to access all resources. + ClientAuthRequired: false + + # Paths to PEM encoded ca certificates to trust for client authentication + ClientRootCAs: [] + +################################################################################ +# +# Metrics Configuration +# +# - This configures metrics collection for the orderer +# +################################################################################ +Metrics: + # The metrics provider is one of statsd, prometheus, or disabled + Provider: disabled + + # The statsd configuration + Statsd: + # network type: tcp or udp + Network: udp + + # the statsd server address + Address: 127.0.0.1:8125 + + # The interval at which locally cached counters and gauges are pushed + # to statsd; timings are pushed immediately + WriteInterval: 30s + + # The prefix is prepended to all emitted statsd metrics + Prefix: + +################################################################################ +# +# Admin Configuration +# +# - This configures the admin server endpoint for the orderer +# +################################################################################ +Admin: + # host and port for the admin server + ListenAddress: 0.0.0.0:9443 + + # TLS configuration for the admin endpoint + TLS: + # TLS enabled + Enabled: false + + # Certificate is the location of the PEM encoded TLS certificate + Certificate: + + # PrivateKey points to the location of the PEM-encoded key + PrivateKey: + + # Most admin service endpoints require client authentication when TLS + # is enabled. ClientAuthRequired requires client certificate authentication + # at the TLS layer to access all resources. + # + # NOTE: When TLS is enabled, the admin endpoint requires mutual TLS. The + # orderer will panic on startup if this value is set to false. + ClientAuthRequired: true + + # Paths to PEM encoded ca certificates to trust for client authentication + ClientRootCAs: [] + +################################################################################ +# +# Channel participation API Configuration +# +# - This provides the channel participation API configuration for the orderer. +# - Channel participation uses the ListenAddress and TLS settings of the Admin +# service. +# +################################################################################ +ChannelParticipation: + # Channel participation API is enabled. + Enabled: true + + # The maximum size of the request body when joining a channel. + MaxRequestBodySize: 1 MB + + +################################################################################ +# +# Consensus Configuration +# +# - This section contains config options for a consensus plugin. It is opaque +# to orderer, and completely up to consensus implementation to make use of. +# +################################################################################ +Consensus: + # The allowed key-value pairs here depend on consensus plugin. For etcd/raft, + # we use following options: + + # WALDir specifies the location at which Write Ahead Logs for etcd/raft are + # stored. Each channel will have its own subdir named after channel ID. + WALDir: /var/hyperledger/production/orderer/etcdraft/wal + + # SnapDir specifies the location at which snapshots for etcd/raft are + # stored. Each channel will have its own subdir named after channel ID. + SnapDir: /var/hyperledger/production/orderer/etcdraft/snapshot diff --git a/test-network-k8s/config/org1/core.yaml b/test-network-k8s/config/org1/core.yaml new file mode 100644 index 00000000..ca60ae52 --- /dev/null +++ b/test-network-k8s/config/org1/core.yaml @@ -0,0 +1,759 @@ +# Copyright IBM Corp. All Rights Reserved. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################### +# +# Peer section +# +############################################################################### +peer: + + # The peer id provides a name for this peer instance and is used when + # naming docker resources. + id: jdoe + + # The networkId allows for logical separation of networks and is used when + # naming docker resources. + networkId: dev + + # The Address at local network interface this Peer will listen on. + # By default, it will listen on all network interfaces + listenAddress: 0.0.0.0:7051 + + # The endpoint this peer uses to listen for inbound chaincode connections. + # If this is commented-out, the listen address is selected to be + # the peer's address (see below) with port 7052 + # chaincodeListenAddress: 0.0.0.0:7052 + + # The endpoint the chaincode for this peer uses to connect to the peer. + # If this is not specified, the chaincodeListenAddress address is selected. + # And if chaincodeListenAddress is not specified, address is selected from + # peer address (see below). If specified peer address is invalid then it + # will fallback to the auto detected IP (local IP) regardless of the peer + # addressAutoDetect value. + # chaincodeAddress: 0.0.0.0:7052 + + # When used as peer config, this represents the endpoint to other peers + # in the same organization. For peers in other organization, see + # gossip.externalEndpoint for more info. + # When used as CLI config, this means the peer's endpoint to interact with + address: 0.0.0.0:7051 + + # Whether the Peer should programmatically determine its address + # This case is useful for docker containers. + # When set to true, will override peer address. + addressAutoDetect: false + + # Keepalive settings for peer server and clients + keepalive: + # Interval is the duration after which if the server does not see + # any activity from the client it pings the client to see if it's alive + interval: 7200s + # Timeout is the duration the server waits for a response + # from the client after sending a ping before closing the connection + timeout: 20s + # MinInterval is the minimum permitted time between client pings. + # If clients send pings more frequently, the peer server will + # disconnect them + minInterval: 60s + # Client keepalive settings for communicating with other peer nodes + client: + # Interval is the time between pings to peer nodes. This must + # greater than or equal to the minInterval specified by peer + # nodes + interval: 60s + # Timeout is the duration the client waits for a response from + # peer nodes before closing the connection + timeout: 20s + # DeliveryClient keepalive settings for communication with ordering + # nodes. + deliveryClient: + # Interval is the time between pings to ordering nodes. This must + # greater than or equal to the minInterval specified by ordering + # nodes. + interval: 60s + # Timeout is the duration the client waits for a response from + # ordering nodes before closing the connection + timeout: 20s + + + # Gossip related configuration + gossip: + # Bootstrap set to initialize gossip with. + # This is a list of other peers that this peer reaches out to at startup. + # Important: The endpoints here have to be endpoints of peers in the same + # organization, because the peer would refuse connecting to these endpoints + # unless they are in the same organization as the peer. + bootstrap: 127.0.0.1:7051 + + # NOTE: orgLeader and useLeaderElection parameters are mutual exclusive. + # Setting both to true would result in the termination of the peer + # since this is undefined state. If the peers are configured with + # useLeaderElection=false, make sure there is at least 1 peer in the + # organization that its orgLeader is set to true. + + # Defines whenever peer will initialize dynamic algorithm for + # "leader" selection, where leader is the peer to establish + # connection with ordering service and use delivery protocol + # to pull ledger blocks from ordering service. + useLeaderElection: false + # Statically defines peer to be an organization "leader", + # where this means that current peer will maintain connection + # with ordering service and disseminate block across peers in + # its own organization. Multiple peers or all peers in an organization + # may be configured as org leaders, so that they all pull + # blocks directly from ordering service. + orgLeader: true + + # Interval for membershipTracker polling + membershipTrackerInterval: 5s + + # Overrides the endpoint that the peer publishes to peers + # in its organization. For peers in foreign organizations + # see 'externalEndpoint' + endpoint: + # Maximum count of blocks stored in memory + maxBlockCountToStore: 10 + # Max time between consecutive message pushes(unit: millisecond) + maxPropagationBurstLatency: 10ms + # Max number of messages stored until a push is triggered to remote peers + maxPropagationBurstSize: 10 + # Number of times a message is pushed to remote peers + propagateIterations: 1 + # Number of peers selected to push messages to + propagatePeerNum: 3 + # Determines frequency of pull phases(unit: second) + # Must be greater than digestWaitTime + responseWaitTime + pullInterval: 4s + # Number of peers to pull from + pullPeerNum: 3 + # Determines frequency of pulling state info messages from peers(unit: second) + requestStateInfoInterval: 4s + # Determines frequency of pushing state info messages to peers(unit: second) + publishStateInfoInterval: 4s + # Maximum time a stateInfo message is kept until expired + stateInfoRetentionInterval: + # Time from startup certificates are included in Alive messages(unit: second) + publishCertPeriod: 10s + # Should we skip verifying block messages or not (currently not in use) + skipBlockVerification: false + # Dial timeout(unit: second) + dialTimeout: 3s + # Connection timeout(unit: second) + connTimeout: 2s + # Buffer size of received messages + recvBuffSize: 20 + # Buffer size of sending messages + sendBuffSize: 200 + # Time to wait before pull engine processes incoming digests (unit: second) + # Should be slightly smaller than requestWaitTime + digestWaitTime: 1s + # Time to wait before pull engine removes incoming nonce (unit: milliseconds) + # Should be slightly bigger than digestWaitTime + requestWaitTime: 1500ms + # Time to wait before pull engine ends pull (unit: second) + responseWaitTime: 2s + # Alive check interval(unit: second) + aliveTimeInterval: 5s + # Alive expiration timeout(unit: second) + aliveExpirationTimeout: 25s + # Reconnect interval(unit: second) + reconnectInterval: 25s + # Max number of attempts to connect to a peer + maxConnectionAttempts: 120 + # Message expiration factor for alive messages + msgExpirationFactor: 20 + # This is an endpoint that is published to peers outside of the organization. + # If this isn't set, the peer will not be known to other organizations. + externalEndpoint: + # Leader election service configuration + election: + # Longest time peer waits for stable membership during leader election startup (unit: second) + startupGracePeriod: 15s + # Interval gossip membership samples to check its stability (unit: second) + membershipSampleInterval: 1s + # Time passes since last declaration message before peer decides to perform leader election (unit: second) + leaderAliveThreshold: 10s + # Time between peer sends propose message and declares itself as a leader (sends declaration message) (unit: second) + leaderElectionDuration: 5s + + pvtData: + # pullRetryThreshold determines the maximum duration of time private data corresponding for a given block + # would be attempted to be pulled from peers until the block would be committed without the private data + pullRetryThreshold: 60s + # As private data enters the transient store, it is associated with the peer's ledger's height at that time. + # transientstoreMaxBlockRetention defines the maximum difference between the current ledger's height upon commit, + # and the private data residing inside the transient store that is guaranteed not to be purged. + # Private data is purged from the transient store when blocks with sequences that are multiples + # of transientstoreMaxBlockRetention are committed. + transientstoreMaxBlockRetention: 1000 + # pushAckTimeout is the maximum time to wait for an acknowledgement from each peer + # at private data push at endorsement time. + pushAckTimeout: 3s + # Block to live pulling margin, used as a buffer + # to prevent peer from trying to pull private data + # from peers that is soon to be purged in next N blocks. + # This helps a newly joined peer catch up to current + # blockchain height quicker. + btlPullMargin: 10 + # the process of reconciliation is done in an endless loop, while in each iteration reconciler tries to + # pull from the other peers the most recent missing blocks with a maximum batch size limitation. + # reconcileBatchSize determines the maximum batch size of missing private data that will be reconciled in a + # single iteration. + reconcileBatchSize: 10 + # reconcileSleepInterval determines the time reconciler sleeps from end of an iteration until the beginning + # of the next reconciliation iteration. + reconcileSleepInterval: 1m + # reconciliationEnabled is a flag that indicates whether private data reconciliation is enable or not. + reconciliationEnabled: true + # skipPullingInvalidTransactionsDuringCommit is a flag that indicates whether pulling of invalid + # transaction's private data from other peers need to be skipped during the commit time and pulled + # only through reconciler. + skipPullingInvalidTransactionsDuringCommit: false + # implicitCollectionDisseminationPolicy specifies the dissemination policy for the peer's own implicit collection. + # When a peer endorses a proposal that writes to its own implicit collection, below values override the default values + # for disseminating private data. + # Note that it is applicable to all channels the peer has joined. The implication is that requiredPeerCount has to + # be smaller than the number of peers in a channel that has the lowest numbers of peers from the organization. + implicitCollectionDisseminationPolicy: + # requiredPeerCount defines the minimum number of eligible peers to which the peer must successfully + # disseminate private data for its own implicit collection during endorsement. Default value is 0. + requiredPeerCount: 0 + # maxPeerCount defines the maximum number of eligible peers to which the peer will attempt to + # disseminate private data for its own implicit collection during endorsement. Default value is 1. + maxPeerCount: 1 + + # Gossip state transfer related configuration + state: + # indicates whenever state transfer is enabled or not + # default value is true, i.e. state transfer is active + # and takes care to sync up missing blocks allowing + # lagging peer to catch up to speed with rest network. + # Keep in mind that when peer.gossip.useLeaderElection is true + # and there are several peers in the organization, + # or peer.gossip.useLeaderElection is false alongside with + # peer.gossip.orgleader being false, the peer's ledger may lag behind + # the rest of the peers and will never catch up due to state transfer + # being disabled. + enabled: false + # checkInterval interval to check whether peer is lagging behind enough to + # request blocks via state transfer from another peer. + checkInterval: 10s + # responseTimeout amount of time to wait for state transfer response from + # other peers + responseTimeout: 3s + # batchSize the number of blocks to request via state transfer from another peer + batchSize: 10 + # blockBufferSize reflects the size of the re-ordering buffer + # which captures blocks and takes care to deliver them in order + # down to the ledger layer. The actual buffer size is bounded between + # 0 and 2*blockBufferSize, each channel maintains its own buffer + blockBufferSize: 20 + # maxRetries maximum number of re-tries to ask + # for single state transfer request + maxRetries: 3 + + # TLS Settings + tls: + # Require server-side TLS + enabled: true + # Require client certificates / mutual TLS for inbound connections. + # Note that clients that are not configured to use a certificate will + # fail to connect to the peer. + clientAuthRequired: false + # X.509 certificate used for TLS server + cert: + file: tls/server.crt + # Private key used for TLS server + key: + file: tls/server.key + # rootcert.file represents the trusted root certificate chain used for verifying certificates + # of other nodes during outbound connections. + # It is not required to be set, but can be used to augment the set of TLS CA certificates + # available from the MSPs of each channel’s configuration. + rootcert: + file: tls/ca.crt + # If mutual TLS is enabled, clientRootCAs.files contains a list of additional root certificates + # used for verifying certificates of client connections. + # It augments the set of TLS CA certificates available from the MSPs of each channel’s configuration. + # Minimally, set your organization's TLS CA root certificate so that the peer can receive join channel requests. + clientRootCAs: + files: + - tls/ca.crt + # Private key used for TLS when making client connections. + # If not set, peer.tls.key.file will be used instead + clientKey: + file: + # X.509 certificate used for TLS when making client connections. + # If not set, peer.tls.cert.file will be used instead + clientCert: + file: + + # Authentication contains configuration parameters related to authenticating + # client messages + authentication: + # the acceptable difference between the current server time and the + # client's time as specified in a client request message + timewindow: 15m + + # Path on the file system where peer will store data (eg ledger). This + # location must be access control protected to prevent unintended + # modification that might corrupt the peer operations. + fileSystemPath: /var/hyperledger/production + + # BCCSP (Blockchain crypto provider): Select which crypto implementation or + # library to use + BCCSP: + Default: SW + # Settings for the SW crypto provider (i.e. when DEFAULT: SW) + SW: + # TODO: The default Hash and Security level needs refactoring to be + # fully configurable. Changing these defaults requires coordination + # SHA2 is hardcoded in several places, not only BCCSP + Hash: SHA2 + Security: 256 + # Location of Key Store + FileKeyStore: + # If "", defaults to 'mspConfigPath'/keystore + KeyStore: + # Settings for the PKCS#11 crypto provider (i.e. when DEFAULT: PKCS11) + PKCS11: + # Location of the PKCS11 module library + Library: + # Token Label + Label: + # User PIN + Pin: + Hash: + Security: + + # Path on the file system where peer will find MSP local configurations + mspConfigPath: msp + + # Identifier of the local MSP + # ----!!!!IMPORTANT!!!-!!!IMPORTANT!!!-!!!IMPORTANT!!!!---- + # Deployers need to change the value of the localMspId string. + # In particular, the name of the local MSP ID of a peer needs + # to match the name of one of the MSPs in each of the channel + # that this peer is a member of. Otherwise this peer's messages + # will not be identified as valid by other nodes. + localMspId: Org1MSP + + # CLI common client config options + client: + # connection timeout + connTimeout: 3s + + # Delivery service related config + deliveryclient: + # It sets the total time the delivery service may spend in reconnection + # attempts until its retry logic gives up and returns an error + reconnectTotalTimeThreshold: 3600s + + # It sets the delivery service <-> ordering service node connection timeout + connTimeout: 3s + + # It sets the delivery service maximal delay between consecutive retries + reConnectBackoffThreshold: 3600s + + # A list of orderer endpoint addresses which should be overridden + # when found in channel configurations. + addressOverrides: + # - from: + # to: + # caCertsFile: + # - from: + # to: + # caCertsFile: + + # Type for the local MSP - by default it's of type bccsp + localMspType: bccsp + + # Used with Go profiling tools only in none production environment. In + # production, it should be disabled (eg enabled: false) + profile: + enabled: false + listenAddress: 0.0.0.0:6060 + + # Handlers defines custom handlers that can filter and mutate + # objects passing within the peer, such as: + # Auth filter - reject or forward proposals from clients + # Decorators - append or mutate the chaincode input passed to the chaincode + # Endorsers - Custom signing over proposal response payload and its mutation + # Valid handler definition contains: + # - A name which is a factory method name defined in + # core/handlers/library/library.go for statically compiled handlers + # - library path to shared object binary for pluggable filters + # Auth filters and decorators are chained and executed in the order that + # they are defined. For example: + # authFilters: + # - + # name: FilterOne + # library: /opt/lib/filter.so + # - + # name: FilterTwo + # decorators: + # - + # name: DecoratorOne + # - + # name: DecoratorTwo + # library: /opt/lib/decorator.so + # Endorsers are configured as a map that its keys are the endorsement system chaincodes that are being overridden. + # Below is an example that overrides the default ESCC and uses an endorsement plugin that has the same functionality + # as the default ESCC. + # If the 'library' property is missing, the name is used as the constructor method in the builtin library similar + # to auth filters and decorators. + # endorsers: + # escc: + # name: DefaultESCC + # library: /etc/hyperledger/fabric/plugin/escc.so + handlers: + authFilters: + - + name: DefaultAuth + - + name: ExpirationCheck # This filter checks identity x509 certificate expiration + decorators: + - + name: DefaultDecorator + endorsers: + escc: + name: DefaultEndorsement + library: + validators: + vscc: + name: DefaultValidation + library: + + # library: /etc/hyperledger/fabric/plugin/escc.so + # Number of goroutines that will execute transaction validation in parallel. + # By default, the peer chooses the number of CPUs on the machine. Set this + # variable to override that choice. + # NOTE: overriding this value might negatively influence the performance of + # the peer so please change this value only if you know what you're doing + validatorPoolSize: + + # The discovery service is used by clients to query information about peers, + # such as - which peers have joined a certain channel, what is the latest + # channel config, and most importantly - given a chaincode and a channel, + # what possible sets of peers satisfy the endorsement policy. + discovery: + enabled: true + # Whether the authentication cache is enabled or not. + authCacheEnabled: true + # The maximum size of the cache, after which a purge takes place + authCacheMaxSize: 1000 + # The proportion (0 to 1) of entries that remain in the cache after the cache is purged due to overpopulation + authCachePurgeRetentionRatio: 0.75 + # Whether to allow non-admins to perform non channel scoped queries. + # When this is false, it means that only peer admins can perform non channel scoped queries. + orgMembersAllowedAccess: false + + # Limits is used to configure some internal resource limits. + limits: + # Concurrency limits the number of concurrently running requests to a service on each peer. + # Currently this option is only applied to endorser service and deliver service. + # When the property is missing or the value is 0, the concurrency limit is disabled for the service. + concurrency: + # endorserService limits concurrent requests to endorser service that handles chaincode deployment, query and invocation, + # including both user chaincodes and system chaincodes. + endorserService: 2500 + # deliverService limits concurrent event listeners registered to deliver service for blocks and transaction events. + deliverService: 2500 + +############################################################################### +# +# VM section +# +############################################################################### +vm: + + # Endpoint of the vm management system. For docker can be one of the following in general + # unix:///var/run/docker.sock + # http://localhost:2375 + # https://localhost:2376 + endpoint: unix:///var/run/docker.sock + + # settings for docker vms + docker: + tls: + enabled: false + ca: + file: docker/ca.crt + cert: + file: docker/tls.crt + key: + file: docker/tls.key + + # Enables/disables the standard out/err from chaincode containers for + # debugging purposes + attachStdout: false + + # Parameters on creating docker container. + # Container may be efficiently created using ipam & dns-server for cluster + # NetworkMode - sets the networking mode for the container. Supported + # standard values are: `host`(default),`bridge`,`ipvlan`,`none`. + # Dns - a list of DNS servers for the container to use. + # Note: `Privileged` `Binds` `Links` and `PortBindings` properties of + # Docker Host Config are not supported and will not be used if set. + # LogConfig - sets the logging driver (Type) and related options + # (Config) for Docker. For more info, + # https://docs.docker.com/engine/admin/logging/overview/ + # Note: Set LogConfig using Environment Variables is not supported. + hostConfig: + NetworkMode: host + Dns: + # - 192.168.0.1 + LogConfig: + Type: json-file + Config: + max-size: "50m" + max-file: "5" + Memory: 2147483648 + +############################################################################### +# +# Chaincode section +# +############################################################################### +chaincode: + + # The id is used by the Chaincode stub to register the executing Chaincode + # ID with the Peer and is generally supplied through ENV variables + # the `path` form of ID is provided when installing the chaincode. + # The `name` is used for all other requests and can be any string. + id: + path: + name: + + # Generic builder environment, suitable for most chaincode types + builder: $(DOCKER_NS)/fabric-ccenv:$(TWO_DIGIT_VERSION) + + # Enables/disables force pulling of the base docker images (listed below) + # during user chaincode instantiation. + # Useful when using moving image tags (such as :latest) + pull: false + + golang: + # golang will never need more than baseos + runtime: $(DOCKER_NS)/fabric-baseos:$(TWO_DIGIT_VERSION) + + # whether or not golang chaincode should be linked dynamically + dynamicLink: false + + java: + # This is an image based on java:openjdk-8 with addition compiler + # tools added for java shim layer packaging. + # This image is packed with shim layer libraries that are necessary + # for Java chaincode runtime. + runtime: $(DOCKER_NS)/fabric-javaenv:$(TWO_DIGIT_VERSION) + + node: + # This is an image based on node:$(NODE_VER)-alpine + runtime: $(DOCKER_NS)/fabric-nodeenv:$(TWO_DIGIT_VERSION) + + # List of directories to treat as external builders and launchers for + # chaincode. The external builder detection processing will iterate over the + # builders in the order specified below. + externalBuilders: + - path: /var/hyperledger/fabric/chaincode/ccs-builder + name: ccs-builder + propagateEnvironment: + - HOME + - CORE_PEER_ID + - CORE_PEER_LOCALMSPID + + # The maximum duration to wait for the chaincode build and install process + # to complete. + installTimeout: 300s + + # Timeout duration for starting up a container and waiting for Register + # to come through. + startuptimeout: 300s + + # Timeout duration for Invoke and Init calls to prevent runaway. + # This timeout is used by all chaincodes in all the channels, including + # system chaincodes. + # Note that during Invoke, if the image is not available (e.g. being + # cleaned up when in development environment), the peer will automatically + # build the image, which might take more time. In production environment, + # the chaincode image is unlikely to be deleted, so the timeout could be + # reduced accordingly. + executetimeout: 30s + + # There are 2 modes: "dev" and "net". + # In dev mode, user runs the chaincode after starting peer from + # command line on local machine. + # In net mode, peer will run chaincode in a docker container. + mode: net + + # keepalive in seconds. In situations where the communication goes through a + # proxy that does not support keep-alive, this parameter will maintain connection + # between peer and chaincode. + # A value <= 0 turns keepalive off + keepalive: 0 + + # enabled system chaincodes + system: + _lifecycle: enable + cscc: enable + lscc: enable + qscc: enable + + # Logging section for the chaincode container + logging: + # Default level for all loggers within the chaincode container + level: info + # Override default level for the 'shim' logger + shim: warning + # Format for the chaincode container logs + format: '%{color}%{time:2006-01-02 15:04:05.000 MST} [%{module}] %{shortfunc} -> %{level:.4s} %{id:03x}%{color:reset} %{message}' + +############################################################################### +# +# Ledger section - ledger configuration encompasses both the blockchain +# and the state +# +############################################################################### +ledger: + + blockchain: + + state: + # stateDatabase - options are "goleveldb", "CouchDB" + # goleveldb - default state database stored in goleveldb. + # CouchDB - store state database in CouchDB + stateDatabase: goleveldb + # Limit on the number of records to return per query + totalQueryLimit: 100000 + couchDBConfig: + # It is recommended to run CouchDB on the same server as the peer, and + # not map the CouchDB container port to a server port in docker-compose. + # Otherwise proper security must be provided on the connection between + # CouchDB client (on the peer) and server. + couchDBAddress: 127.0.0.1:5984 + # This username must have read and write authority on CouchDB + username: + # The password is recommended to pass as an environment variable + # during start up (eg CORE_LEDGER_STATE_COUCHDBCONFIG_PASSWORD). + # If it is stored here, the file must be access control protected + # to prevent unintended users from discovering the password. + password: + # Number of retries for CouchDB errors + maxRetries: 3 + # Number of retries for CouchDB errors during peer startup. + # The delay between retries doubles for each attempt. + # Default of 10 retries results in 11 attempts over 2 minutes. + maxRetriesOnStartup: 10 + # CouchDB request timeout (unit: duration, e.g. 20s) + requestTimeout: 35s + # Limit on the number of records per each CouchDB query + # Note that chaincode queries are only bound by totalQueryLimit. + # Internally the chaincode may execute multiple CouchDB queries, + # each of size internalQueryLimit. + internalQueryLimit: 1000 + # Limit on the number of records per CouchDB bulk update batch + maxBatchUpdateSize: 1000 + # Warm indexes after every N blocks. + # This option warms any indexes that have been + # deployed to CouchDB after every N blocks. + # A value of 1 will warm indexes after every block commit, + # to ensure fast selector queries. + # Increasing the value may improve write efficiency of peer and CouchDB, + # but may degrade query response time. + warmIndexesAfterNBlocks: 1 + # Create the _global_changes system database + # This is optional. Creating the global changes database will require + # additional system resources to track changes and maintain the database + createGlobalChangesDB: false + # CacheSize denotes the maximum mega bytes (MB) to be allocated for the in-memory state + # cache. Note that CacheSize needs to be a multiple of 32 MB. If it is not a multiple + # of 32 MB, the peer would round the size to the next multiple of 32 MB. + # To disable the cache, 0 MB needs to be assigned to the cacheSize. + cacheSize: 64 + + history: + # enableHistoryDatabase - options are true or false + # Indicates if the history of key updates should be stored. + # All history 'index' will be stored in goleveldb, regardless if using + # CouchDB or alternate database for the state. + enableHistoryDatabase: true + + pvtdataStore: + # the maximum db batch size for converting + # the ineligible missing data entries to eligible missing data entries + collElgProcMaxDbBatchSize: 5000 + # the minimum duration (in milliseconds) between writing + # two consecutive db batches for converting the ineligible missing data entries to eligible missing data entries + collElgProcDbBatchesInterval: 1000 + # The missing data entries are classified into two categories: + # (1) prioritized + # (2) deprioritized + # Initially, all missing data are in the prioritized list. When the + # reconciler is unable to fetch the missing data from other peers, + # the unreconciled missing data would be moved to the deprioritized list. + # The reconciler would retry deprioritized missing data after every + # deprioritizedDataReconcilerInterval (unit: minutes). Note that the + # interval needs to be greater than the reconcileSleepInterval + deprioritizedDataReconcilerInterval: 60m + + snapshots: + # Path on the file system where peer will store ledger snapshots + rootDir: /var/hyperledger/production/snapshots + +############################################################################### +# +# Operations section +# +############################################################################### +operations: + # host and port for the operations server + listenAddress: 127.0.0.1:9443 + + # TLS configuration for the operations endpoint + tls: + # TLS enabled + enabled: false + + # path to PEM encoded server certificate for the operations server + cert: + file: + + # path to PEM encoded server key for the operations server + key: + file: + + # most operations service endpoints require client authentication when TLS + # is enabled. clientAuthRequired requires client certificate authentication + # at the TLS layer to access all resources. + clientAuthRequired: false + + # paths to PEM encoded ca certificates to trust for client authentication + clientRootCAs: + files: [] + +############################################################################### +# +# Metrics section +# +############################################################################### +metrics: + # metrics provider is one of statsd, prometheus, or disabled + provider: disabled + + # statsd configuration + statsd: + # network type: tcp or udp + network: udp + + # statsd server address + address: 127.0.0.1:8125 + + # the interval at which locally cached counters and gauges are pushed + # to statsd; timings are pushed immediately + writeInterval: 10s + + # prefix is prepended to all emitted statsd metrics + prefix: diff --git a/test-network-k8s/config/org1/fabric-ecert-ca-server-config.yaml b/test-network-k8s/config/org1/fabric-ecert-ca-server-config.yaml new file mode 100644 index 00000000..f1ed9da4 --- /dev/null +++ b/test-network-k8s/config/org1/fabric-ecert-ca-server-config.yaml @@ -0,0 +1,506 @@ +############################################################################# +# This is a configuration file for the fabric-ca-server command. +# +# COMMAND LINE ARGUMENTS AND ENVIRONMENT VARIABLES +# ------------------------------------------------ +# Each configuration element can be overridden via command line +# arguments or environment variables. The precedence for determining +# the value of each element is as follows: +# 1) command line argument +# Examples: +# a) --port 443 +# To set the listening port +# b) --ca.keyfile ../mykey.pem +# To set the "keyfile" element in the "ca" section below; +# note the '.' separator character. +# 2) environment variable +# Examples: +# a) FABRIC_CA_SERVER_PORT=443 +# To set the listening port +# b) FABRIC_CA_SERVER_CA_KEYFILE="../mykey.pem" +# To set the "keyfile" element in the "ca" section below; +# note the '_' separator character. +# 3) configuration file +# 4) default value (if there is one) +# All default values are shown beside each element below. +# +# FILE NAME ELEMENTS +# ------------------ +# The value of all fields whose name ends with "file" or "files" are +# name or names of other files. +# For example, see "tls.certfile" and "tls.clientauth.certfiles". +# The value of each of these fields can be a simple filename, a +# relative path, or an absolute path. If the value is not an +# absolute path, it is interpretted as being relative to the location +# of this configuration file. +# +############################################################################# + +# Version of config file +version: 1.5.2 + +# Server's listening port (default: 7054) +port: 443 + +# Cross-Origin Resource Sharing (CORS) +cors: + enabled: false + origins: + - "*" + +# Enables debug logging (default: false) +debug: false + +# Size limit of an acceptable CRL in bytes (default: 512000) +crlsizelimit: 512000 + +############################################################################# +# TLS section for the server's listening port +# +# The following types are supported for client authentication: NoClientCert, +# RequestClientCert, RequireAnyClientCert, VerifyClientCertIfGiven, +# and RequireAndVerifyClientCert. +# +# Certfiles is a list of root certificate authorities that the server uses +# when verifying client certificates. +############################################################################# +tls: + # Enable TLS (default: false) + enabled: true + # TLS for the server's listening port + certfile: + keyfile: + clientauth: + type: noclientcert + certfiles: + +############################################################################# +# The CA section contains information related to the Certificate Authority +# including the name of the CA, which should be unique for all members +# of a blockchain network. It also includes the key and certificate files +# used when issuing enrollment certificates (ECerts) and transaction +# certificates (TCerts). +# The chainfile (if it exists) contains the certificate chain which +# should be trusted for this CA, where the 1st in the chain is always the +# root CA certificate. +############################################################################# +ca: + # Name of this CA + name: org1-ecert-ca + # Key file (is only used to import a private key into BCCSP) + keyfile: + # Certificate file (default: ca-cert.pem) + certfile: + # Chain file + chainfile: + +############################################################################# +# The gencrl REST endpoint is used to generate a CRL that contains revoked +# certificates. This section contains configuration options that are used +# during gencrl request processing. +############################################################################# +crl: + # Specifies expiration for the generated CRL. The number of hours + # specified by this property is added to the UTC time, the resulting time + # is used to set the 'Next Update' date of the CRL. + expiry: 24h + +############################################################################# +# The registry section controls how the fabric-ca-server does two things: +# 1) authenticates enrollment requests which contain a username and password +# (also known as an enrollment ID and secret). +# 2) once authenticated, retrieves the identity's attribute names and +# values which the fabric-ca-server optionally puts into TCerts +# which it issues for transacting on the Hyperledger Fabric blockchain. +# These attributes are useful for making access control decisions in +# chaincode. +# There are two main configuration options: +# 1) The fabric-ca-server is the registry. +# This is true if "ldap.enabled" in the ldap section below is false. +# 2) An LDAP server is the registry, in which case the fabric-ca-server +# calls the LDAP server to perform these tasks. +# This is true if "ldap.enabled" in the ldap section below is true, +# which means this "registry" section is ignored. +############################################################################# +registry: + # Maximum number of times a password/secret can be reused for enrollment + # (default: -1, which means there is no limit) + maxenrollments: -1 + + # Contains identity information which is used when LDAP is disabled + identities: + - name: rcaadmin + pass: rcaadminpw + type: client + affiliation: "" + attrs: + hf.Registrar.Roles: "*" + hf.Registrar.DelegateRoles: "*" + hf.Revoker: true + hf.IntermediateCA: true + hf.GenCRL: true + hf.Registrar.Attributes: "*" + hf.AffiliationMgr: true + +############################################################################# +# Database section +# Supported types are: "sqlite3", "postgres", and "mysql". +# The datasource value depends on the type. +# If the type is "sqlite3", the datasource value is a file name to use +# as the database store. Since "sqlite3" is an embedded database, it +# may not be used if you want to run the fabric-ca-server in a cluster. +# To run the fabric-ca-server in a cluster, you must choose "postgres" +# or "mysql". +############################################################################# +db: + type: sqlite3 + datasource: fabric-ca-server.db + tls: + enabled: false + certfiles: + client: + certfile: + keyfile: + +############################################################################# +# LDAP section +# If LDAP is enabled, the fabric-ca-server calls LDAP to: +# 1) authenticate enrollment ID and secret (i.e. username and password) +# for enrollment requests; +# 2) To retrieve identity attributes +############################################################################# +ldap: + # Enables or disables the LDAP client (default: false) + # If this is set to true, the "registry" section is ignored. + enabled: false + # The URL of the LDAP server + url: ldap://:@:/ + # TLS configuration for the client connection to the LDAP server + tls: + certfiles: + client: + certfile: + keyfile: + # Attribute related configuration for mapping from LDAP entries to Fabric CA attributes + attribute: + # 'names' is an array of strings containing the LDAP attribute names which are + # requested from the LDAP server for an LDAP identity's entry + names: ['uid','member'] + # The 'converters' section is used to convert an LDAP entry to the value of + # a fabric CA attribute. + # For example, the following converts an LDAP 'uid' attribute + # whose value begins with 'revoker' to a fabric CA attribute + # named "hf.Revoker" with a value of "true" (because the boolean expression + # evaluates to true). + # converters: + # - name: hf.Revoker + # value: attr("uid") =~ "revoker*" + converters: + - name: + value: + # The 'maps' section contains named maps which may be referenced by the 'map' + # function in the 'converters' section to map LDAP responses to arbitrary values. + # For example, assume a user has an LDAP attribute named 'member' which has multiple + # values which are each a distinguished name (i.e. a DN). For simplicity, assume the + # values of the 'member' attribute are 'dn1', 'dn2', and 'dn3'. + # Further assume the following configuration. + # converters: + # - name: hf.Registrar.Roles + # value: map(attr("member"),"groups") + # maps: + # groups: + # - name: dn1 + # value: peer + # - name: dn2 + # value: client + # The value of the user's 'hf.Registrar.Roles' attribute is then computed to be + # "peer,client,dn3". This is because the value of 'attr("member")' is + # "dn1,dn2,dn3", and the call to 'map' with a 2nd argument of + # "group" replaces "dn1" with "peer" and "dn2" with "client". + maps: + groups: + - name: + value: + +############################################################################# +# Affiliations section. Fabric CA server can be bootstrapped with the +# affiliations specified in this section. Affiliations are specified as maps. +# For example: +# businessunit1: +# department1: +# - team1 +# businessunit2: +# - department2 +# - department3 +# +# Affiliations are hierarchical in nature. In the above example, +# department1 (used as businessunit1.department1) is the child of businessunit1. +# team1 (used as businessunit1.department1.team1) is the child of department1. +# department2 (used as businessunit2.department2) and department3 (businessunit2.department3) +# are children of businessunit2. +# Note: Affiliations are case sensitive except for the non-leaf affiliations +# (like businessunit1, department1, businessunit2) that are specified in the configuration file, +# which are always stored in lower case. +############################################################################# +affiliations: + org1: + - department1 + - department2 + org2: + - department1 + +############################################################################# +# Signing section +# +# The "default" subsection is used to sign enrollment certificates; +# the default expiration ("expiry" field) is "8760h", which is 1 year in hours. +# +# The "ca" profile subsection is used to sign intermediate CA certificates; +# the default expiration ("expiry" field) is "43800h" which is 5 years in hours. +# Note that "isca" is true, meaning that it issues a CA certificate. +# A maxpathlen of 0 means that the intermediate CA cannot issue other +# intermediate CA certificates, though it can still issue end entity certificates. +# (See RFC 5280, section 4.2.1.9) +# +# The "tls" profile subsection is used to sign TLS certificate requests; +# the default expiration ("expiry" field) is "8760h", which is 1 year in hours. +############################################################################# +signing: + default: + usage: + - digital signature + expiry: 8760h + profiles: + ca: + usage: + - cert sign + - crl sign + expiry: 43800h + caconstraint: + isca: true + maxpathlen: 0 + tls: + usage: + - signing + - key encipherment + - server auth + - client auth + - key agreement + expiry: 8760h + +########################################################################### +# Certificate Signing Request (CSR) section. +# This controls the creation of the root CA certificate. +# The expiration for the root CA certificate is configured with the +# "ca.expiry" field below, whose default value is "131400h" which is +# 15 years in hours. +# The pathlength field is used to limit CA certificate hierarchy as described +# in section 4.2.1.9 of RFC 5280. +# Examples: +# 1) No pathlength value means no limit is requested. +# 2) pathlength == 1 means a limit of 1 is requested which is the default for +# a root CA. This means the root CA can issue intermediate CA certificates, +# but these intermediate CAs may not in turn issue other CA certificates +# though they can still issue end entity certificates. +# 3) pathlength == 0 means a limit of 0 is requested; +# this is the default for an intermediate CA, which means it can not issue +# CA certificates though it can still issue end entity certificates. +########################################################################### +csr: + cn: fabric-ca-server + keyrequest: + algo: ecdsa + size: 256 + names: + - C: US + ST: "North Carolina" + L: + O: Hyperledger + OU: Fabric + hosts: + - localhost + - 127.0.0.1 + - org1-ecert-ca + - org1-ecert-ca.test-network.svc.cluster.local + ca: + expiry: 131400h + pathlength: 1 + +########################################################################### +# Each CA can issue both X509 enrollment certificate as well as Idemix +# Credential. This section specifies configuration for the issuer component +# that is responsible for issuing Idemix credentials. +########################################################################### +idemix: + # Specifies pool size for revocation handles. A revocation handle is an unique identifier of an + # Idemix credential. The issuer will create a pool revocation handles of this specified size. When + # a credential is requested, issuer will get handle from the pool and assign it to the credential. + # Issuer will repopulate the pool with new handles when the last handle in the pool is used. + # A revocation handle and credential revocation information (CRI) are used to create non revocation proof + # by the prover to prove to the verifier that her credential is not revoked. + rhpoolsize: 1000 + + # The Idemix credential issuance is a two step process. First step is to get a nonce from the issuer + # and second step is send credential request that is constructed using the nonce to the isuser to + # request a credential. This configuration property specifies expiration for the nonces. By default is + # nonces expire after 15 seconds. The value is expressed in the time.Duration format (see https://golang.org/pkg/time/#ParseDuration). + nonceexpiration: 15s + + # Specifies interval at which expired nonces are removed from datastore. Default value is 15 minutes. + # The value is expressed in the time.Duration format (see https://golang.org/pkg/time/#ParseDuration) + noncesweepinterval: 15m + +############################################################################# +# BCCSP (BlockChain Crypto Service Provider) section is used to select which +# crypto library implementation to use +############################################################################# +bccsp: + default: SW + sw: + hash: SHA2 + security: 256 + filekeystore: + # The directory used for the software file-based keystore + keystore: msp/keystore + +############################################################################# +# Multi CA section +# +# Each Fabric CA server contains one CA by default. This section is used +# to configure multiple CAs in a single server. +# +# 1) --cacount +# Automatically generate non-default CAs. The names of these +# additional CAs are "ca1", "ca2", ... "caN", where "N" is +# This is particularly useful in a development environment to quickly set up +# multiple CAs. Note that, this config option is not applicable to intermediate CA server +# i.e., Fabric CA server that is started with intermediate.parentserver.url config +# option (-u command line option) +# +# 2) --cafiles +# For each CA config file in the list, generate a separate signing CA. Each CA +# config file in this list MAY contain all of the same elements as are found in +# the server config file except port, debug, and tls sections. +# +# Examples: +# fabric-ca-server start -b admin:adminpw --cacount 2 +# +# fabric-ca-server start -b admin:adminpw --cafiles ca/ca1/fabric-ca-server-config.yaml +# --cafiles ca/ca2/fabric-ca-server-config.yaml +# +############################################################################# + +cacount: + +cafiles: + +############################################################################# +# Intermediate CA section +# +# The relationship between servers and CAs is as follows: +# 1) A single server process may contain or function as one or more CAs. +# This is configured by the "Multi CA section" above. +# 2) Each CA is either a root CA or an intermediate CA. +# 3) Each intermediate CA has a parent CA which is either a root CA or another intermediate CA. +# +# This section pertains to configuration of #2 and #3. +# If the "intermediate.parentserver.url" property is set, +# then this is an intermediate CA with the specified parent +# CA. +# +# parentserver section +# url - The URL of the parent server +# caname - Name of the CA to enroll within the server +# +# enrollment section used to enroll intermediate CA with parent CA +# profile - Name of the signing profile to use in issuing the certificate +# label - Label to use in HSM operations +# +# tls section for secure socket connection +# certfiles - PEM-encoded list of trusted root certificate files +# client: +# certfile - PEM-encoded certificate file for when client authentication +# is enabled on server +# keyfile - PEM-encoded key file for when client authentication +# is enabled on server +############################################################################# +intermediate: + parentserver: + url: + caname: + + enrollment: + hosts: + profile: + label: + + tls: + certfiles: + client: + certfile: + keyfile: + +############################################################################# +# CA configuration section +# +# Configure the number of incorrect password attempts are allowed for +# identities. By default, the value of 'passwordattempts' is 10, which +# means that 10 incorrect password attempts can be made before an identity get +# locked out. +############################################################################# +cfg: + identities: + passwordattempts: 10 + +############################################################################### +# +# Operations section +# +############################################################################### +operations: + # host and port for the operations server + listenAddress: 127.0.0.1:9443 + + # TLS configuration for the operations endpoint + tls: + # TLS enabled + enabled: false + + # path to PEM encoded server certificate for the operations server + cert: + file: + + # path to PEM encoded server key for the operations server + key: + file: + + # require client certificate authentication to access all resources + clientAuthRequired: false + + # paths to PEM encoded ca certificates to trust for client authentication + clientRootCAs: + files: [] + +############################################################################### +# +# Metrics section +# +############################################################################### +metrics: + # statsd, prometheus, or disabled + provider: disabled + + # statsd configuration + statsd: + # network type: tcp or udp + network: udp + + # statsd server address + address: 127.0.0.1:8125 + + # the interval at which locally cached counters and gauges are pushsed + # to statsd; timings are pushed immediately + writeInterval: 10s + + # prefix is prepended to all emitted statsd merics + prefix: server diff --git a/test-network-k8s/config/org1/fabric-tls-ca-server-config.yaml b/test-network-k8s/config/org1/fabric-tls-ca-server-config.yaml new file mode 100644 index 00000000..23860537 --- /dev/null +++ b/test-network-k8s/config/org1/fabric-tls-ca-server-config.yaml @@ -0,0 +1,496 @@ +############################################################################# +# This is a configuration file for the fabric-ca-server command. +# +# COMMAND LINE ARGUMENTS AND ENVIRONMENT VARIABLES +# ------------------------------------------------ +# Each configuration element can be overridden via command line +# arguments or environment variables. The precedence for determining +# the value of each element is as follows: +# 1) command line argument +# Examples: +# a) --port 443 +# To set the listening port +# b) --ca.keyfile ../mykey.pem +# To set the "keyfile" element in the "ca" section below; +# note the '.' separator character. +# 2) environment variable +# Examples: +# a) FABRIC_CA_SERVER_PORT=443 +# To set the listening port +# b) FABRIC_CA_SERVER_CA_KEYFILE="../mykey.pem" +# To set the "keyfile" element in the "ca" section below; +# note the '_' separator character. +# 3) configuration file +# 4) default value (if there is one) +# All default values are shown beside each element below. +# +# FILE NAME ELEMENTS +# ------------------ +# The value of all fields whose name ends with "file" or "files" are +# name or names of other files. +# For example, see "tls.certfile" and "tls.clientauth.certfiles". +# The value of each of these fields can be a simple filename, a +# relative path, or an absolute path. If the value is not an +# absolute path, it is interpretted as being relative to the location +# of this configuration file. +# +############################################################################# + +# Version of config file +version: 1.5.2 + +# Server's listening port (default: 7054) +port: 443 + +# Cross-Origin Resource Sharing (CORS) +cors: + enabled: false + origins: + - "*" + +# Enables debug logging (default: false) +debug: false + +# Size limit of an acceptable CRL in bytes (default: 512000) +crlsizelimit: 512000 + +############################################################################# +# TLS section for the server's listening port +# +# The following types are supported for client authentication: NoClientCert, +# RequestClientCert, RequireAnyClientCert, VerifyClientCertIfGiven, +# and RequireAndVerifyClientCert. +# +# Certfiles is a list of root certificate authorities that the server uses +# when verifying client certificates. +############################################################################# +tls: + # Enable TLS (default: false) + enabled: true + # TLS for the server's listening port + certfile: + keyfile: + clientauth: + type: noclientcert + certfiles: + +############################################################################# +# The CA section contains information related to the Certificate Authority +# including the name of the CA, which should be unique for all members +# of a blockchain network. It also includes the key and certificate files +# used when issuing enrollment certificates (ECerts) and transaction +# certificates (TCerts). +# The chainfile (if it exists) contains the certificate chain which +# should be trusted for this CA, where the 1st in the chain is always the +# root CA certificate. +############################################################################# +ca: + # Name of this CA + name: org1-tls-ca + # Key file (is only used to import a private key into BCCSP) + keyfile: + # Certificate file (default: ca-cert.pem) + certfile: + # Chain file + chainfile: + +############################################################################# +# The gencrl REST endpoint is used to generate a CRL that contains revoked +# certificates. This section contains configuration options that are used +# during gencrl request processing. +############################################################################# +crl: + # Specifies expiration for the generated CRL. The number of hours + # specified by this property is added to the UTC time, the resulting time + # is used to set the 'Next Update' date of the CRL. + expiry: 24h + +############################################################################# +# The registry section controls how the fabric-ca-server does two things: +# 1) authenticates enrollment requests which contain a username and password +# (also known as an enrollment ID and secret). +# 2) once authenticated, retrieves the identity's attribute names and +# values which the fabric-ca-server optionally puts into TCerts +# which it issues for transacting on the Hyperledger Fabric blockchain. +# These attributes are useful for making access control decisions in +# chaincode. +# There are two main configuration options: +# 1) The fabric-ca-server is the registry. +# This is true if "ldap.enabled" in the ldap section below is false. +# 2) An LDAP server is the registry, in which case the fabric-ca-server +# calls the LDAP server to perform these tasks. +# This is true if "ldap.enabled" in the ldap section below is true, +# which means this "registry" section is ignored. +############################################################################# +registry: + # Maximum number of times a password/secret can be reused for enrollment + # (default: -1, which means there is no limit) + maxenrollments: -1 + + # Contains identity information which is used when LDAP is disabled + identities: + - name: tlsadmin + pass: tlsadminpw + type: client + affiliation: "" + attrs: + hf.Registrar.Roles: "*" + hf.Registrar.DelegateRoles: "*" + hf.Revoker: true + hf.IntermediateCA: true + hf.GenCRL: true + hf.Registrar.Attributes: "*" + hf.AffiliationMgr: true + +############################################################################# +# Database section +# Supported types are: "sqlite3", "postgres", and "mysql". +# The datasource value depends on the type. +# If the type is "sqlite3", the datasource value is a file name to use +# as the database store. Since "sqlite3" is an embedded database, it +# may not be used if you want to run the fabric-ca-server in a cluster. +# To run the fabric-ca-server in a cluster, you must choose "postgres" +# or "mysql". +############################################################################# +db: + type: sqlite3 + datasource: fabric-ca-server.db + tls: + enabled: false + certfiles: + client: + certfile: + keyfile: + +############################################################################# +# LDAP section +# If LDAP is enabled, the fabric-ca-server calls LDAP to: +# 1) authenticate enrollment ID and secret (i.e. username and password) +# for enrollment requests; +# 2) To retrieve identity attributes +############################################################################# +ldap: + # Enables or disables the LDAP client (default: false) + # If this is set to true, the "registry" section is ignored. + enabled: false + # The URL of the LDAP server + url: ldap://:@:/ + # TLS configuration for the client connection to the LDAP server + tls: + certfiles: + client: + certfile: + keyfile: + # Attribute related configuration for mapping from LDAP entries to Fabric CA attributes + attribute: + # 'names' is an array of strings containing the LDAP attribute names which are + # requested from the LDAP server for an LDAP identity's entry + names: ['uid','member'] + # The 'converters' section is used to convert an LDAP entry to the value of + # a fabric CA attribute. + # For example, the following converts an LDAP 'uid' attribute + # whose value begins with 'revoker' to a fabric CA attribute + # named "hf.Revoker" with a value of "true" (because the boolean expression + # evaluates to true). + # converters: + # - name: hf.Revoker + # value: attr("uid") =~ "revoker*" + converters: + - name: + value: + # The 'maps' section contains named maps which may be referenced by the 'map' + # function in the 'converters' section to map LDAP responses to arbitrary values. + # For example, assume a user has an LDAP attribute named 'member' which has multiple + # values which are each a distinguished name (i.e. a DN). For simplicity, assume the + # values of the 'member' attribute are 'dn1', 'dn2', and 'dn3'. + # Further assume the following configuration. + # converters: + # - name: hf.Registrar.Roles + # value: map(attr("member"),"groups") + # maps: + # groups: + # - name: dn1 + # value: peer + # - name: dn2 + # value: client + # The value of the user's 'hf.Registrar.Roles' attribute is then computed to be + # "peer,client,dn3". This is because the value of 'attr("member")' is + # "dn1,dn2,dn3", and the call to 'map' with a 2nd argument of + # "group" replaces "dn1" with "peer" and "dn2" with "client". + maps: + groups: + - name: + value: + +############################################################################# +# Affiliations section. Fabric CA server can be bootstrapped with the +# affiliations specified in this section. Affiliations are specified as maps. +# For example: +# businessunit1: +# department1: +# - team1 +# businessunit2: +# - department2 +# - department3 +# +# Affiliations are hierarchical in nature. In the above example, +# department1 (used as businessunit1.department1) is the child of businessunit1. +# team1 (used as businessunit1.department1.team1) is the child of department1. +# department2 (used as businessunit2.department2) and department3 (businessunit2.department3) +# are children of businessunit2. +# Note: Affiliations are case sensitive except for the non-leaf affiliations +# (like businessunit1, department1, businessunit2) that are specified in the configuration file, +# which are always stored in lower case. +############################################################################# +affiliations: + org1: + - department1 + - department2 + org2: + - department1 + +############################################################################# +# Signing section +# +# The "default" subsection is used to sign enrollment certificates; +# the default expiration ("expiry" field) is "8760h", which is 1 year in hours. +# +# The "ca" profile subsection is used to sign intermediate CA certificates; +# the default expiration ("expiry" field) is "43800h" which is 5 years in hours. +# Note that "isca" is true, meaning that it issues a CA certificate. +# A maxpathlen of 0 means that the intermediate CA cannot issue other +# intermediate CA certificates, though it can still issue end entity certificates. +# (See RFC 5280, section 4.2.1.9) +# +# The "tls" profile subsection is used to sign TLS certificate requests; +# the default expiration ("expiry" field) is "8760h", which is 1 year in hours. +############################################################################# +signing: + default: + authremote: {} + caconstraint: {} + expiry: 8760h + usage: + - signing + - key encipherment + - server auth + - client auth + - key agreement + profiles: null + +########################################################################### +# Certificate Signing Request (CSR) section. +# This controls the creation of the root CA certificate. +# The expiration for the root CA certificate is configured with the +# "ca.expiry" field below, whose default value is "131400h" which is +# 15 years in hours. +# The pathlength field is used to limit CA certificate hierarchy as described +# in section 4.2.1.9 of RFC 5280. +# Examples: +# 1) No pathlength value means no limit is requested. +# 2) pathlength == 1 means a limit of 1 is requested which is the default for +# a root CA. This means the root CA can issue intermediate CA certificates, +# but these intermediate CAs may not in turn issue other CA certificates +# though they can still issue end entity certificates. +# 3) pathlength == 0 means a limit of 0 is requested; +# this is the default for an intermediate CA, which means it can not issue +# CA certificates though it can still issue end entity certificates. +########################################################################### +csr: + cn: fabric-ca-server + keyrequest: + algo: ecdsa + size: 256 + names: + - C: US + ST: "North Carolina" + L: + O: Hyperledger + OU: Fabric + hosts: + - localhost + - 127.0.0.1 + - org1-tls-ca + - org1-tls-ca.test-network.svc.cluster.local + ca: + expiry: 131400h + pathlength: 1 + +########################################################################### +# Each CA can issue both X509 enrollment certificate as well as Idemix +# Credential. This section specifies configuration for the issuer component +# that is responsible for issuing Idemix credentials. +########################################################################### +idemix: + # Specifies pool size for revocation handles. A revocation handle is an unique identifier of an + # Idemix credential. The issuer will create a pool revocation handles of this specified size. When + # a credential is requested, issuer will get handle from the pool and assign it to the credential. + # Issuer will repopulate the pool with new handles when the last handle in the pool is used. + # A revocation handle and credential revocation information (CRI) are used to create non revocation proof + # by the prover to prove to the verifier that her credential is not revoked. + rhpoolsize: 1000 + + # The Idemix credential issuance is a two step process. First step is to get a nonce from the issuer + # and second step is send credential request that is constructed using the nonce to the isuser to + # request a credential. This configuration property specifies expiration for the nonces. By default is + # nonces expire after 15 seconds. The value is expressed in the time.Duration format (see https://golang.org/pkg/time/#ParseDuration). + nonceexpiration: 15s + + # Specifies interval at which expired nonces are removed from datastore. Default value is 15 minutes. + # The value is expressed in the time.Duration format (see https://golang.org/pkg/time/#ParseDuration) + noncesweepinterval: 15m + +############################################################################# +# BCCSP (BlockChain Crypto Service Provider) section is used to select which +# crypto library implementation to use +############################################################################# +bccsp: + default: SW + sw: + hash: SHA2 + security: 256 + filekeystore: + # The directory used for the software file-based keystore + keystore: msp/keystore + +############################################################################# +# Multi CA section +# +# Each Fabric CA server contains one CA by default. This section is used +# to configure multiple CAs in a single server. +# +# 1) --cacount +# Automatically generate non-default CAs. The names of these +# additional CAs are "ca1", "ca2", ... "caN", where "N" is +# This is particularly useful in a development environment to quickly set up +# multiple CAs. Note that, this config option is not applicable to intermediate CA server +# i.e., Fabric CA server that is started with intermediate.parentserver.url config +# option (-u command line option) +# +# 2) --cafiles +# For each CA config file in the list, generate a separate signing CA. Each CA +# config file in this list MAY contain all of the same elements as are found in +# the server config file except port, debug, and tls sections. +# +# Examples: +# fabric-ca-server start -b admin:adminpw --cacount 2 +# +# fabric-ca-server start -b admin:adminpw --cafiles ca/ca1/fabric-ca-server-config.yaml +# --cafiles ca/ca2/fabric-ca-server-config.yaml +# +############################################################################# + +cacount: + +cafiles: + +############################################################################# +# Intermediate CA section +# +# The relationship between servers and CAs is as follows: +# 1) A single server process may contain or function as one or more CAs. +# This is configured by the "Multi CA section" above. +# 2) Each CA is either a root CA or an intermediate CA. +# 3) Each intermediate CA has a parent CA which is either a root CA or another intermediate CA. +# +# This section pertains to configuration of #2 and #3. +# If the "intermediate.parentserver.url" property is set, +# then this is an intermediate CA with the specified parent +# CA. +# +# parentserver section +# url - The URL of the parent server +# caname - Name of the CA to enroll within the server +# +# enrollment section used to enroll intermediate CA with parent CA +# profile - Name of the signing profile to use in issuing the certificate +# label - Label to use in HSM operations +# +# tls section for secure socket connection +# certfiles - PEM-encoded list of trusted root certificate files +# client: +# certfile - PEM-encoded certificate file for when client authentication +# is enabled on server +# keyfile - PEM-encoded key file for when client authentication +# is enabled on server +############################################################################# +intermediate: + parentserver: + url: + caname: + + enrollment: + hosts: + profile: + label: + + tls: + certfiles: + client: + certfile: + keyfile: + +############################################################################# +# CA configuration section +# +# Configure the number of incorrect password attempts are allowed for +# identities. By default, the value of 'passwordattempts' is 10, which +# means that 10 incorrect password attempts can be made before an identity get +# locked out. +############################################################################# +cfg: + identities: + passwordattempts: 10 + +############################################################################### +# +# Operations section +# +############################################################################### +operations: + # host and port for the operations server + listenAddress: 127.0.0.1:9444 + + # TLS configuration for the operations endpoint + tls: + # TLS enabled + enabled: false + + # path to PEM encoded server certificate for the operations server + cert: + file: + + # path to PEM encoded server key for the operations server + key: + file: + + # require client certificate authentication to access all resources + clientAuthRequired: false + + # paths to PEM encoded ca certificates to trust for client authentication + clientRootCAs: + files: [] + +############################################################################### +# +# Metrics section +# +############################################################################### +metrics: + # statsd, prometheus, or disabled + provider: disabled + + # statsd configuration + statsd: + # network type: tcp or udp + network: udp + + # statsd server address + address: 127.0.0.1:8125 + + # the interval at which locally cached counters and gauges are pushsed + # to statsd; timings are pushed immediately + writeInterval: 10s + + # prefix is prepended to all emitted statsd merics + prefix: server diff --git a/test-network-k8s/config/org2/core.yaml b/test-network-k8s/config/org2/core.yaml new file mode 100644 index 00000000..791af4c3 --- /dev/null +++ b/test-network-k8s/config/org2/core.yaml @@ -0,0 +1,759 @@ +# Copyright IBM Corp. All Rights Reserved. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################### +# +# Peer section +# +############################################################################### +peer: + + # The peer id provides a name for this peer instance and is used when + # naming docker resources. + id: jdoe + + # The networkId allows for logical separation of networks and is used when + # naming docker resources. + networkId: dev + + # The Address at local network interface this Peer will listen on. + # By default, it will listen on all network interfaces + listenAddress: 0.0.0.0:7051 + + # The endpoint this peer uses to listen for inbound chaincode connections. + # If this is commented-out, the listen address is selected to be + # the peer's address (see below) with port 7052 + # chaincodeListenAddress: 0.0.0.0:7052 + + # The endpoint the chaincode for this peer uses to connect to the peer. + # If this is not specified, the chaincodeListenAddress address is selected. + # And if chaincodeListenAddress is not specified, address is selected from + # peer address (see below). If specified peer address is invalid then it + # will fallback to the auto detected IP (local IP) regardless of the peer + # addressAutoDetect value. + # chaincodeAddress: 0.0.0.0:7052 + + # When used as peer config, this represents the endpoint to other peers + # in the same organization. For peers in other organization, see + # gossip.externalEndpoint for more info. + # When used as CLI config, this means the peer's endpoint to interact with + address: 0.0.0.0:7051 + + # Whether the Peer should programmatically determine its address + # This case is useful for docker containers. + # When set to true, will override peer address. + addressAutoDetect: false + + # Keepalive settings for peer server and clients + keepalive: + # Interval is the duration after which if the server does not see + # any activity from the client it pings the client to see if it's alive + interval: 7200s + # Timeout is the duration the server waits for a response + # from the client after sending a ping before closing the connection + timeout: 20s + # MinInterval is the minimum permitted time between client pings. + # If clients send pings more frequently, the peer server will + # disconnect them + minInterval: 60s + # Client keepalive settings for communicating with other peer nodes + client: + # Interval is the time between pings to peer nodes. This must + # greater than or equal to the minInterval specified by peer + # nodes + interval: 60s + # Timeout is the duration the client waits for a response from + # peer nodes before closing the connection + timeout: 20s + # DeliveryClient keepalive settings for communication with ordering + # nodes. + deliveryClient: + # Interval is the time between pings to ordering nodes. This must + # greater than or equal to the minInterval specified by ordering + # nodes. + interval: 60s + # Timeout is the duration the client waits for a response from + # ordering nodes before closing the connection + timeout: 20s + + + # Gossip related configuration + gossip: + # Bootstrap set to initialize gossip with. + # This is a list of other peers that this peer reaches out to at startup. + # Important: The endpoints here have to be endpoints of peers in the same + # organization, because the peer would refuse connecting to these endpoints + # unless they are in the same organization as the peer. + bootstrap: 127.0.0.1:7051 + + # NOTE: orgLeader and useLeaderElection parameters are mutual exclusive. + # Setting both to true would result in the termination of the peer + # since this is undefined state. If the peers are configured with + # useLeaderElection=false, make sure there is at least 1 peer in the + # organization that its orgLeader is set to true. + + # Defines whenever peer will initialize dynamic algorithm for + # "leader" selection, where leader is the peer to establish + # connection with ordering service and use delivery protocol + # to pull ledger blocks from ordering service. + useLeaderElection: false + # Statically defines peer to be an organization "leader", + # where this means that current peer will maintain connection + # with ordering service and disseminate block across peers in + # its own organization. Multiple peers or all peers in an organization + # may be configured as org leaders, so that they all pull + # blocks directly from ordering service. + orgLeader: true + + # Interval for membershipTracker polling + membershipTrackerInterval: 5s + + # Overrides the endpoint that the peer publishes to peers + # in its organization. For peers in foreign organizations + # see 'externalEndpoint' + endpoint: + # Maximum count of blocks stored in memory + maxBlockCountToStore: 10 + # Max time between consecutive message pushes(unit: millisecond) + maxPropagationBurstLatency: 10ms + # Max number of messages stored until a push is triggered to remote peers + maxPropagationBurstSize: 10 + # Number of times a message is pushed to remote peers + propagateIterations: 1 + # Number of peers selected to push messages to + propagatePeerNum: 3 + # Determines frequency of pull phases(unit: second) + # Must be greater than digestWaitTime + responseWaitTime + pullInterval: 4s + # Number of peers to pull from + pullPeerNum: 3 + # Determines frequency of pulling state info messages from peers(unit: second) + requestStateInfoInterval: 4s + # Determines frequency of pushing state info messages to peers(unit: second) + publishStateInfoInterval: 4s + # Maximum time a stateInfo message is kept until expired + stateInfoRetentionInterval: + # Time from startup certificates are included in Alive messages(unit: second) + publishCertPeriod: 10s + # Should we skip verifying block messages or not (currently not in use) + skipBlockVerification: false + # Dial timeout(unit: second) + dialTimeout: 3s + # Connection timeout(unit: second) + connTimeout: 2s + # Buffer size of received messages + recvBuffSize: 20 + # Buffer size of sending messages + sendBuffSize: 200 + # Time to wait before pull engine processes incoming digests (unit: second) + # Should be slightly smaller than requestWaitTime + digestWaitTime: 1s + # Time to wait before pull engine removes incoming nonce (unit: milliseconds) + # Should be slightly bigger than digestWaitTime + requestWaitTime: 1500ms + # Time to wait before pull engine ends pull (unit: second) + responseWaitTime: 2s + # Alive check interval(unit: second) + aliveTimeInterval: 5s + # Alive expiration timeout(unit: second) + aliveExpirationTimeout: 25s + # Reconnect interval(unit: second) + reconnectInterval: 25s + # Max number of attempts to connect to a peer + maxConnectionAttempts: 120 + # Message expiration factor for alive messages + msgExpirationFactor: 20 + # This is an endpoint that is published to peers outside of the organization. + # If this isn't set, the peer will not be known to other organizations. + externalEndpoint: + # Leader election service configuration + election: + # Longest time peer waits for stable membership during leader election startup (unit: second) + startupGracePeriod: 15s + # Interval gossip membership samples to check its stability (unit: second) + membershipSampleInterval: 1s + # Time passes since last declaration message before peer decides to perform leader election (unit: second) + leaderAliveThreshold: 10s + # Time between peer sends propose message and declares itself as a leader (sends declaration message) (unit: second) + leaderElectionDuration: 5s + + pvtData: + # pullRetryThreshold determines the maximum duration of time private data corresponding for a given block + # would be attempted to be pulled from peers until the block would be committed without the private data + pullRetryThreshold: 60s + # As private data enters the transient store, it is associated with the peer's ledger's height at that time. + # transientstoreMaxBlockRetention defines the maximum difference between the current ledger's height upon commit, + # and the private data residing inside the transient store that is guaranteed not to be purged. + # Private data is purged from the transient store when blocks with sequences that are multiples + # of transientstoreMaxBlockRetention are committed. + transientstoreMaxBlockRetention: 1000 + # pushAckTimeout is the maximum time to wait for an acknowledgement from each peer + # at private data push at endorsement time. + pushAckTimeout: 3s + # Block to live pulling margin, used as a buffer + # to prevent peer from trying to pull private data + # from peers that is soon to be purged in next N blocks. + # This helps a newly joined peer catch up to current + # blockchain height quicker. + btlPullMargin: 10 + # the process of reconciliation is done in an endless loop, while in each iteration reconciler tries to + # pull from the other peers the most recent missing blocks with a maximum batch size limitation. + # reconcileBatchSize determines the maximum batch size of missing private data that will be reconciled in a + # single iteration. + reconcileBatchSize: 10 + # reconcileSleepInterval determines the time reconciler sleeps from end of an iteration until the beginning + # of the next reconciliation iteration. + reconcileSleepInterval: 1m + # reconciliationEnabled is a flag that indicates whether private data reconciliation is enable or not. + reconciliationEnabled: true + # skipPullingInvalidTransactionsDuringCommit is a flag that indicates whether pulling of invalid + # transaction's private data from other peers need to be skipped during the commit time and pulled + # only through reconciler. + skipPullingInvalidTransactionsDuringCommit: false + # implicitCollectionDisseminationPolicy specifies the dissemination policy for the peer's own implicit collection. + # When a peer endorses a proposal that writes to its own implicit collection, below values override the default values + # for disseminating private data. + # Note that it is applicable to all channels the peer has joined. The implication is that requiredPeerCount has to + # be smaller than the number of peers in a channel that has the lowest numbers of peers from the organization. + implicitCollectionDisseminationPolicy: + # requiredPeerCount defines the minimum number of eligible peers to which the peer must successfully + # disseminate private data for its own implicit collection during endorsement. Default value is 0. + requiredPeerCount: 0 + # maxPeerCount defines the maximum number of eligible peers to which the peer will attempt to + # disseminate private data for its own implicit collection during endorsement. Default value is 1. + maxPeerCount: 1 + + # Gossip state transfer related configuration + state: + # indicates whenever state transfer is enabled or not + # default value is true, i.e. state transfer is active + # and takes care to sync up missing blocks allowing + # lagging peer to catch up to speed with rest network. + # Keep in mind that when peer.gossip.useLeaderElection is true + # and there are several peers in the organization, + # or peer.gossip.useLeaderElection is false alongside with + # peer.gossip.orgleader being false, the peer's ledger may lag behind + # the rest of the peers and will never catch up due to state transfer + # being disabled. + enabled: false + # checkInterval interval to check whether peer is lagging behind enough to + # request blocks via state transfer from another peer. + checkInterval: 10s + # responseTimeout amount of time to wait for state transfer response from + # other peers + responseTimeout: 3s + # batchSize the number of blocks to request via state transfer from another peer + batchSize: 10 + # blockBufferSize reflects the size of the re-ordering buffer + # which captures blocks and takes care to deliver them in order + # down to the ledger layer. The actual buffer size is bounded between + # 0 and 2*blockBufferSize, each channel maintains its own buffer + blockBufferSize: 20 + # maxRetries maximum number of re-tries to ask + # for single state transfer request + maxRetries: 3 + + # TLS Settings + tls: + # Require server-side TLS + enabled: true + # Require client certificates / mutual TLS for inbound connections. + # Note that clients that are not configured to use a certificate will + # fail to connect to the peer. + clientAuthRequired: false + # X.509 certificate used for TLS server + cert: + file: tls/server.crt + # Private key used for TLS server + key: + file: tls/server.key + # rootcert.file represents the trusted root certificate chain used for verifying certificates + # of other nodes during outbound connections. + # It is not required to be set, but can be used to augment the set of TLS CA certificates + # available from the MSPs of each channel’s configuration. + rootcert: + file: tls/ca.crt + # If mutual TLS is enabled, clientRootCAs.files contains a list of additional root certificates + # used for verifying certificates of client connections. + # It augments the set of TLS CA certificates available from the MSPs of each channel’s configuration. + # Minimally, set your organization's TLS CA root certificate so that the peer can receive join channel requests. + clientRootCAs: + files: + - tls/ca.crt + # Private key used for TLS when making client connections. + # If not set, peer.tls.key.file will be used instead + clientKey: + file: + # X.509 certificate used for TLS when making client connections. + # If not set, peer.tls.cert.file will be used instead + clientCert: + file: + + # Authentication contains configuration parameters related to authenticating + # client messages + authentication: + # the acceptable difference between the current server time and the + # client's time as specified in a client request message + timewindow: 15m + + # Path on the file system where peer will store data (eg ledger). This + # location must be access control protected to prevent unintended + # modification that might corrupt the peer operations. + fileSystemPath: /var/hyperledger/production + + # BCCSP (Blockchain crypto provider): Select which crypto implementation or + # library to use + BCCSP: + Default: SW + # Settings for the SW crypto provider (i.e. when DEFAULT: SW) + SW: + # TODO: The default Hash and Security level needs refactoring to be + # fully configurable. Changing these defaults requires coordination + # SHA2 is hardcoded in several places, not only BCCSP + Hash: SHA2 + Security: 256 + # Location of Key Store + FileKeyStore: + # If "", defaults to 'mspConfigPath'/keystore + KeyStore: + # Settings for the PKCS#11 crypto provider (i.e. when DEFAULT: PKCS11) + PKCS11: + # Location of the PKCS11 module library + Library: + # Token Label + Label: + # User PIN + Pin: + Hash: + Security: + + # Path on the file system where peer will find MSP local configurations + mspConfigPath: msp + + # Identifier of the local MSP + # ----!!!!IMPORTANT!!!-!!!IMPORTANT!!!-!!!IMPORTANT!!!!---- + # Deployers need to change the value of the localMspId string. + # In particular, the name of the local MSP ID of a peer needs + # to match the name of one of the MSPs in each of the channel + # that this peer is a member of. Otherwise this peer's messages + # will not be identified as valid by other nodes. + localMspId: Org2MSP + + # CLI common client config options + client: + # connection timeout + connTimeout: 3s + + # Delivery service related config + deliveryclient: + # It sets the total time the delivery service may spend in reconnection + # attempts until its retry logic gives up and returns an error + reconnectTotalTimeThreshold: 3600s + + # It sets the delivery service <-> ordering service node connection timeout + connTimeout: 3s + + # It sets the delivery service maximal delay between consecutive retries + reConnectBackoffThreshold: 3600s + + # A list of orderer endpoint addresses which should be overridden + # when found in channel configurations. + addressOverrides: + # - from: + # to: + # caCertsFile: + # - from: + # to: + # caCertsFile: + + # Type for the local MSP - by default it's of type bccsp + localMspType: bccsp + + # Used with Go profiling tools only in none production environment. In + # production, it should be disabled (eg enabled: false) + profile: + enabled: false + listenAddress: 0.0.0.0:6060 + + # Handlers defines custom handlers that can filter and mutate + # objects passing within the peer, such as: + # Auth filter - reject or forward proposals from clients + # Decorators - append or mutate the chaincode input passed to the chaincode + # Endorsers - Custom signing over proposal response payload and its mutation + # Valid handler definition contains: + # - A name which is a factory method name defined in + # core/handlers/library/library.go for statically compiled handlers + # - library path to shared object binary for pluggable filters + # Auth filters and decorators are chained and executed in the order that + # they are defined. For example: + # authFilters: + # - + # name: FilterOne + # library: /opt/lib/filter.so + # - + # name: FilterTwo + # decorators: + # - + # name: DecoratorOne + # - + # name: DecoratorTwo + # library: /opt/lib/decorator.so + # Endorsers are configured as a map that its keys are the endorsement system chaincodes that are being overridden. + # Below is an example that overrides the default ESCC and uses an endorsement plugin that has the same functionality + # as the default ESCC. + # If the 'library' property is missing, the name is used as the constructor method in the builtin library similar + # to auth filters and decorators. + # endorsers: + # escc: + # name: DefaultESCC + # library: /etc/hyperledger/fabric/plugin/escc.so + handlers: + authFilters: + - + name: DefaultAuth + - + name: ExpirationCheck # This filter checks identity x509 certificate expiration + decorators: + - + name: DefaultDecorator + endorsers: + escc: + name: DefaultEndorsement + library: + validators: + vscc: + name: DefaultValidation + library: + + # library: /etc/hyperledger/fabric/plugin/escc.so + # Number of goroutines that will execute transaction validation in parallel. + # By default, the peer chooses the number of CPUs on the machine. Set this + # variable to override that choice. + # NOTE: overriding this value might negatively influence the performance of + # the peer so please change this value only if you know what you're doing + validatorPoolSize: + + # The discovery service is used by clients to query information about peers, + # such as - which peers have joined a certain channel, what is the latest + # channel config, and most importantly - given a chaincode and a channel, + # what possible sets of peers satisfy the endorsement policy. + discovery: + enabled: true + # Whether the authentication cache is enabled or not. + authCacheEnabled: true + # The maximum size of the cache, after which a purge takes place + authCacheMaxSize: 1000 + # The proportion (0 to 1) of entries that remain in the cache after the cache is purged due to overpopulation + authCachePurgeRetentionRatio: 0.75 + # Whether to allow non-admins to perform non channel scoped queries. + # When this is false, it means that only peer admins can perform non channel scoped queries. + orgMembersAllowedAccess: false + + # Limits is used to configure some internal resource limits. + limits: + # Concurrency limits the number of concurrently running requests to a service on each peer. + # Currently this option is only applied to endorser service and deliver service. + # When the property is missing or the value is 0, the concurrency limit is disabled for the service. + concurrency: + # endorserService limits concurrent requests to endorser service that handles chaincode deployment, query and invocation, + # including both user chaincodes and system chaincodes. + endorserService: 2500 + # deliverService limits concurrent event listeners registered to deliver service for blocks and transaction events. + deliverService: 2500 + +############################################################################### +# +# VM section +# +############################################################################### +vm: + + # Endpoint of the vm management system. For docker can be one of the following in general + # unix:///var/run/docker.sock + # http://localhost:2375 + # https://localhost:2376 + endpoint: unix:///var/run/docker.sock + + # settings for docker vms + docker: + tls: + enabled: false + ca: + file: docker/ca.crt + cert: + file: docker/tls.crt + key: + file: docker/tls.key + + # Enables/disables the standard out/err from chaincode containers for + # debugging purposes + attachStdout: false + + # Parameters on creating docker container. + # Container may be efficiently created using ipam & dns-server for cluster + # NetworkMode - sets the networking mode for the container. Supported + # standard values are: `host`(default),`bridge`,`ipvlan`,`none`. + # Dns - a list of DNS servers for the container to use. + # Note: `Privileged` `Binds` `Links` and `PortBindings` properties of + # Docker Host Config are not supported and will not be used if set. + # LogConfig - sets the logging driver (Type) and related options + # (Config) for Docker. For more info, + # https://docs.docker.com/engine/admin/logging/overview/ + # Note: Set LogConfig using Environment Variables is not supported. + hostConfig: + NetworkMode: host + Dns: + # - 192.168.0.1 + LogConfig: + Type: json-file + Config: + max-size: "50m" + max-file: "5" + Memory: 2147483648 + +############################################################################### +# +# Chaincode section +# +############################################################################### +chaincode: + + # The id is used by the Chaincode stub to register the executing Chaincode + # ID with the Peer and is generally supplied through ENV variables + # the `path` form of ID is provided when installing the chaincode. + # The `name` is used for all other requests and can be any string. + id: + path: + name: + + # Generic builder environment, suitable for most chaincode types + builder: $(DOCKER_NS)/fabric-ccenv:$(TWO_DIGIT_VERSION) + + # Enables/disables force pulling of the base docker images (listed below) + # during user chaincode instantiation. + # Useful when using moving image tags (such as :latest) + pull: false + + golang: + # golang will never need more than baseos + runtime: $(DOCKER_NS)/fabric-baseos:$(TWO_DIGIT_VERSION) + + # whether or not golang chaincode should be linked dynamically + dynamicLink: false + + java: + # This is an image based on java:openjdk-8 with addition compiler + # tools added for java shim layer packaging. + # This image is packed with shim layer libraries that are necessary + # for Java chaincode runtime. + runtime: $(DOCKER_NS)/fabric-javaenv:$(TWO_DIGIT_VERSION) + + node: + # This is an image based on node:$(NODE_VER)-alpine + runtime: $(DOCKER_NS)/fabric-nodeenv:$(TWO_DIGIT_VERSION) + + # List of directories to treat as external builders and launchers for + # chaincode. The external builder detection processing will iterate over the + # builders in the order specified below. + externalBuilders: + - path: /var/hyperledger/fabric/chaincode/ccs-builder + name: ccs-builder + propagateEnvironment: + - HOME + - CORE_PEER_ID + - CORE_PEER_LOCALMSPID + + # The maximum duration to wait for the chaincode build and install process + # to complete. + installTimeout: 300s + + # Timeout duration for starting up a container and waiting for Register + # to come through. + startuptimeout: 300s + + # Timeout duration for Invoke and Init calls to prevent runaway. + # This timeout is used by all chaincodes in all the channels, including + # system chaincodes. + # Note that during Invoke, if the image is not available (e.g. being + # cleaned up when in development environment), the peer will automatically + # build the image, which might take more time. In production environment, + # the chaincode image is unlikely to be deleted, so the timeout could be + # reduced accordingly. + executetimeout: 30s + + # There are 2 modes: "dev" and "net". + # In dev mode, user runs the chaincode after starting peer from + # command line on local machine. + # In net mode, peer will run chaincode in a docker container. + mode: net + + # keepalive in seconds. In situations where the communication goes through a + # proxy that does not support keep-alive, this parameter will maintain connection + # between peer and chaincode. + # A value <= 0 turns keepalive off + keepalive: 0 + + # enabled system chaincodes + system: + _lifecycle: enable + cscc: enable + lscc: enable + qscc: enable + + # Logging section for the chaincode container + logging: + # Default level for all loggers within the chaincode container + level: info + # Override default level for the 'shim' logger + shim: warning + # Format for the chaincode container logs + format: '%{color}%{time:2006-01-02 15:04:05.000 MST} [%{module}] %{shortfunc} -> %{level:.4s} %{id:03x}%{color:reset} %{message}' + +############################################################################### +# +# Ledger section - ledger configuration encompasses both the blockchain +# and the state +# +############################################################################### +ledger: + + blockchain: + + state: + # stateDatabase - options are "goleveldb", "CouchDB" + # goleveldb - default state database stored in goleveldb. + # CouchDB - store state database in CouchDB + stateDatabase: goleveldb + # Limit on the number of records to return per query + totalQueryLimit: 100000 + couchDBConfig: + # It is recommended to run CouchDB on the same server as the peer, and + # not map the CouchDB container port to a server port in docker-compose. + # Otherwise proper security must be provided on the connection between + # CouchDB client (on the peer) and server. + couchDBAddress: 127.0.0.1:5984 + # This username must have read and write authority on CouchDB + username: + # The password is recommended to pass as an environment variable + # during start up (eg CORE_LEDGER_STATE_COUCHDBCONFIG_PASSWORD). + # If it is stored here, the file must be access control protected + # to prevent unintended users from discovering the password. + password: + # Number of retries for CouchDB errors + maxRetries: 3 + # Number of retries for CouchDB errors during peer startup. + # The delay between retries doubles for each attempt. + # Default of 10 retries results in 11 attempts over 2 minutes. + maxRetriesOnStartup: 10 + # CouchDB request timeout (unit: duration, e.g. 20s) + requestTimeout: 35s + # Limit on the number of records per each CouchDB query + # Note that chaincode queries are only bound by totalQueryLimit. + # Internally the chaincode may execute multiple CouchDB queries, + # each of size internalQueryLimit. + internalQueryLimit: 1000 + # Limit on the number of records per CouchDB bulk update batch + maxBatchUpdateSize: 1000 + # Warm indexes after every N blocks. + # This option warms any indexes that have been + # deployed to CouchDB after every N blocks. + # A value of 1 will warm indexes after every block commit, + # to ensure fast selector queries. + # Increasing the value may improve write efficiency of peer and CouchDB, + # but may degrade query response time. + warmIndexesAfterNBlocks: 1 + # Create the _global_changes system database + # This is optional. Creating the global changes database will require + # additional system resources to track changes and maintain the database + createGlobalChangesDB: false + # CacheSize denotes the maximum mega bytes (MB) to be allocated for the in-memory state + # cache. Note that CacheSize needs to be a multiple of 32 MB. If it is not a multiple + # of 32 MB, the peer would round the size to the next multiple of 32 MB. + # To disable the cache, 0 MB needs to be assigned to the cacheSize. + cacheSize: 64 + + history: + # enableHistoryDatabase - options are true or false + # Indicates if the history of key updates should be stored. + # All history 'index' will be stored in goleveldb, regardless if using + # CouchDB or alternate database for the state. + enableHistoryDatabase: true + + pvtdataStore: + # the maximum db batch size for converting + # the ineligible missing data entries to eligible missing data entries + collElgProcMaxDbBatchSize: 5000 + # the minimum duration (in milliseconds) between writing + # two consecutive db batches for converting the ineligible missing data entries to eligible missing data entries + collElgProcDbBatchesInterval: 1000 + # The missing data entries are classified into two categories: + # (1) prioritized + # (2) deprioritized + # Initially, all missing data are in the prioritized list. When the + # reconciler is unable to fetch the missing data from other peers, + # the unreconciled missing data would be moved to the deprioritized list. + # The reconciler would retry deprioritized missing data after every + # deprioritizedDataReconcilerInterval (unit: minutes). Note that the + # interval needs to be greater than the reconcileSleepInterval + deprioritizedDataReconcilerInterval: 60m + + snapshots: + # Path on the file system where peer will store ledger snapshots + rootDir: /var/hyperledger/production/snapshots + +############################################################################### +# +# Operations section +# +############################################################################### +operations: + # host and port for the operations server + listenAddress: 127.0.0.1:9443 + + # TLS configuration for the operations endpoint + tls: + # TLS enabled + enabled: false + + # path to PEM encoded server certificate for the operations server + cert: + file: + + # path to PEM encoded server key for the operations server + key: + file: + + # most operations service endpoints require client authentication when TLS + # is enabled. clientAuthRequired requires client certificate authentication + # at the TLS layer to access all resources. + clientAuthRequired: false + + # paths to PEM encoded ca certificates to trust for client authentication + clientRootCAs: + files: [] + +############################################################################### +# +# Metrics section +# +############################################################################### +metrics: + # metrics provider is one of statsd, prometheus, or disabled + provider: disabled + + # statsd configuration + statsd: + # network type: tcp or udp + network: udp + + # statsd server address + address: 127.0.0.1:8125 + + # the interval at which locally cached counters and gauges are pushed + # to statsd; timings are pushed immediately + writeInterval: 10s + + # prefix is prepended to all emitted statsd metrics + prefix: diff --git a/test-network-k8s/config/org2/fabric-ecert-ca-server-config.yaml b/test-network-k8s/config/org2/fabric-ecert-ca-server-config.yaml new file mode 100644 index 00000000..23732ff8 --- /dev/null +++ b/test-network-k8s/config/org2/fabric-ecert-ca-server-config.yaml @@ -0,0 +1,506 @@ +############################################################################# +# This is a configuration file for the fabric-ca-server command. +# +# COMMAND LINE ARGUMENTS AND ENVIRONMENT VARIABLES +# ------------------------------------------------ +# Each configuration element can be overridden via command line +# arguments or environment variables. The precedence for determining +# the value of each element is as follows: +# 1) command line argument +# Examples: +# a) --port 443 +# To set the listening port +# b) --ca.keyfile ../mykey.pem +# To set the "keyfile" element in the "ca" section below; +# note the '.' separator character. +# 2) environment variable +# Examples: +# a) FABRIC_CA_SERVER_PORT=443 +# To set the listening port +# b) FABRIC_CA_SERVER_CA_KEYFILE="../mykey.pem" +# To set the "keyfile" element in the "ca" section below; +# note the '_' separator character. +# 3) configuration file +# 4) default value (if there is one) +# All default values are shown beside each element below. +# +# FILE NAME ELEMENTS +# ------------------ +# The value of all fields whose name ends with "file" or "files" are +# name or names of other files. +# For example, see "tls.certfile" and "tls.clientauth.certfiles". +# The value of each of these fields can be a simple filename, a +# relative path, or an absolute path. If the value is not an +# absolute path, it is interpretted as being relative to the location +# of this configuration file. +# +############################################################################# + +# Version of config file +version: 1.5.2 + +# Server's listening port (default: 7054) +port: 443 + +# Cross-Origin Resource Sharing (CORS) +cors: + enabled: false + origins: + - "*" + +# Enables debug logging (default: false) +debug: false + +# Size limit of an acceptable CRL in bytes (default: 512000) +crlsizelimit: 512000 + +############################################################################# +# TLS section for the server's listening port +# +# The following types are supported for client authentication: NoClientCert, +# RequestClientCert, RequireAnyClientCert, VerifyClientCertIfGiven, +# and RequireAndVerifyClientCert. +# +# Certfiles is a list of root certificate authorities that the server uses +# when verifying client certificates. +############################################################################# +tls: + # Enable TLS (default: false) + enabled: true + # TLS for the server's listening port + certfile: + keyfile: + clientauth: + type: noclientcert + certfiles: + +############################################################################# +# The CA section contains information related to the Certificate Authority +# including the name of the CA, which should be unique for all members +# of a blockchain network. It also includes the key and certificate files +# used when issuing enrollment certificates (ECerts) and transaction +# certificates (TCerts). +# The chainfile (if it exists) contains the certificate chain which +# should be trusted for this CA, where the 1st in the chain is always the +# root CA certificate. +############################################################################# +ca: + # Name of this CA + name: org2-ecert-ca + # Key file (is only used to import a private key into BCCSP) + keyfile: + # Certificate file (default: ca-cert.pem) + certfile: + # Chain file + chainfile: + +############################################################################# +# The gencrl REST endpoint is used to generate a CRL that contains revoked +# certificates. This section contains configuration options that are used +# during gencrl request processing. +############################################################################# +crl: + # Specifies expiration for the generated CRL. The number of hours + # specified by this property is added to the UTC time, the resulting time + # is used to set the 'Next Update' date of the CRL. + expiry: 24h + +############################################################################# +# The registry section controls how the fabric-ca-server does two things: +# 1) authenticates enrollment requests which contain a username and password +# (also known as an enrollment ID and secret). +# 2) once authenticated, retrieves the identity's attribute names and +# values which the fabric-ca-server optionally puts into TCerts +# which it issues for transacting on the Hyperledger Fabric blockchain. +# These attributes are useful for making access control decisions in +# chaincode. +# There are two main configuration options: +# 1) The fabric-ca-server is the registry. +# This is true if "ldap.enabled" in the ldap section below is false. +# 2) An LDAP server is the registry, in which case the fabric-ca-server +# calls the LDAP server to perform these tasks. +# This is true if "ldap.enabled" in the ldap section below is true, +# which means this "registry" section is ignored. +############################################################################# +registry: + # Maximum number of times a password/secret can be reused for enrollment + # (default: -1, which means there is no limit) + maxenrollments: -1 + + # Contains identity information which is used when LDAP is disabled + identities: + - name: rcaadmin + pass: rcaadminpw + type: client + affiliation: "" + attrs: + hf.Registrar.Roles: "*" + hf.Registrar.DelegateRoles: "*" + hf.Revoker: true + hf.IntermediateCA: true + hf.GenCRL: true + hf.Registrar.Attributes: "*" + hf.AffiliationMgr: true + +############################################################################# +# Database section +# Supported types are: "sqlite3", "postgres", and "mysql". +# The datasource value depends on the type. +# If the type is "sqlite3", the datasource value is a file name to use +# as the database store. Since "sqlite3" is an embedded database, it +# may not be used if you want to run the fabric-ca-server in a cluster. +# To run the fabric-ca-server in a cluster, you must choose "postgres" +# or "mysql". +############################################################################# +db: + type: sqlite3 + datasource: fabric-ca-server.db + tls: + enabled: false + certfiles: + client: + certfile: + keyfile: + +############################################################################# +# LDAP section +# If LDAP is enabled, the fabric-ca-server calls LDAP to: +# 1) authenticate enrollment ID and secret (i.e. username and password) +# for enrollment requests; +# 2) To retrieve identity attributes +############################################################################# +ldap: + # Enables or disables the LDAP client (default: false) + # If this is set to true, the "registry" section is ignored. + enabled: false + # The URL of the LDAP server + url: ldap://:@:/ + # TLS configuration for the client connection to the LDAP server + tls: + certfiles: + client: + certfile: + keyfile: + # Attribute related configuration for mapping from LDAP entries to Fabric CA attributes + attribute: + # 'names' is an array of strings containing the LDAP attribute names which are + # requested from the LDAP server for an LDAP identity's entry + names: ['uid','member'] + # The 'converters' section is used to convert an LDAP entry to the value of + # a fabric CA attribute. + # For example, the following converts an LDAP 'uid' attribute + # whose value begins with 'revoker' to a fabric CA attribute + # named "hf.Revoker" with a value of "true" (because the boolean expression + # evaluates to true). + # converters: + # - name: hf.Revoker + # value: attr("uid") =~ "revoker*" + converters: + - name: + value: + # The 'maps' section contains named maps which may be referenced by the 'map' + # function in the 'converters' section to map LDAP responses to arbitrary values. + # For example, assume a user has an LDAP attribute named 'member' which has multiple + # values which are each a distinguished name (i.e. a DN). For simplicity, assume the + # values of the 'member' attribute are 'dn1', 'dn2', and 'dn3'. + # Further assume the following configuration. + # converters: + # - name: hf.Registrar.Roles + # value: map(attr("member"),"groups") + # maps: + # groups: + # - name: dn1 + # value: peer + # - name: dn2 + # value: client + # The value of the user's 'hf.Registrar.Roles' attribute is then computed to be + # "peer,client,dn3". This is because the value of 'attr("member")' is + # "dn1,dn2,dn3", and the call to 'map' with a 2nd argument of + # "group" replaces "dn1" with "peer" and "dn2" with "client". + maps: + groups: + - name: + value: + +############################################################################# +# Affiliations section. Fabric CA server can be bootstrapped with the +# affiliations specified in this section. Affiliations are specified as maps. +# For example: +# businessunit1: +# department1: +# - team1 +# businessunit2: +# - department2 +# - department3 +# +# Affiliations are hierarchical in nature. In the above example, +# department1 (used as businessunit1.department1) is the child of businessunit1. +# team1 (used as businessunit1.department1.team1) is the child of department1. +# department2 (used as businessunit2.department2) and department3 (businessunit2.department3) +# are children of businessunit2. +# Note: Affiliations are case sensitive except for the non-leaf affiliations +# (like businessunit1, department1, businessunit2) that are specified in the configuration file, +# which are always stored in lower case. +############################################################################# +affiliations: + org1: + - department1 + - department2 + org2: + - department1 + +############################################################################# +# Signing section +# +# The "default" subsection is used to sign enrollment certificates; +# the default expiration ("expiry" field) is "8760h", which is 1 year in hours. +# +# The "ca" profile subsection is used to sign intermediate CA certificates; +# the default expiration ("expiry" field) is "43800h" which is 5 years in hours. +# Note that "isca" is true, meaning that it issues a CA certificate. +# A maxpathlen of 0 means that the intermediate CA cannot issue other +# intermediate CA certificates, though it can still issue end entity certificates. +# (See RFC 5280, section 4.2.1.9) +# +# The "tls" profile subsection is used to sign TLS certificate requests; +# the default expiration ("expiry" field) is "8760h", which is 1 year in hours. +############################################################################# +signing: + default: + usage: + - digital signature + expiry: 8760h + profiles: + ca: + usage: + - cert sign + - crl sign + expiry: 43800h + caconstraint: + isca: true + maxpathlen: 0 + tls: + usage: + - signing + - key encipherment + - server auth + - client auth + - key agreement + expiry: 8760h + +########################################################################### +# Certificate Signing Request (CSR) section. +# This controls the creation of the root CA certificate. +# The expiration for the root CA certificate is configured with the +# "ca.expiry" field below, whose default value is "131400h" which is +# 15 years in hours. +# The pathlength field is used to limit CA certificate hierarchy as described +# in section 4.2.1.9 of RFC 5280. +# Examples: +# 1) No pathlength value means no limit is requested. +# 2) pathlength == 1 means a limit of 1 is requested which is the default for +# a root CA. This means the root CA can issue intermediate CA certificates, +# but these intermediate CAs may not in turn issue other CA certificates +# though they can still issue end entity certificates. +# 3) pathlength == 0 means a limit of 0 is requested; +# this is the default for an intermediate CA, which means it can not issue +# CA certificates though it can still issue end entity certificates. +########################################################################### +csr: + cn: fabric-ca-server + keyrequest: + algo: ecdsa + size: 256 + names: + - C: US + ST: "North Carolina" + L: + O: Hyperledger + OU: Fabric + hosts: + - localhost + - 127.0.0.1 + - org2-ecert-ca + - org2-ecert-ca.test-network.svc.cluster.local + ca: + expiry: 131400h + pathlength: 1 + +########################################################################### +# Each CA can issue both X509 enrollment certificate as well as Idemix +# Credential. This section specifies configuration for the issuer component +# that is responsible for issuing Idemix credentials. +########################################################################### +idemix: + # Specifies pool size for revocation handles. A revocation handle is an unique identifier of an + # Idemix credential. The issuer will create a pool revocation handles of this specified size. When + # a credential is requested, issuer will get handle from the pool and assign it to the credential. + # Issuer will repopulate the pool with new handles when the last handle in the pool is used. + # A revocation handle and credential revocation information (CRI) are used to create non revocation proof + # by the prover to prove to the verifier that her credential is not revoked. + rhpoolsize: 1000 + + # The Idemix credential issuance is a two step process. First step is to get a nonce from the issuer + # and second step is send credential request that is constructed using the nonce to the isuser to + # request a credential. This configuration property specifies expiration for the nonces. By default is + # nonces expire after 15 seconds. The value is expressed in the time.Duration format (see https://golang.org/pkg/time/#ParseDuration). + nonceexpiration: 15s + + # Specifies interval at which expired nonces are removed from datastore. Default value is 15 minutes. + # The value is expressed in the time.Duration format (see https://golang.org/pkg/time/#ParseDuration) + noncesweepinterval: 15m + +############################################################################# +# BCCSP (BlockChain Crypto Service Provider) section is used to select which +# crypto library implementation to use +############################################################################# +bccsp: + default: SW + sw: + hash: SHA2 + security: 256 + filekeystore: + # The directory used for the software file-based keystore + keystore: msp/keystore + +############################################################################# +# Multi CA section +# +# Each Fabric CA server contains one CA by default. This section is used +# to configure multiple CAs in a single server. +# +# 1) --cacount +# Automatically generate non-default CAs. The names of these +# additional CAs are "ca1", "ca2", ... "caN", where "N" is +# This is particularly useful in a development environment to quickly set up +# multiple CAs. Note that, this config option is not applicable to intermediate CA server +# i.e., Fabric CA server that is started with intermediate.parentserver.url config +# option (-u command line option) +# +# 2) --cafiles +# For each CA config file in the list, generate a separate signing CA. Each CA +# config file in this list MAY contain all of the same elements as are found in +# the server config file except port, debug, and tls sections. +# +# Examples: +# fabric-ca-server start -b admin:adminpw --cacount 2 +# +# fabric-ca-server start -b admin:adminpw --cafiles ca/ca1/fabric-ca-server-config.yaml +# --cafiles ca/ca2/fabric-ca-server-config.yaml +# +############################################################################# + +cacount: + +cafiles: + +############################################################################# +# Intermediate CA section +# +# The relationship between servers and CAs is as follows: +# 1) A single server process may contain or function as one or more CAs. +# This is configured by the "Multi CA section" above. +# 2) Each CA is either a root CA or an intermediate CA. +# 3) Each intermediate CA has a parent CA which is either a root CA or another intermediate CA. +# +# This section pertains to configuration of #2 and #3. +# If the "intermediate.parentserver.url" property is set, +# then this is an intermediate CA with the specified parent +# CA. +# +# parentserver section +# url - The URL of the parent server +# caname - Name of the CA to enroll within the server +# +# enrollment section used to enroll intermediate CA with parent CA +# profile - Name of the signing profile to use in issuing the certificate +# label - Label to use in HSM operations +# +# tls section for secure socket connection +# certfiles - PEM-encoded list of trusted root certificate files +# client: +# certfile - PEM-encoded certificate file for when client authentication +# is enabled on server +# keyfile - PEM-encoded key file for when client authentication +# is enabled on server +############################################################################# +intermediate: + parentserver: + url: + caname: + + enrollment: + hosts: + profile: + label: + + tls: + certfiles: + client: + certfile: + keyfile: + +############################################################################# +# CA configuration section +# +# Configure the number of incorrect password attempts are allowed for +# identities. By default, the value of 'passwordattempts' is 10, which +# means that 10 incorrect password attempts can be made before an identity get +# locked out. +############################################################################# +cfg: + identities: + passwordattempts: 10 + +############################################################################### +# +# Operations section +# +############################################################################### +operations: + # host and port for the operations server + listenAddress: 127.0.0.1:9443 + + # TLS configuration for the operations endpoint + tls: + # TLS enabled + enabled: false + + # path to PEM encoded server certificate for the operations server + cert: + file: + + # path to PEM encoded server key for the operations server + key: + file: + + # require client certificate authentication to access all resources + clientAuthRequired: false + + # paths to PEM encoded ca certificates to trust for client authentication + clientRootCAs: + files: [] + +############################################################################### +# +# Metrics section +# +############################################################################### +metrics: + # statsd, prometheus, or disabled + provider: disabled + + # statsd configuration + statsd: + # network type: tcp or udp + network: udp + + # statsd server address + address: 127.0.0.1:8125 + + # the interval at which locally cached counters and gauges are pushsed + # to statsd; timings are pushed immediately + writeInterval: 10s + + # prefix is prepended to all emitted statsd merics + prefix: server diff --git a/test-network-k8s/config/org2/fabric-tls-ca-server-config.yaml b/test-network-k8s/config/org2/fabric-tls-ca-server-config.yaml new file mode 100644 index 00000000..74879302 --- /dev/null +++ b/test-network-k8s/config/org2/fabric-tls-ca-server-config.yaml @@ -0,0 +1,496 @@ +############################################################################# +# This is a configuration file for the fabric-ca-server command. +# +# COMMAND LINE ARGUMENTS AND ENVIRONMENT VARIABLES +# ------------------------------------------------ +# Each configuration element can be overridden via command line +# arguments or environment variables. The precedence for determining +# the value of each element is as follows: +# 1) command line argument +# Examples: +# a) --port 443 +# To set the listening port +# b) --ca.keyfile ../mykey.pem +# To set the "keyfile" element in the "ca" section below; +# note the '.' separator character. +# 2) environment variable +# Examples: +# a) FABRIC_CA_SERVER_PORT=443 +# To set the listening port +# b) FABRIC_CA_SERVER_CA_KEYFILE="../mykey.pem" +# To set the "keyfile" element in the "ca" section below; +# note the '_' separator character. +# 3) configuration file +# 4) default value (if there is one) +# All default values are shown beside each element below. +# +# FILE NAME ELEMENTS +# ------------------ +# The value of all fields whose name ends with "file" or "files" are +# name or names of other files. +# For example, see "tls.certfile" and "tls.clientauth.certfiles". +# The value of each of these fields can be a simple filename, a +# relative path, or an absolute path. If the value is not an +# absolute path, it is interpretted as being relative to the location +# of this configuration file. +# +############################################################################# + +# Version of config file +version: 1.5.2 + +# Server's listening port (default: 7054) +port: 443 + +# Cross-Origin Resource Sharing (CORS) +cors: + enabled: false + origins: + - "*" + +# Enables debug logging (default: false) +debug: false + +# Size limit of an acceptable CRL in bytes (default: 512000) +crlsizelimit: 512000 + +############################################################################# +# TLS section for the server's listening port +# +# The following types are supported for client authentication: NoClientCert, +# RequestClientCert, RequireAnyClientCert, VerifyClientCertIfGiven, +# and RequireAndVerifyClientCert. +# +# Certfiles is a list of root certificate authorities that the server uses +# when verifying client certificates. +############################################################################# +tls: + # Enable TLS (default: false) + enabled: true + # TLS for the server's listening port + certfile: + keyfile: + clientauth: + type: noclientcert + certfiles: + +############################################################################# +# The CA section contains information related to the Certificate Authority +# including the name of the CA, which should be unique for all members +# of a blockchain network. It also includes the key and certificate files +# used when issuing enrollment certificates (ECerts) and transaction +# certificates (TCerts). +# The chainfile (if it exists) contains the certificate chain which +# should be trusted for this CA, where the 1st in the chain is always the +# root CA certificate. +############################################################################# +ca: + # Name of this CA + name: org2-tls-ca + # Key file (is only used to import a private key into BCCSP) + keyfile: + # Certificate file (default: ca-cert.pem) + certfile: + # Chain file + chainfile: + +############################################################################# +# The gencrl REST endpoint is used to generate a CRL that contains revoked +# certificates. This section contains configuration options that are used +# during gencrl request processing. +############################################################################# +crl: + # Specifies expiration for the generated CRL. The number of hours + # specified by this property is added to the UTC time, the resulting time + # is used to set the 'Next Update' date of the CRL. + expiry: 24h + +############################################################################# +# The registry section controls how the fabric-ca-server does two things: +# 1) authenticates enrollment requests which contain a username and password +# (also known as an enrollment ID and secret). +# 2) once authenticated, retrieves the identity's attribute names and +# values which the fabric-ca-server optionally puts into TCerts +# which it issues for transacting on the Hyperledger Fabric blockchain. +# These attributes are useful for making access control decisions in +# chaincode. +# There are two main configuration options: +# 1) The fabric-ca-server is the registry. +# This is true if "ldap.enabled" in the ldap section below is false. +# 2) An LDAP server is the registry, in which case the fabric-ca-server +# calls the LDAP server to perform these tasks. +# This is true if "ldap.enabled" in the ldap section below is true, +# which means this "registry" section is ignored. +############################################################################# +registry: + # Maximum number of times a password/secret can be reused for enrollment + # (default: -1, which means there is no limit) + maxenrollments: -1 + + # Contains identity information which is used when LDAP is disabled + identities: + - name: tlsadmin + pass: tlsadminpw + type: client + affiliation: "" + attrs: + hf.Registrar.Roles: "*" + hf.Registrar.DelegateRoles: "*" + hf.Revoker: true + hf.IntermediateCA: true + hf.GenCRL: true + hf.Registrar.Attributes: "*" + hf.AffiliationMgr: true + +############################################################################# +# Database section +# Supported types are: "sqlite3", "postgres", and "mysql". +# The datasource value depends on the type. +# If the type is "sqlite3", the datasource value is a file name to use +# as the database store. Since "sqlite3" is an embedded database, it +# may not be used if you want to run the fabric-ca-server in a cluster. +# To run the fabric-ca-server in a cluster, you must choose "postgres" +# or "mysql". +############################################################################# +db: + type: sqlite3 + datasource: fabric-ca-server.db + tls: + enabled: false + certfiles: + client: + certfile: + keyfile: + +############################################################################# +# LDAP section +# If LDAP is enabled, the fabric-ca-server calls LDAP to: +# 1) authenticate enrollment ID and secret (i.e. username and password) +# for enrollment requests; +# 2) To retrieve identity attributes +############################################################################# +ldap: + # Enables or disables the LDAP client (default: false) + # If this is set to true, the "registry" section is ignored. + enabled: false + # The URL of the LDAP server + url: ldap://:@:/ + # TLS configuration for the client connection to the LDAP server + tls: + certfiles: + client: + certfile: + keyfile: + # Attribute related configuration for mapping from LDAP entries to Fabric CA attributes + attribute: + # 'names' is an array of strings containing the LDAP attribute names which are + # requested from the LDAP server for an LDAP identity's entry + names: ['uid','member'] + # The 'converters' section is used to convert an LDAP entry to the value of + # a fabric CA attribute. + # For example, the following converts an LDAP 'uid' attribute + # whose value begins with 'revoker' to a fabric CA attribute + # named "hf.Revoker" with a value of "true" (because the boolean expression + # evaluates to true). + # converters: + # - name: hf.Revoker + # value: attr("uid") =~ "revoker*" + converters: + - name: + value: + # The 'maps' section contains named maps which may be referenced by the 'map' + # function in the 'converters' section to map LDAP responses to arbitrary values. + # For example, assume a user has an LDAP attribute named 'member' which has multiple + # values which are each a distinguished name (i.e. a DN). For simplicity, assume the + # values of the 'member' attribute are 'dn1', 'dn2', and 'dn3'. + # Further assume the following configuration. + # converters: + # - name: hf.Registrar.Roles + # value: map(attr("member"),"groups") + # maps: + # groups: + # - name: dn1 + # value: peer + # - name: dn2 + # value: client + # The value of the user's 'hf.Registrar.Roles' attribute is then computed to be + # "peer,client,dn3". This is because the value of 'attr("member")' is + # "dn1,dn2,dn3", and the call to 'map' with a 2nd argument of + # "group" replaces "dn1" with "peer" and "dn2" with "client". + maps: + groups: + - name: + value: + +############################################################################# +# Affiliations section. Fabric CA server can be bootstrapped with the +# affiliations specified in this section. Affiliations are specified as maps. +# For example: +# businessunit1: +# department1: +# - team1 +# businessunit2: +# - department2 +# - department3 +# +# Affiliations are hierarchical in nature. In the above example, +# department1 (used as businessunit1.department1) is the child of businessunit1. +# team1 (used as businessunit1.department1.team1) is the child of department1. +# department2 (used as businessunit2.department2) and department3 (businessunit2.department3) +# are children of businessunit2. +# Note: Affiliations are case sensitive except for the non-leaf affiliations +# (like businessunit1, department1, businessunit2) that are specified in the configuration file, +# which are always stored in lower case. +############################################################################# +affiliations: + org1: + - department1 + - department2 + org2: + - department1 + +############################################################################# +# Signing section +# +# The "default" subsection is used to sign enrollment certificates; +# the default expiration ("expiry" field) is "8760h", which is 1 year in hours. +# +# The "ca" profile subsection is used to sign intermediate CA certificates; +# the default expiration ("expiry" field) is "43800h" which is 5 years in hours. +# Note that "isca" is true, meaning that it issues a CA certificate. +# A maxpathlen of 0 means that the intermediate CA cannot issue other +# intermediate CA certificates, though it can still issue end entity certificates. +# (See RFC 5280, section 4.2.1.9) +# +# The "tls" profile subsection is used to sign TLS certificate requests; +# the default expiration ("expiry" field) is "8760h", which is 1 year in hours. +############################################################################# +signing: + default: + authremote: {} + caconstraint: {} + expiry: 8760h + usage: + - signing + - key encipherment + - server auth + - client auth + - key agreement + profiles: null + +########################################################################### +# Certificate Signing Request (CSR) section. +# This controls the creation of the root CA certificate. +# The expiration for the root CA certificate is configured with the +# "ca.expiry" field below, whose default value is "131400h" which is +# 15 years in hours. +# The pathlength field is used to limit CA certificate hierarchy as described +# in section 4.2.1.9 of RFC 5280. +# Examples: +# 1) No pathlength value means no limit is requested. +# 2) pathlength == 1 means a limit of 1 is requested which is the default for +# a root CA. This means the root CA can issue intermediate CA certificates, +# but these intermediate CAs may not in turn issue other CA certificates +# though they can still issue end entity certificates. +# 3) pathlength == 0 means a limit of 0 is requested; +# this is the default for an intermediate CA, which means it can not issue +# CA certificates though it can still issue end entity certificates. +########################################################################### +csr: + cn: fabric-ca-server + keyrequest: + algo: ecdsa + size: 256 + names: + - C: US + ST: "North Carolina" + L: + O: Hyperledger + OU: Fabric + hosts: + - localhost + - 127.0.0.1 + - org2-tls-ca + - org2-tls-ca.test-network.svc.cluster.local + ca: + expiry: 131400h + pathlength: 1 + +########################################################################### +# Each CA can issue both X509 enrollment certificate as well as Idemix +# Credential. This section specifies configuration for the issuer component +# that is responsible for issuing Idemix credentials. +########################################################################### +idemix: + # Specifies pool size for revocation handles. A revocation handle is an unique identifier of an + # Idemix credential. The issuer will create a pool revocation handles of this specified size. When + # a credential is requested, issuer will get handle from the pool and assign it to the credential. + # Issuer will repopulate the pool with new handles when the last handle in the pool is used. + # A revocation handle and credential revocation information (CRI) are used to create non revocation proof + # by the prover to prove to the verifier that her credential is not revoked. + rhpoolsize: 1000 + + # The Idemix credential issuance is a two step process. First step is to get a nonce from the issuer + # and second step is send credential request that is constructed using the nonce to the isuser to + # request a credential. This configuration property specifies expiration for the nonces. By default is + # nonces expire after 15 seconds. The value is expressed in the time.Duration format (see https://golang.org/pkg/time/#ParseDuration). + nonceexpiration: 15s + + # Specifies interval at which expired nonces are removed from datastore. Default value is 15 minutes. + # The value is expressed in the time.Duration format (see https://golang.org/pkg/time/#ParseDuration) + noncesweepinterval: 15m + +############################################################################# +# BCCSP (BlockChain Crypto Service Provider) section is used to select which +# crypto library implementation to use +############################################################################# +bccsp: + default: SW + sw: + hash: SHA2 + security: 256 + filekeystore: + # The directory used for the software file-based keystore + keystore: msp/keystore + +############################################################################# +# Multi CA section +# +# Each Fabric CA server contains one CA by default. This section is used +# to configure multiple CAs in a single server. +# +# 1) --cacount +# Automatically generate non-default CAs. The names of these +# additional CAs are "ca1", "ca2", ... "caN", where "N" is +# This is particularly useful in a development environment to quickly set up +# multiple CAs. Note that, this config option is not applicable to intermediate CA server +# i.e., Fabric CA server that is started with intermediate.parentserver.url config +# option (-u command line option) +# +# 2) --cafiles +# For each CA config file in the list, generate a separate signing CA. Each CA +# config file in this list MAY contain all of the same elements as are found in +# the server config file except port, debug, and tls sections. +# +# Examples: +# fabric-ca-server start -b admin:adminpw --cacount 2 +# +# fabric-ca-server start -b admin:adminpw --cafiles ca/ca1/fabric-ca-server-config.yaml +# --cafiles ca/ca2/fabric-ca-server-config.yaml +# +############################################################################# + +cacount: + +cafiles: + +############################################################################# +# Intermediate CA section +# +# The relationship between servers and CAs is as follows: +# 1) A single server process may contain or function as one or more CAs. +# This is configured by the "Multi CA section" above. +# 2) Each CA is either a root CA or an intermediate CA. +# 3) Each intermediate CA has a parent CA which is either a root CA or another intermediate CA. +# +# This section pertains to configuration of #2 and #3. +# If the "intermediate.parentserver.url" property is set, +# then this is an intermediate CA with the specified parent +# CA. +# +# parentserver section +# url - The URL of the parent server +# caname - Name of the CA to enroll within the server +# +# enrollment section used to enroll intermediate CA with parent CA +# profile - Name of the signing profile to use in issuing the certificate +# label - Label to use in HSM operations +# +# tls section for secure socket connection +# certfiles - PEM-encoded list of trusted root certificate files +# client: +# certfile - PEM-encoded certificate file for when client authentication +# is enabled on server +# keyfile - PEM-encoded key file for when client authentication +# is enabled on server +############################################################################# +intermediate: + parentserver: + url: + caname: + + enrollment: + hosts: + profile: + label: + + tls: + certfiles: + client: + certfile: + keyfile: + +############################################################################# +# CA configuration section +# +# Configure the number of incorrect password attempts are allowed for +# identities. By default, the value of 'passwordattempts' is 10, which +# means that 10 incorrect password attempts can be made before an identity get +# locked out. +############################################################################# +cfg: + identities: + passwordattempts: 10 + +############################################################################### +# +# Operations section +# +############################################################################### +operations: + # host and port for the operations server + listenAddress: 127.0.0.1:9444 + + # TLS configuration for the operations endpoint + tls: + # TLS enabled + enabled: false + + # path to PEM encoded server certificate for the operations server + cert: + file: + + # path to PEM encoded server key for the operations server + key: + file: + + # require client certificate authentication to access all resources + clientAuthRequired: false + + # paths to PEM encoded ca certificates to trust for client authentication + clientRootCAs: + files: [] + +############################################################################### +# +# Metrics section +# +############################################################################### +metrics: + # statsd, prometheus, or disabled + provider: disabled + + # statsd configuration + statsd: + # network type: tcp or udp + network: udp + + # statsd server address + address: 127.0.0.1:8125 + + # the interval at which locally cached counters and gauges are pushsed + # to statsd; timings are pushed immediately + writeInterval: 10s + + # prefix is prepended to all emitted statsd merics + prefix: server diff --git a/test-network-k8s/docs/APPLICATIONS.md b/test-network-k8s/docs/APPLICATIONS.md new file mode 100644 index 00000000..2758f463 --- /dev/null +++ b/test-network-k8s/docs/APPLICATIONS.md @@ -0,0 +1,106 @@ +# Working with Applications + +## TL/DR: + +```shell +$ ./network rest-easy +Launching fabric-rest-sample application: +✅ - Ensuring fabric-rest-sample image ... +✅ - Constructing fabric-rest-sample connection profiles ... +✅ - Starting fabric-rest-sample ... + +The fabric-rest-sample has started. See https://github.com/hyperledgendary/fabric-rest-sample/tree/main/asset-transfer-basic/rest-api-typescript#rest-api for additional usage. +To access the endpoint: + +export SAMPLE_APIKEY=97834158-3224-4CE7-95F9-A148C886653E +curl -s --header "X-Api-Key: ${SAMPLE_APIKEY}" http://localhost/api/assets + +🏁 - Fabric REST sample is ready. +``` + +```shell +$ export SAMPLE_APIKEY=97834158-3224-4CE7-95F9-A148C886653E + +$ ./network chaincode invoke '{"Args":["CreateAsset","1","blue","35","tom","1000"]}' + +$ curl -s --header "X-Api-Key: ${SAMPLE_APIKEY}" http://localhost/api/assets | jq +[ + { + "Key": "1", + "Record": { + "ID": "1", + "color": "blue", + "size": 35, + "owner": "tom", + "appraisedValue": 1000 + } + } +] + +$ open https://github.com/hyperledgendary/fabric-rest-sample/tree/main/asset-transfer-basic/rest-api-typescript#rest-api +``` + +## Guide for Gateway Client Applications + +TODO: this section is a work-in-progress. + +### EXTERNAL Gateway Client (localhost) + +For certain development scenarios, it is advantageous to run a Gateway Client externally, using a bridge +or port forward to access services running behind the veil of Kubernetes networking. For instance, during active +development we can run a Gateway Client under the microscope of an IDE / debugger, on a local system, connected +to a remote network as if it were running resident within the Kube. As the system is developed (bugs addressed, etc.), +the application author can transition the updated routines into Docker containers, verify locally, and push +into the container registry for validation within the Kubernetes network. + +Here is a brief overview of the steps necessary to run EXTERNAL gateway applications: + +1. Open a TCP port forward from the local host to a targeted peer: +```shell +kubectl -n test-network port-forward svc/org1-peer1 7051:7051 +``` + +2. Add "mock DNS" records to /etc/hosts for TLS host validation: +```shell +127.0.0.1 org1-peer1 +``` + +3. Configure the gateway client to connect to `org1-peer1:7051`, or the kube TCP port forward. + + +4. Launch the gateway client application locally, e.g. in a docker container or attached to an IDE. + + +5. Update this guide with feedback, recipes, and stories of successful client development on Kube/KIND. + + +### INTERNAL Gateway Client (In Kube) + +#### TODO: Deploy + +```shell +./network application ACTION +``` + + +#### Local Container Registry + +Docker images built locally can be uploaded to the `localhost:5000` container registry for +immediate access within the Kube/KIND cluster. In addition to providing fast turn-around to/from containers +running in Kube, the use of a private container registry allows us to quickly iterate on code without uploading +images to the Internet. Even when using _private_ container registries, the use of a local server saves valuable +time when loading images into the kind control plane. + +e.g.: +```shell +docker build -t localhost:5000/my-gateway-app . +docker push localhost:5000/my-gateway-app +``` + +Provided that the `imagePullPolicy` for the client deployment is not set to `IfNotPresent`, killing the current pod +running the gateway client will force a refresh with the latest image layer available at the local registry. + + +#### Aggregating MSP and Certificates + +#### Deploying to the Namespace \ No newline at end of file diff --git a/test-network-k8s/docs/CA.md b/test-network-k8s/docs/CA.md new file mode 100644 index 00000000..c8758b24 --- /dev/null +++ b/test-network-k8s/docs/CA.md @@ -0,0 +1,281 @@ +# Certificate Authorities + +This guide serves as a companion to the [Fabric CA Deployment Guide](https://hyperledger-fabric-ca.readthedocs.io/en/latest/deployguide/ca-deploy.html), +the definitive reference for planning, configuring, and managing CAs within a production Hyperledger Fabric installation. + +For individual fabric nodes to communicate securely over a network, all interactions are performed over secure sockets +with (at a minimum) server side TLS certificate verification. In addition, for the individual participants of a Fabric +network to interact with the blockchain, the participant identities and activities are verified against an Enrollment +Certificate or 'ECert' authority. + +In this document we'll outline the key aspects of bootstrapping test network TLS and ECert CAs, registration and +enrollment of node identities, and address some effective strategies for storage and organization of channel and +node local MSP data structures. + + +### TL/DR : +```shell +$ ./network up + +Launching network "test-network": +... +✅ - Launching TLS CAs ... +✅ - Enrolling bootstrap TLS CA users ... + +✅ - Registering and enrolling ECert CA bootstrap users ... +✅ - Launching ECert CAs ... +✅ - Enrolling bootstrap ECert CA users ... +... +🏁 - Network is ready. +``` + + +## [Planning for a CA](https://hyperledger-fabric-ca.readthedocs.io/en/latest/deployguide/ca-deploy-topology.html#planning-for-a-ca) + +Setting up a CA framework is one of the more daunting aspects of a Fabric installation. There is an incredible amount +of flexibility possible with the Fabric CA architecture, so to keep things straightforward we have opted to aim for a +simplified, but realistic CA deployment illustrating the key touch points with Kubernetes: + +- Each organization maintains distinct, [independent volumes](../kube/pv-fabric-org0.yaml) for the storage of MSP and + TLS certificates. This forces the consortium organizer to plan for the distribution of _public_ certificates to + member organizations, while maintaining an independent, secret storage location for _private_ signing keys. + + +- Each organization maintains two distinct, separate CA instances : one dedicated to [TLS](../kube/org0/org0-tls-ca.yaml) + Certificate Signing Requests, and a second process dedicated to [ECert](../kube/org0/org0-ecert-ca.yaml) Enrollments + and identity MSPs. + + +- Certificate organization and [Folder Structure](https://hyperledger-fabric-ca.readthedocs.io/en/latest/deployguide/use_CA.html#folder-structure-for-your-org-and-node-admin-identities) + strictly adheres to the best practices and guidelines recommended by the CA Deployment Guide. + + +- The `cryptogen` anti-pattern is **strictly forbidden**. All TLS and MSP enrollments are constructed using the CA + registration and enrollment REST services, coordinated by calls to `fabric-ca-client` running directly on the + CA pods. When working with certificates, the fabric CA client ONLY has visibility to the organization's local volume + storage. + + +- TLS CA configuration and certificates are maintained in each org's persistent volume at `/var/hyperledger/fabric-tls-ca-server` + + +- ECert CA configuration and certificates are maintained in each org's persistent volume at `/var/hyperledger/fabric-ca-server` + + +- fabric-ca-client configuration and certificates are maintained in each org's persistent volume at `/var/hyperledger/fabric-ca-client` + + +- ECert and MSP data structures are maintained in each org's persistent volume at `/var/hyperledger/fabric/organizations` + + + +### Future Enhancements: + +- **_Bring your own Certificates_** : It would be nice to boostrap the network using a single, top-level signing authority, + rather than generating self-signed certificates when the system is bootstrapped. Ideally this will be realized by + introducing an [Intermediate CA](https://hyperledger-fabric-ca.readthedocs.io/en/latest/deployguide/ca-deploy-topology.html#when-would-i-want-an-intermediate-ca) + and/or alternate signing chains backed by formal (e.g. letsencrypt, Thawte, Verisign, etc.) certificate authorities. + + +- **_Dual Headed CAs_** : In practice, juggling two distinct deployments between TLS and ECert servers adds little + functional value. It would be nice to simplify the configuration, deployment, and bootstrapping scripts such that + each org manages a single, dual-headed CA capable of responding to both TLS as well as ECert enrollmnent rerquests. + + +- **_Time-Bomb Certificates_** : By default the certificates issued by the test network are valid for 1 (one) year. For + lightweight or adhoc testing, this is fine. But when applied to production deployments, certificate expiry is a + real operational challenge. For instance, it is possible to soft-lock a Fabric network when all system certificates + expire _en-masse_ - it's impossible to re-establish a consensus and renew the certificates! + + +- **_Mutual TLS_** : Server-side TLS is a minimum, but the addition of client-side TLS certificates will help fully + secure all TCP channels within the Fabric network. + + +- **_Bugs_** : `./network up` currently goes through the process of bootstrapping a fabric network from scratch, but + does not handle "multiple runs" or the complete course of errors that can occur in the wild. For instance, If the + routine is run multiple times in succession, it will overwrite the network's certificate chains and soft-lock the + network. + + +## [Process Overview](https://hyperledger-fabric-ca.readthedocs.io/en/latest/deployguide/cadeploy.html#) + +The [sequence of activities](https://hyperledger-fabric-ca.readthedocs.io/en/latest/deployguide/cadeploy.html#what-order-should-i-deploy-the-cas) +necessary to bring up a CA infrastructure is well documented by the CA Deployment Guide: + +1. [Deploy the TLS CAs](https://hyperledger-fabric-ca.readthedocs.io/en/latest/deployguide/cadeploy.html#deploy-the-tls-ca) + 1. [Configure the TLS CA Servers](https://hyperledger-fabric-ca.readthedocs.io/en/latest/deployguide/cadeploy.html#modify-the-tls-ca-server-configuration) + 1. [Launch the TLS CA Servers](https://hyperledger-fabric-ca.readthedocs.io/en/latest/deployguide/cadeploy.html#start-the-tls-ca-server) + 1. [Enroll the TLS CA Bootstrap Admin Users](https://hyperledger-fabric-ca.readthedocs.io/en/latest/deployguide/cadeploy.html#enroll-bootstrap-user-with-tls-ca) + +1. [Deploy the Organization CA](https://hyperledger-fabric-ca.readthedocs.io/en/latest/deployguide/cadeploy.html#deploy-an-organization-ca) + 1. [Register and enroll the org CA bootstrap identity with the TLS CA](https://hyperledger-fabric-ca.readthedocs.io/en/latest/deployguide/cadeploy.html#register-and-enroll-the-organization-ca-bootstrap-identity-with-the-tls-ca) + 1. [Configure the ECert CA Servers](https://hyperledger-fabric-ca.readthedocs.io/en/latest/deployguide/cadeploy.html#modify-the-ca-server-configuration) + 1. [Launch the ECert CA Servers](https://hyperledger-fabric-ca.readthedocs.io/en/latest/deployguide/cadeploy.html#start-the-ca-server) + 1. [Enroll the ECert CA Bootstrap / Admin User](https://hyperledger-fabric-ca.readthedocs.io/en/latest/deployguide/cadeploy.html#enroll-the-ca-admin) + + +## [Deploy the TLS CAs](https://hyperledger-fabric-ca.readthedocs.io/en/latest/deployguide/cadeploy.html#deploy-the-tls-ca) + +### [Configure the TLS CA Servers](https://hyperledger-fabric-ca.readthedocs.io/en/latest/deployguide/cadeploy.html#modify-the-tls-ca-server-configuration) + +While the CA guide suggests running the `fabric-ca-server` binary to generate a default configuration file, for the +test network we've skipped this step and have added a [config/fabric-tls-ca-server-config.yaml](../config/org0/fabric-tls-ca-server-config.yaml) +to the top level of this project. + +Changes have been made to reflect: + +- `port: 443` binds all traffic to the default HTTPS port +- `tls.enabled: true` enables TLS for registration and enrollment requests +- `ca.name: ` matches the Kubernetes `Service` host alias +- `csr.hosts:` includes host aliases for accessing the CA with Kube DNS + + +Prior to launching the CA, for each org we create a configmap including the TLS CA server yaml: + +```shell +kubectl -n test-network create configmap org0-config --from-file=config/org0 +kubectl -n test-network create configmap org1-config --from-file=config/org1 +kubectl -n test-network create configmap org2-config --from-file=config/org2 +``` + + +### [Launch the TLS CA Servers](https://hyperledger-fabric-ca.readthedocs.io/en/latest/deployguide/cadeploy.html#start-the-tls-ca-server) + +```shell +✅ - Launching TLS CAs ... +``` + +For each org we create a Kube Deployment and Service, ensuring that the org config +map and persistent volume maps to the correct location on disk. + +```shell +kubectl -n test-network apply -f kube/org0/org0-tls-ca.yaml +kubectl -n test-network apply -f kube/org1/org1-tls-ca.yaml +kubectl -n test-network apply -f kube/org2/org2-tls-ca.yaml +``` + +As a side-effect of bootstrapping the TLS CA, each storage volume will include a self-signed certificate +pair to serve as the **Root TLS Certificate**. Pay special attention to this path, as it will be used extensively +to verify the TLS host name of all services within the organization: +```shell +${FABRIC_CA_CLIENT_HOME}/tls-root-cert/tls-ca-cert.pem +``` + + +### [Enroll the TLS CA Bootstrap Admin Users](https://hyperledger-fabric-ca.readthedocs.io/en/latest/deployguide/cadeploy.html#enroll-bootstrap-user-with-tls-ca) +```shell +✅ - Enrolling bootstrap TLS CA users ... +``` + +After the TLS server is running, we need to enroll the bootstrap admin user with the CA. This admin user will +then be employed to fulfill a Certificate Signing request for the ECert CA servers, allowing for full host +verification when connecting to the ECert CAs via https. + +To enroll the bootstrap TLS CA users, each org runs within the TLS CA pod: +```shell + fabric-ca-client enroll \ + --url https://'$auth'@'${tlsca}' \ + --tls.certfiles $FABRIC_CA_CLIENT_HOME/tls-root-cert/tls-ca-cert.pem \ + --csr.hosts '${tlsca}' \ + --mspdir $FABRIC_CA_CLIENT_HOME/tls-ca/tlsadmin/msp +``` + +The --mspdir output of this command is a set of certificates for use with the ECert CA. This enrollment MSP +will be used to register and enroll the ECert bootstrap user. + + +## [Deploy the Organization CA](https://hyperledger-fabric-ca.readthedocs.io/en/latest/deployguide/cadeploy.html#deploy-an-organization-ca) + +The organization (ECert) CA is used to issue MSP certificates for nodes, channels, and identities in the fabric network. +Before we can set up the peers, orderers, and channels, we will need to bootstrap an ECert CA administrator +for each org in the network. + + +### [Register and enroll the organization CA bootstrap identity with the TLS CA](https://hyperledger-fabric-ca.readthedocs.io/en/latest/deployguide/cadeploy.html#register-and-enroll-the-organization-ca-bootstrap-identity-with-the-tls-ca) +```shell +✅ - Registering and enrolling ECert CA bootstrap users ... +``` + +The TLS CA can be used to fulfill a Certificate Signing Request on behalf of each organization's ECert CA. + +```shell + fabric-ca-client register \ + --id.name rcaadmin \ + --id.secret rcaadminpw \ + --url https://'${tlsca}' \ + --tls.certfiles $FABRIC_CA_CLIENT_HOME/tls-root-cert/tls-ca-cert.pem \ + --mspdir $FABRIC_CA_CLIENT_HOME/tls-ca/tlsadmin/msp + + fabric-ca-client enroll \ + --url https://'${tlsauth}'@'${tlsca}' \ + --tls.certfiles $FABRIC_CA_CLIENT_HOME/tls-root-cert/tls-ca-cert.pem \ + --csr.hosts '${ecertca}' \ + --mspdir $FABRIC_CA_CLIENT_HOME/tls-ca/rcaadmin/msp +``` + +**Important**: The output from this enrollment includes the ECert CA's public certificate and private signing keys. +When the ECert CA pod is launched, the server configuration references the `tls.certfile` and `tls.keyfile` attributes +by specifying `FABRIC_CA_SERVER_TLS_CERTFILE` and `FABRIC_CA_SERVER_TLS_KEYFILE` environment in the pod's environment. + + +### [Configure the ECert CA Servers](https://hyperledger-fabric-ca.readthedocs.io/en/latest/deployguide/cadeploy.html#modify-the-ca-server-configuration) + +When launching the ECert CA pods, both the org volume shares and org config maps are made available via volume shares. +The [fabric-ecert-ca-server.yaml](../config/org0/fabric-ecert-ca-server-config.yaml) includes overrides for: + +- `port: 443` binds all traffic to the default HTTPS port +- `tls.enabled: true` enables TLS for registration and enrollment requests +- `ca.name: ` matches the Kubernetes `Service` host alias +- `csr.hosts:` includes host aliases for accessing the CA with Kube DNS + +In addition, pay special attention to the location of the `FABRIC_CA_SERVER_TLS_CERTFILE` and `FABRIC_CA_SERVER_TLS_KEYFILE` +environment variables in the [ECert deployment descriptor](../kube/org0/org0-ecert-ca.yaml). These variables +reference the TLS certificate authority and signing keys as generated by the admin bootstrap enrollment. + + +### [Launch the ECert CA Servers](https://hyperledger-fabric-ca.readthedocs.io/en/latest/deployguide/cadeploy.html#start-the-ca-server) +```shell +✅ - Launching ECert CAs ... +``` + +```shell +kubectl -n test-network apply -f kube/org0/org0-ecert-ca.yaml +kubectl -n test-network apply -f kube/org1/org1-ecert-ca.yaml +kubectl -n test-network apply -f kube/org2/org2-ecert-ca.yaml +``` +- [x] Note: The `rcaadmin` enrollment's `cert.pem` and `key.pem` locations are specified in the ecert CA's k8s deployment as environment variables. + + +### [Enroll the ECert CA Bootstrap / Admin User](https://hyperledger-fabric-ca.readthedocs.io/en/latest/deployguide/cadeploy.html#enroll-the-ca-admin) +```shell +✅ - Enrolling bootstrap ECert CA users ... +``` + +Finally, after the services are active, we can connect to each organization's ECert CA using TLS and +activate the `rcaadmin` (Root Certificate Authority) admin user. This user will be employed to generate the +local MSP certificate structure for all of the nodes in our test network. + +```shell + fabric-ca-client enroll \ + --url https://'${auth}'@'${ecert_ca}' \ + --tls.certfiles $FABRIC_CA_CLIENT_HOME/tls-root-cert/tls-ca-cert.pem \ + --mspdir $FABRIC_CA_CLIENT_HOME/'${ecert_ca}'/rcaadmin/msp +``` + + +## Next Steps : + +After the CAs have been deployed, each org in the Kube namespace includes: + +- One TLS CA `Service`, forwarding internal traffic from https://orgN-tls-ca to the TLS CA +- One TLS CA `Deployment` +- One TLS CA `Pod` +- One ECert CA `Service`, forwarding internal traffic from https://orgN-ecert-ca to the ECert CA +- One ECert CA `Deployment` +- One ECert CA `Pod` +- One TLS CA admin bootstrap user `tlsadmin` enrollment and TLS root certificate. +- One ECert CA admin bootstrap user `rcaadmin` enrollment and MSP root certificate. + + +### [Launch the Test Network...](TEST_NETWORK.md) diff --git a/test-network-k8s/docs/CHAINCODE.md b/test-network-k8s/docs/CHAINCODE.md new file mode 100644 index 00000000..e967d882 --- /dev/null +++ b/test-network-k8s/docs/CHAINCODE.md @@ -0,0 +1,278 @@ +# Working with Chaincode + +In this guide we will launch the Asset Transfer Basic chaincode "as a service" on the Kubernetes test network. +In addition, we will demonstrate how to connect the test network to a chaincode process running on your local +machine as a local binary, attached in an IDE debugger, or in a Docker container. + +## TL/DR : +```shell +$ ./network chaincode deploy +✅ - Packaging chaincode folder chaincode/asset-transfer-basic ... +✅ - Transferring chaincode archive to org1 ... +✅ - Installing chaincode for org org1 ... +✅ - Launching chaincode container "ghcr.io/hyperledgendary/fabric-ccaas-asset-transfer-basic" ... +✅ - Activating chaincode basic_1.0:5e0c4db62c1f91599f58dcbfe2c37566453b1e02933646c49ba46f196723cc30 ... +🏁 - Chaincode is ready. +``` + +```shell +$ ./network chaincode invoke '{"Args":["CreateAsset","1","blue","35","tom","1000"]}' +2021-10-03 17:23:43.508 UTC [chaincodeCmd] chaincodeInvokeOrQuery -> INFO 001 Chaincode invoke successful. result: status:200 + +$ ./network chaincode query '{"Args":["ReadAsset","1"]}' | jq +{ + "ID": "1", + "color": "blue", + "size": 35, + "owner": "tom", + "appraisedValue": 1000 +} +``` + +## Running Smart Contracts on Kubernetes + +In the Kubernetes Test Network, smart contracts are developed with the [Chaincode as a Service](link) +pattern, relying on an embedded [External Builder](link) to avoid the use of a Docker daemon. With +Chaincode-as-a-Service, smart contracts are deployed to Kubernetes as `Services`, +`Deployments`, and `Pods`. When invoking smart contracts, the Peer network connects to the grpc receiver +through the port exposed by the chaincode's Kube `Service` as described in the chaincode +connection.json. + +Before installing chaincode to the network, a smart contract must: + +- Utilize the `ChaincodeServer` grpc receiver, as described in the [Fabric Operations + Guide](https://hyperledger-fabric.readthedocs.io/en/latest/cc_service.html#writing-chaincode-to-run-as-an-external-service). + +- Run as a Docker image published to a container registry. + +- Maintain a connection.json and metadata.json files in the `chaincode/$CHAINCODE_NAME` folder. + +- Accept the `CHAINCODE_ID` environment variable: _CHAINCODE_LABEL:sha_256(chaincode.tar.gz)_. + + +## Deploying Chaincode to the Network +```shell +✅ - Packaging chaincode "asset-transfer-basic" archive ... +✅ - Deploying chaincode "asset-transfer-basic" for org org1 ... +``` + +When working with chaincode, the `./network` script includes two parameters that define the Docker image +launched in the cluster and the chaincode metadata: + +- `${TEST_NETWORK_CHAINCODE_NAME:-asset-transfer-basic}` refers to the _name_ associated with the chaincode. + While packaging and deploying to the network, the `scripts/chaincode.sh` script uses this string to search + the local `/chaincode` folder for associated metadata and connection json descriptor files. + + +- `${TEST_NETWORK_CHAINCODE_IMAGE:-ghcr.io/hyperledgendary/fabric-ccaas-asset-transfer-basic}` defines the + container image that will be used when running the chaincode in Kubernetes. + + +To deploy the chaincode, the network script will: + +1. Read the `connection.json` and `metadata.json` files from the `/chaincode/${TEST_NETWORK_CHAINCODE_NAME` + folder, bundling the files into a chaincode tar.gz archive. + + +2. `kubectl cp` the chaincode archive from the local file system to the organization's persistent volume storage. + + +3. Install the chaincode archive on a peer in the organization: +```shell + export CORE_PEER_ADDRESS='${org}'-peer1:7051 + peer lifecycle chaincode install chaincode/asset-transfer-basic.tgz +``` + + +4. In typical Fabric operations, the output of the `chaincode install` command includes a generated ID of the + chaincode archive printed to standard out. This ID is manually inspected and transcribed by the + network operator when executing subsequent commands with the network peers. To avoid scraping the + output of the installation command, the test network scripts precompute the chaincode ID + as the `sha256` checksum of the tar.gz archive. + + +5. The chaincode docker [image is launched](../kube/org1/org1-cc-asset-transfer-basic.yaml) as a Kubernetes + `Deployment` specifying _CHAINCODE_ID=sha-256(archive)_ in the environment and binding a `Service` port 9999 + within the namespace. When the network sends messages to the chaincode process, it will use the host URL as + defined in the `connection.json`, connecting to the kubernetes `Service` URL and `Deployment`. + + +6. Finally, the Admin CLI issues a series of peer commands to approve and commit the chaincode for the org: + +```shell + export CORE_PEER_ADDRESS='${org}'-peer1:7051 + + peer lifecycle \ + chaincode approveformyorg \ + --channelID '${CHANNEL_NAME}' \ + --name '${CHAINCODE_NAME}' \ + --version 1 \ + --package-id '${cc_id}' \ + --sequence 1 \ + -o org0-orderer1:6050 \ + --tls --cafile /var/hyperledger/fabric/organizations/ordererOrganizations/org0.example.com/msp/tlscacerts/org0-tls-ca.pem + + peer lifecycle \ + chaincode commit \ + --channelID '${CHANNEL_NAME}' \ + --name '${CHAINCODE_NAME}' \ + --version 1 \ + --sequence 1 \ + -o org0-orderer1:6050 \ + --tls --cafile /var/hyperledger/fabric/organizations/ordererOrganizations/org0.example.com/msp/tlscacerts/org0-tls-ca.pem +``` + +## Invoking and Querying the Chaincode + +Once the chaincode service has been deployed to the cluster, and the peers have approved the chaincode, +the test scripts can issue adhoc invoke, query, and metadata requests to the network: + +### Invoke +```shell +$ ./network chaincode invoke '{"Args":["CreateAsset","1","blue","35","tom","1000"]}' +2021-10-03 17:23:43.508 UTC [chaincodeCmd] chaincodeInvokeOrQuery -> INFO 001 Chaincode invoke successful. result: status:200 +``` + +### Query +```shell +$ ./network chaincode query '{"Args":["ReadAsset","1"]}' | jq +{ + "ID": "1", + "color": "blue", + "size": 35, + "owner": "tom", + "appraisedValue": 1000 +} +``` + +### Describe +```shell +$ ./network chaincode metadata | jq | head +{ + "info": { + "title": "undefined", + "version": "latest" + }, + "contracts": { + "SmartContract": { + "info": { + "title": "SmartContract", + "version": "latest" +``` + + +## Build a Chaincode Docker Image + +Before chaincode can be started in the network, it must be compiled, linked with the grpc `ChaincodeServer`, +embedded into a Docker image, and pushed to a container registry visible to the Kubernetes cluster. + +By default, the `./network` script will launch the [asset-transfer-basic](../../asset-transfer-basic/chaincode-external) +chaincode. When the test network installs this chaincode, there is no need to build a custom Docker image as it +has previously been uploaded to a public container registry. + +As an exercise, we recommend making some updates to the asset transfer basic chaincode and then running the +modified smart contract on your local Kubernetes cluster. For instance, the current version of the +[assetTransfer.go](../../asset-transfer-basic/chaincode-external/assetTransfer.go) code is completely +silent, printing nothing to the log when functions are invoked in the container. Try adding some debugging +information to the stdout of this process, bundling into a Docker image, and pushing the docker +image to the local development container registry. + +1. Add some print statements to assetTransfer.go. E.g.: +```java + fmt.Printf("reading asset %s\n", id) +``` + +2. Build the docker image locally with: +```shell +docker build -t asset-transfer-basic ../asset-transfer-basic/chaincode-external +``` + +3. Override the test network's default chaincode image, pointing to our local container registry: +```shell +export TEST_NETWORK_CHAINCODE_IMAGE=localhost:5000/asset-transfer-basic +``` + +3. Publish the custom image to the local registry: + +```shell +docker tag asset-transfer-basic $TEST_NETWORK_CHAINCODE_IMAGE +docker push $TEST_NETWORK_CHAINCODE_IMAGE +``` + + +## Debugging Chaincode + +One of the most compelling features of Fabric's _Chaincode-as-a-Service_ pattern is that when the peer connects to a +chaincode URL, it can connect back to a port on the local host. Instead of connecting to a pod running in a +container within Kubernetes, we can simply connect to a native binary running in a debugger, an IDE, or docker image +running locally! + +Using a singular framework, we can employ this method to enable _rapid_ **edit/test/debug cycles** when authoring +code, **verify** docker images generated by a CI/CD pipeline, and run integration tests on a local Kubernetes. + +For example, we can deploy the basic asset transfer smart contract with a [connection.json](../chaincode/asset-transfer-basic-debug/connection.json) +referencing a service bound to the Docker network's IP address for the local host: +```json +{ + "address": "host.docker.internal:9999", +} +``` +When the test network opens a TCP socket to the chaincode process, the connection will be made from containers +running within Kubernetes to the port opened on the local system. Let's employ this to technique by running a +chaincode endpoint in a local Docker container, native binary, or IDE debugger: + + +0. Edit assetTransfer.go and [Build the Chaincode Image](#build-a-chaincode-docker-image) + + +1. Bring up the test network with: +```shell +$ ./network up +$ ./network channel create +``` + +2. Install the debug chaincode archive, using a connection to localhost:9999 : +```shell +$ export TEST_NETWORK_CHAINCODE_NAME=asset-transfer-basic-debug +$ export TEST_NETWORK_CHAINCODE_IMAGE=localhost:5000/asset-transfer-basic + +$ ./network chaincode install +Installing chaincode "asset-transfer-basic-debug": +✅ - Packaging chaincode folder chaincode/asset-transfer-basic-debug ... +✅ - Transferring chaincode archive to org1 ... +✅ - Installing chaincode for org org1 ... +🏁 - Chaincode is installed with CHAINCODE_ID=basic_1.0:159ed2f227586f40c5804e157919903fda2b861488f35eefb365eb9d85a73da3 +``` + +3. Set the `CHAINCODE_ID` and launch the chaincode binding to localhost:9999: +```shell +$ export CHAINCODE_ID=basic_1.0:159ed2f227586f40c5804e157919903fda2b861488f35eefb365eb9d85a73da3 + +$ docker run \ + --rm \ + --name asset-transfer-basic-debug \ + -e CHAINCODE_ID \ + -e CHAINCODE_SERVER_ADDRESS=0.0.0.0:9999 \ + -p 9999:9999 \ + localhost:5000/asset-transfer-basic +``` + +4. Activate the chaincode (commit and approve on the peer): +```shell +$ ./network chaincode activate +``` + +When the peer communicates with chaincode in this fashion, the network will reach out to the grpc server +bound to the localhost:9999, rather than connecting to services locked up behind the wall of Kubernetes +networking. + +As an exercise, try using this approach to: + +- introduce some `fmt.Printf` logging output to the chaincode, attaching to a process running locally in an IDE / debugger. +- build your local modifications into a docker container, publishing locally to localhost:5000/asset-transfer-basic +- test your local modifications by running a chaincode referencing the image hosted in the local container registry. + + +## Next Steps: + +[Writing a Blockchain Application](APPLICATIONS.md) \ No newline at end of file diff --git a/test-network-k8s/docs/CHANNELS.md b/test-network-k8s/docs/CHANNELS.md new file mode 100644 index 00000000..23fa33f6 --- /dev/null +++ b/test-network-k8s/docs/CHANNELS.md @@ -0,0 +1,206 @@ +# Channels + +Once the test network peers and orderers have been started, and the network identities have been registered +and enrolled with the ECert CA, we can construct a channel linking the participants of the test network +blockchain. + +## TL/DR : + +```shell +$ export TEST_NETWORK_CHANNEL_NAME="mychannel" + +$ ./network channel create +Creating channel "mychannel": +✅ - Creating channel MSP ... +✅ - Aggregating channel MSP ... +✅ - Launching admin CLIs ... +✅ - Creating channel "mychannel" ... +✅ - Joining org1 peers to channel "mychannel" ... +✅ - Joining org2 peers to channel "mychannel" ... +🏁 - Channel is ready. +``` + +## Process Overview + +In order to construct a communication channel, the following steps must be performed: + +1. TLS and MSP public certificates must be aggregated and distributed to all participants in the network. + +2. Each organization will launch a command-line pod with the MSP environment such that all fabric binaries are + executed as the Admin user. + +3. The channel genesis block is constructed from `configtx.yaml`, and `osnadmin` is used to distribute the new + channel configuration block to all orderers in the network. + +4. The network peers fetch the genesis block from the orderers, and use the configuration to join the channel. + + +## Distributing Channel MSP +```shell +✅ - Creating channel MSP ... +✅ - Aggregating channel MSP ... +``` + +One of the responsibilities of a Hyperledger Fabric _Consortium Organizer_ is to distribute the public MSP and +TLS certificates to organizations participating in a blockchain. In the Docker composed based test network, or +systems bootstrapped with the `cryptogen` command, all of the public certificates will be available on a common +file system or volume share. In our Kubernetes test network, each organization maintains the cryptographic +assets on a distinct persistent volume, invisible to other the other participants in the network. + +To distribute the TLS and MSP _public_ certificates, the test network emulates the responsibilities of the +consortium organizer by constructing a [Channel MSP](https://hyperledger-fabric.readthedocs.io/en/latest/membership/membership.html#channel-msps) +structure, extracting the relevant certificate files into a single `msp-{org}.tar.gz` archive. This MSP +archive is then relayed to network participants, where it can be extracted and used to set the MSP context +in which the peer executes administrative commands. + +The kube-specific techniques employed in MSP [construction](link) and [distribution](link) are: + +- Channel MSP is generated by piping shell commands into each org's ECert CA pod. +- Channel MSP is extracted to the local system by piping the output of `tar` through `kubectl` (equivalent + to the [kubectl cp command](https://kubernetes.io/docs/reference/generated/kubectl/kubectl-commands#cp)). + MSP archives are saved locally in `/build/msp-{org}.tar.gz` archives. +- Channel MSP is distributed across network participants by transferring the MSP archive files from + `build/msp/msp-{org}.tgz` into the cluster as a config map. +- An `initContainer` is launched in each organization's Admin CLI pod, unfurling the MSP context from the + `msp-config` config map into the local volume. + +Despite this additional complexity, this technique allows us to carefully target the MSP context in which +remote peer commands execute. The construct of an _MSP Archive_ may be extended to other circumstances +in which a consortium organizer transfers the _public_ certificates in an out-of-band fashion to +participants of a blockchain network. + +Aggregating the certificates as a local MSP archive is accomplished by piping a `tar` archive from the output +of a remote `kubectl` into a local archive files. These files are then mounted into the Kube namespace by +constructing the `msp-config` config map: + +```shell +kubectl -n $NS exec deploy/org0-ecert-ca -- tar zcvf - -C /var/hyperledger/fabric organizations/ordererOrganizations/org0.example.com/msp > msp/msp-org0.example.com.tgz +kubectl -n $NS exec deploy/org1-ecert-ca -- tar zcvf - -C /var/hyperledger/fabric organizations/peerOrganizations/org1.example.com/msp > msp/msp-org1.example.com.tgz +kubectl -n $NS exec deploy/org2-ecert-ca -- tar zcvf - -C /var/hyperledger/fabric organizations/peerOrganizations/org2.example.com/msp > msp/msp-org2.example.com.tgz + +kubectl -n $NS delete configmap msp-config || true +kubectl -n $NS create configmap msp-config --from-file=msp/``` +``` + + +## `Admin` Commands +```shell +✅ - Launching admin CLIs ... +``` + +After the channel MSP archives have been constructed and loaded into the `msp-config` ConfigMap, a series +of kubernetes pods are launched in the namespace with an environment suitable for running the Fabric +`peer` commands as the organization Administrator. Before starting the CLI admin container, an `initContainer` +reads the MSP archives and unfurls them into a location on the org's persistent volume: + +```yaml +# This init container will unfurl all of the MSP archives listed in the msp-config config map. +initContainers: +- name: msp-unfurl + image: busybox + command: + - sh + - -c + - "for msp in $(ls /msp/msp-*.tgz); do echo $msp && tar zxvf $msp -C /var/hyperledger/fabric; done" + volumeMounts: + - name: msp-config + mountPath: /msp + - name: fabric-volume + mountPath: /var/hyperledger +``` + +Once the MSP archives are extracted, the CLI is launched and the environment set such that `peer` commands +will be executed with the organization's Administrative role. + +```shell +cat kube/org0/org0-admin-cli.yaml | sed 's,{{FABRIC_VERSION}},'${FABRIC_VERSION}',g' | kubectl -n $NS apply -f - +cat kube/org1/org1-admin-cli.yaml | sed 's,{{FABRIC_VERSION}},'${FABRIC_VERSION}',g' | kubectl -n $NS apply -f - +cat kube/org2/org2-admin-cli.yaml | sed 's,{{FABRIC_VERSION}},'${FABRIC_VERSION}',g' | kubectl -n $NS apply -f - +``` + +## Create the Channel +```shell +✅ - Creating channel "mychannel" ... +``` + +As the _consortium leader_ org0, we create the channel's genesis block and use the orderer admin REST +services to register the channel genesis block configuration on the ordering nodes: + +```shell +configtxgen -profile TwoOrgsApplicationGenesis -channelID '${CHANNEL_NAME}' -outputBlock genesis_block.pb + +osnadmin channel join --orderer-address org0-orderer1:9443 --channelID '${CHANNEL_NAME}' --config-block genesis_block.pb +osnadmin channel join --orderer-address org0-orderer2:9443 --channelID '${CHANNEL_NAME}' --config-block genesis_block.pb +osnadmin channel join --orderer-address org0-orderer3:9443 --channelID '${CHANNEL_NAME}' --config-block genesis_block.pb +``` + + +## Join Peers + +```shell +✅ - Joining org1 peers to channel "mychannel" ... +✅ - Joining org2 peers to channel "mychannel" ... +``` + +After the channel configurations have been registered with the network orderers, we will join the peers to the channel +by retrieving the genesis block from the orderers and then joining the channel: + +```shell + # Fetch the genesis block from an orderer + peer channel \ + fetch oldest \ + genesis_block.pb \ + -c '${CHANNEL_NAME}' \ + -o org0-orderer1:6050 \ + --tls --cafile /var/hyperledger/fabric/organizations/ordererOrganizations/org0.example.com/msp/tlscacerts/org0-tls-ca.pem + + # Join peer1 to the channel. + CORE_PEER_ADDRESS='${org}'-peer1:7051 \ + peer channel \ + join \ + -b genesis_block.pb \ + -o org0-orderer1:6050 \ + --tls --cafile /var/hyperledger/fabric/organizations/ordererOrganizations/org0.example.com/msp/tlscacerts/org0-tls-ca.pem + + # Join peer2 to the channel. + CORE_PEER_ADDRESS='${org}'-peer2:7051 \ + peer channel \ + join \ + -b genesis_block.pb \ + -o org0-orderer1:6050 \ + --tls --cafile /var/hyperledger/fabric/organizations/ordererOrganizations/org0.example.com/msp/tlscacerts/org0-tls-ca.pem +``` + + +## Set Anchor Peers (Optional) +```shell +$ ./network anchor peer2 +✅ - Updating anchor peers to "peer2" ... +``` + +In the test network, the configtx.yaml sets the organization [Anchor Peers](https://hyperledger-fabric.readthedocs.io/en/latest/glossary.html?highlight=anchor#anchor-peer) +to "peer1" in the genesis block. As such, no additional configuration is necessary for neighboring +organizations to discover additional peers in the network. + +However, the process of setting the anchor peers on a channel requires a more complicated scripting process, so we +have included in the test network a mechanism to illustrate how anchor peers may be set on a network after a +channel has been constructed. + +Up to this point in the network configuration, the shell scripts orchestrating the remote volumes, peers, and +admin commands have all been executed by piping a sequence of commands into an existing pod directly +into the input of a `kubectl` command. For small command sets this is adequate, but for the more complicated +process of registering a channel anchor peer, we have elected to use a different approach to launch the peer +update scripts on the kubernetes cluster. + +When updating anchor peers, the `./network` script will: + +1. Transfer the shell scripts from `/scripts/*.sh` into the remote organization's persistent volume. +2. Issue a `kubectl exec -c "script-name.sh {args}"` on the org's admin CLI pod. + +For non-trivial Fabric administative tasks, this approach of uploading a script into the cluster and then +executing in an admin pod works well. + + +## Next Steps + +### [Working with Chaincode](CHAINCODE.md) \ No newline at end of file diff --git a/test-network-k8s/docs/KUBERNETES.md b/test-network-k8s/docs/KUBERNETES.md new file mode 100644 index 00000000..6472e1cf --- /dev/null +++ b/test-network-k8s/docs/KUBERNETES.md @@ -0,0 +1,161 @@ +# Kubernetes + +To get started with the Kube test network, you will need access to a Kubernetes cluster. + +## TL/DR : + +```shell +$ ./network kind +Initializing KIND cluster "kind": +✅ - Pulling docker images for Fabric 2.3.2 ... +✅ - Creating cluster "kind" ... +✅ - Launching Nginx ingress controller ... +✅ - Launching container registry "kind-registry" at localhost:5000 ... +🏁 - Cluster is ready. +``` + +and : +```shell +$ ./network unkind +Deleting cluster "kind": +☠️ - Deleting KIND cluster kind ... +🏁 - Cluster is gone. +``` + + +## Kube Context: + +For illustration purposes, this project attempts in all cases to _keep it simple_ as the +general rule. By default, we will rely on `kind` ([Kubernetes IN Docker](https://kind.sigs.k8s.io)) +as a mechanism to quickly spin up ephemeral, short-lived clusters for development and +illustration. + +To maximize portability across revisions, vendor distributions, hardware profiles, and +network topologies, this project relies _exclusively_ on scripted interaction with the +Kube API controller to reflect updates in a remote cluster. While this may not be the +ideal technique for managing production workloads, the objective of this guide is to provide +clarity on the nuances of Fabric / Kubernetes deployments, rather than an opinionated +perspective on state of the art techniques for cloud Dev/Ops. Targeting +the core Kube APIs means that there is a good chance that the systems will work "as-is" +simply by setting the kubectl context to reference a cloud-native cluster (e.g. OCP, IKS, +AWS, etc.) + +If you don't have access to an existing cluster, or want to set up a short-lived cluster +for development, testing, or CI, you can create a new cluster with: + +```shell +$ ./network kind +``` +or: +```shell +$ kind create cluster +``` + +By default, `kind` will set the current Kube context to reference the new cluster. Any +interaction with `kubectl` (or kube-context aware SDKs) will inherit the current context. + +```shell +$ kubectl cluster-info +Kubernetes control plane is running at https://127.0.0.1:55346 +CoreDNS is running at https://127.0.0.1:55346/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy + +To further debug and diagnose cluster problems, use 'kubectl cluster-info dump'. +``` + +When you are done with the cluster, tear it down with: +```shell +$ ./network unkind +``` +or: +```shell +$ kind delete cluster +``` + +## Test Network Structure + +To emulate a more realistic example of multi-party collaboration, the test network +forms a blockchain consensus group spanning three virtual organizations. Access to the +blockchain is entirely constrained to Kubernetes private networks, and consuming applications +make use of a Kube ingress controller for external visibility. + +In k8s terms: + +- The blockchain is contained within a single Kubernetes `Cluster`. +- Blockchain services (nodes, orderers, chaincode, etc.) reside within a single `Namespace`. +- Each organization maintains a distinct, independent `PersistentVolumeClaim` for TLS certificates, + local MSP, private data, and transaction ledgers. +- Smart Contracts rely exclusively on the [Chaincode-as-a-Service](link) and [External Builder](link) + patterns, running in the cluster as Kube `Deployments` with companion `Services`. +- An HTTP(s) `Ingress` and companion gateway application is required for external access to the blockchain. + +When running the test network locally, the `./network kind` bootstrap will configure the system with +an [Nginx ingress controller](link), a private [Container Registry](link), and persistent volumes / claims for +host-local organization storage. + +Behind the scenes, `./network kind` is running: + +```shell +# Create the KIND cluster and nginx ingress controller bound to :80 and :443 +kind create cluster --name ${TEST_NETWORK_CLUSTER_NAME:-kind} --config scripts/kind-config.yaml + +# Create the Kube namespace +kubectl create namespace ${TEST_NETWORK_NAMESPACE:-test-network} + +# Create host persistent volumes (tied the kind-control-plane docker image lifetime) +kubectl create -f kube/pv-fabric-org0.yaml +kubectl create -f kube/pv-fabric-org1.yaml +kubectl create -f kube/pv-fabric-org2.yaml + +# Create persistent volume claims binding to the host (docker) volumes +kubectl -n $NS create -f kube/pvc-fabric-org0.yaml +kubectl -n $NS create -f kube/pvc-fabric-org1.yaml +kubectl -n $NS create -f kube/pvc-fabric-org2.yaml +``` + +## Container Registry + +The [kube yaml descriptors](../kube) generally rely on the public Fabric images maintained at the public +Docker and GitHub container registries. For casual usage, the test network will bootstrap and launch CAs, +peers, orderers, chaincode, and sample applications without any additional configuration. + +While public images are made available for pre-canned samples, there will undoubtedly be cases +where you would like to build custom chaincode, gateway client applications, or custom builds of core +Fabric binaries without uploading your code to a public registry. For this purpose, the Kube test +network includes a [Local Registry](https://kind.sigs.k8s.io/docs/user/local-registry/) available for +you to _quickly_ deploy custom images directly into the cluster without uploading your code to the +Internet. + +By default, the [kind.sh](../scripts/kind.sh) bootstrap will configure and link up a local container +registry running at `localhost:5000/`. Images pushed to this registry will be immediately available +to Pods deployed to the local cluster. + +For dev/test/CI based flows using an external registry, the traditional Kubernetes practice of +[Adding ImagePullSecrets to a service account](https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/#add-imagepullsecrets-to-a-service-account) +still applies. + + +## Cloud Vendors + +While the test network primarily targets KIND clusters, the singular reliance on the Kube API plane +means that it should also work without modification on any modern cloud-based or bare metal +Kubernetes distribution. While supporting the entire ecosystem of cloud vendors is not in scope +for this sample project, we'd love to hear feedback, success stories, or bugs related to applying the +test network to additional platforms. + +In general, at a high-level the steps required to port the test network to ANY kube vendor are: + +- Configure an HTTP `Ingress` for access to any gateway, REST, or companion blockchain applications. +- Register `PersistentVolumeClaims` for each of the organizations in the test network. +- Create a `Namespace` for each instance of the test network. +- Upload your chaincode, gateway clients, and application logic to an external Container Registry. +- Run with a `ServiceAccount` and role bindings suitable for creating `Pods`, `Deployments`, and `Services`. + +Example configurations for common cloud vendors: + +### IKS +### OCP +### AWS +### Azure + +## Next : [Fabric Certificate Authorities](CA.md) + diff --git a/test-network-k8s/docs/README.md b/test-network-k8s/docs/README.md new file mode 100644 index 00000000..9319e8f8 --- /dev/null +++ b/test-network-k8s/docs/README.md @@ -0,0 +1,49 @@ +# Kubernetes Test Network + +Starting in release 2.0, Hyperledger introduced the [test-network](https://hyperledger-fabric.readthedocs.io/en/latest/test_network.html) +to serve as both an accelerator and learning resource for running Fabric networks. In addition to +providing a study guide for operational patterns, the test-network provided a baseline environment for members of +the Fabric community to quickly get up to speed with a working, local system, author smart contracts, and develop +simple blockchain applications. + +While test-network provided a solid foundation for casual Fabric development, the over-reliance on +[Docker Compose](https://docs.docker.com/compose/) introduced tremendous, non-trivial complexity when transitioning +applications to production. Without belaboring the many issues and anti-patterns present in the Compose-based +test network, we'll submit that the best path forward is to _align_ the development and production patterns around a +common orchestration framework - Kubernetes. + +Similar to Fabric, Kubernetes introduces a steep learning curve and presents a dizzying array of operational +flexibility. In this guide, we'll outline the design considerations in the [`./network`](../network) +scripts, provide a supplement to the [Fabric CA Deployment Guide](https://hyperledger-fabric-ca.readthedocs.io/en/latest/deployguide/ca-deploy.html), +and build up to a reference model for realistic production deployments on Kubernetes. + +_Ahoy!_ + + +## Network Topology + +The Kube test network establishes as consortium among a dedicated ordering organization and two peer organizations. +Participation in the network is managed over a channel, and transactions are committed to the blockchain ledgers by +invoking the [asset-transfer-basic](https://github.com/hyperledgendary/fabric-ccaas-asset-transfer-basic) +_Chaincode-as-a-Service_ running in a shared Kubernetes namespace. Each organization maintains indepedendent TLS +and ECert CAs for management of local, channel, and user MSP contexts. + +![Test Network](images/test-network.png) + + +## Detailed Guides + +- [`./network`](NETWORK.md) +- [Working with Kubernetes](KUBERNETES.md) +- [Certificate Authorities](CA.md) + - [Planning for a CA](CA.md#planning-for-a-ca) + - [Deploy the TLS CAs](CA.md#deploy-the-tls-cas) + - [Deploy the ECert CAs](CA.md#deploy-the-organization-ca) +- [Launching the Test Network](TEST_NETWORK.md) + - [Registering and Enrolling Identities](CA.md#registering-and-enrolling-identities) + - [Assembling Node MSPs](link) + - [Deploy Orderers and Peers](link) +- [Working with Channels](CHANNELS.md) +- [Working with Chaincode](CHAINCODE.md) +- [Working with Applications](APPLICATIONS.md) + diff --git a/test-network-k8s/docs/TEST_NETWORK.md b/test-network-k8s/docs/TEST_NETWORK.md new file mode 100644 index 00000000..2a97728e --- /dev/null +++ b/test-network-k8s/docs/TEST_NETWORK.md @@ -0,0 +1,221 @@ + +## Network Overview + +After we have set up a series of TLS and ECert CA services, we'll use the CAs to generate +[Local MSP](https://hyperledger-fabric.readthedocs.io/en/latest/membership/membership.html#local-msps) structures for +all of the nodes, using the local MSPs to launch our network peers and orderers. + + +### TL/DR : + +```shell +./network up +... +✅ - Creating local node MSP ... +✅ - Launching orderers ... +✅ - Launching peers ... +🏁 - Network is ready. +``` + +## Fabric MSP Context + +Before we launch the network peers and orderers, each node in the network needs to have available: + +- TLS Root Certificates for all organizations in the network +- TLS Certificates and Signing Keys for SSL server/hostname verification of the network node +- Enrollment Certificates validating the network node identity (local MSP) +- Enrollment Certificates for an `Admin` identity / role for the organization. + +In order to create the local node MSP, we must first register and enroll the node identities with the ECert CAs, and +then organize the TLS and MSP certificates into a location suitable for launching the network services. + +The key steps in this process are: + +- [Registering and enrolling identities with a CA](https://hyperledger-fabric-ca.readthedocs.io/en/latest/deployguide/use_CA.html#registering-and-enrolling-identities-with-a-ca) +- [Create the local MSP of a node](https://hyperledger-fabric-ca.readthedocs.io/en/latest/deployguide/use_CA.html#create-the-local-msp-of-a-node) + +In the test network, each organization includes a function that wraps the registration, enrollment, and MSP aggregation +into a series of fabric-ca-client calls. [The script](../scripts/test_network.sh) will be executed directly on the +org's ECert CA pod, with access to the persistent volume for storage of the MSP and TLS certificates. While this is +largely boilerplate scripting, the process is straightforward: For each node in the network, we'll use the CAs to +generate TLS+MSP certificates, bundling into an MSP with a `config.yaml` specifying the fabric roles associated with +the target usage in the network. + +For example, the ordering organization sets up the node local MSP with: +```shell +# Each identity in the network needs a registration and enrollment. +fabric-ca-client register --id.name org0-orderer1 --id.secret ordererpw --id.type orderer --url https://org0-ecert-ca --mspdir $FABRIC_CA_CLIENT_HOME/org0-ecert-ca/rcaadmin/msp +fabric-ca-client register --id.name org0-orderer2 --id.secret ordererpw --id.type orderer --url https://org0-ecert-ca --mspdir $FABRIC_CA_CLIENT_HOME/org0-ecert-ca/rcaadmin/msp +fabric-ca-client register --id.name org0-orderer3 --id.secret ordererpw --id.type orderer --url https://org0-ecert-ca --mspdir $FABRIC_CA_CLIENT_HOME/org0-ecert-ca/rcaadmin/msp +fabric-ca-client register --id.name org0-admin --id.secret org0adminpw --id.type admin --url https://org0-ecert-ca --mspdir $FABRIC_CA_CLIENT_HOME/org0-ecert-ca/rcaadmin/msp --id.attrs "hf.Registrar.Roles=client,hf.Registrar.Attributes=*,hf.Revoker=true,hf.GenCRL=true,admin=true:ecert,abac.init=true:ecert" + +fabric-ca-client enroll --url https://org0-orderer1:ordererpw@org0-ecert-ca --csr.hosts org0-orderer1 --mspdir /var/hyperledger/fabric/organizations/ordererOrganizations/org0.example.com/orderers/org0-orderer1.org0.example.com/msp +fabric-ca-client enroll --url https://org0-orderer2:ordererpw@org0-ecert-ca --csr.hosts org0-orderer2 --mspdir /var/hyperledger/fabric/organizations/ordererOrganizations/org0.example.com/orderers/org0-orderer2.org0.example.com/msp +fabric-ca-client enroll --url https://org0-orderer3:ordererpw@org0-ecert-ca --csr.hosts org0-orderer3 --mspdir /var/hyperledger/fabric/organizations/ordererOrganizations/org0.example.com/orderers/org0-orderer3.org0.example.com/msp +fabric-ca-client enroll --url https://org0-admin:org0adminpw@org0-ecert-ca --mspdir /var/hyperledger/fabric/organizations/ordererOrganizations/org0.example.com/users/Admin@org0.example.com/msp + +# Each node in the network needs a TLS registration and enrollment. +fabric-ca-client register --id.name org0-orderer1 --id.secret ordererpw --id.type orderer --url https://org0-tls-ca --mspdir $FABRIC_CA_CLIENT_HOME/tls-ca/tlsadmin/msp +fabric-ca-client register --id.name org0-orderer2 --id.secret ordererpw --id.type orderer --url https://org0-tls-ca --mspdir $FABRIC_CA_CLIENT_HOME/tls-ca/tlsadmin/msp +fabric-ca-client register --id.name org0-orderer3 --id.secret ordererpw --id.type orderer --url https://org0-tls-ca --mspdir $FABRIC_CA_CLIENT_HOME/tls-ca/tlsadmin/msp + +fabric-ca-client enroll --url https://org0-orderer1:ordererpw@org0-tls-ca --csr.hosts org0-orderer1 --mspdir /var/hyperledger/fabric/organizations/ordererOrganizations/org0.example.com/orderers/org0-orderer1.org0.example.com/tls +fabric-ca-client enroll --url https://org0-orderer2:ordererpw@org0-tls-ca --csr.hosts org0-orderer2 --mspdir /var/hyperledger/fabric/organizations/ordererOrganizations/org0.example.com/orderers/org0-orderer2.org0.example.com/tls +fabric-ca-client enroll --url https://org0-orderer3:ordererpw@org0-tls-ca --csr.hosts org0-orderer3 --mspdir /var/hyperledger/fabric/organizations/ordererOrganizations/org0.example.com/orderers/org0-orderer3.org0.example.com/tls + +# Copy the TLS signing keys to a fixed path for convenience when starting the orderers. +cp /var/hyperledger/fabric/organizations/ordererOrganizations/org0.example.com/orderers/org0-orderer1.org0.example.com/tls/keystore/*_sk /var/hyperledger/fabric/organizations/ordererOrganizations/org0.example.com/orderers/org0-orderer1.org0.example.com/tls/keystore/server.key +cp /var/hyperledger/fabric/organizations/ordererOrganizations/org0.example.com/orderers/org0-orderer2.org0.example.com/tls/keystore/*_sk /var/hyperledger/fabric/organizations/ordererOrganizations/org0.example.com/orderers/org0-orderer2.org0.example.com/tls/keystore/server.key +cp /var/hyperledger/fabric/organizations/ordererOrganizations/org0.example.com/orderers/org0-orderer3.org0.example.com/tls/keystore/*_sk /var/hyperledger/fabric/organizations/ordererOrganizations/org0.example.com/orderers/org0-orderer3.org0.example.com/tls/keystore/server.key + +# Create an MSP config.yaml (why is this not generated by the enrollment by fabric-ca-client?) +echo "NodeOUs: + Enable: true + ClientOUIdentifier: + Certificate: cacerts/org0-ecert-ca.pem + OrganizationalUnitIdentifier: client + PeerOUIdentifier: + Certificate: cacerts/org0-ecert-ca.pem + OrganizationalUnitIdentifier: peer + AdminOUIdentifier: + Certificate: cacerts/org0-ecert-ca.pem + OrganizationalUnitIdentifier: admin + OrdererOUIdentifier: + Certificate: cacerts/org0-ecert-ca.pem + OrganizationalUnitIdentifier: orderer" > /var/hyperledger/fabric/organizations/ordererOrganizations/org0.example.com/orderers/org0-orderer1.org0.example.com/msp/config.yaml + +cp /var/hyperledger/fabric/organizations/ordererOrganizations/org0.example.com/orderers/org0-orderer1.org0.example.com/msp/config.yaml /var/hyperledger/fabric/organizations/ordererOrganizations/org0.example.com/orderers/org0-orderer2.org0.example.com/msp/config.yaml +cp /var/hyperledger/fabric/organizations/ordererOrganizations/org0.example.com/orderers/org0-orderer1.org0.example.com/msp/config.yaml /var/hyperledger/fabric/organizations/ordererOrganizations/org0.example.com/orderers/org0-orderer3.org0.example.com/msp/config.yaml +``` + + +## External Chaincode Builders + +Running Fabric in Kubernetes places some unique constraints on the Chaincode lifecycle: + +- Many cloud-native vendors rely on [containerd.io](https://containerd.io) to manage the lifecycle of containers + within a cluster. By contrast, Fabric assumes the presence of a Docker daemon to compile and launch chaincode + containers. Without a local Docker daemon, Fabric's default chaincode pipeline is doomed! + + +- For security and operational concerns, it is a "non-starter" to run a docker daemon on Kubernetes worker nodes. + + +- For cloud-ready development, test, validation, CI/CD, and production practices, the use of the + [Chaincode as a Service](https://hyperledger-fabric.readthedocs.io/en/latest/cc_service.html) pattern provides a + _vastly superior user experience_. However, with the current (2.3) Fabric builds, the configuration of [External + Chaincode Builders](https://hyperledger-fabric.readthedocs.io/en/latest/cc_launcher.html) is non-trivial and + includes some real complexity for deployment to Kubernetes. + + +- Running Chaincode builds in Docker in Docker, running in Kubernetes in Docker is ... interesting. Let's + step back and _keep it simple_. + + + +For the Kube Test Network, we've configured the peer nodes to launch with the [fabric-ccs-builder](https://github.com/hyperledgendary/fabric-ccs-builder) +External Chaincode Builders pre-bundled into the network. When chaincode is installed on the peers, the external +builder binaries will be invoked, bypassing the reliance on a local Docker daemon running in Kubernetes. + +This configuration is accomplished by registering an external builder in the peer core.yaml: + +```yaml + externalBuilders: + - path: /var/hyperledger/fabric/chaincode/ccs-builder + name: ccs-builder + propagateEnvironment: + - HOME + - CORE_PEER_ID + - CORE_PEER_LOCALMSPID +``` + +At launch time, the Kubernetes deployment includes an init container that will load the fabric-ccs-builder binaries +from a public container registry, copying the external builders into the target volume in the peer: + +```yaml + initContainers: + - name: fabric-ccs-builder + image: ghcr.io/hyperledgendary/fabric-ccs-builder + command: [sh, -c] + args: ["cp /go/bin/* /var/hyperledger/fabric/chaincode/ccs-builder/bin/"] + volumeMounts: + - name: ccs-builder + mountPath: /var/hyperledger/fabric/chaincode/ccs-builder/bin +``` + +With this configuration we eliminate the reliance on Docker daemon, fully supporting the _Chaincode-as-a-Service_ +pattern for building smart contracts in a cloud-native environment. + +- [x] Pro tip: Use the companion container registry at `localhost:5000` to deploy custom chaincode into the test network. +- [x] Pro tip: Deploy a chaincode with `address: host.docker.internal:9999` and run your chaincode in a debugger. +- [ ] Note: An external chaincode builder will be included in future releases of Fabric. + + +## Starting Peers and Orderers + +```shell +✅ - Launching orderers ... +✅ - Launching peers ... +``` + +Once the local MSP structures for the network nodes have been created, the orderers and peers may be launched in the +namespace. System nodes will read base configuration files (orderer.yaml and core.yaml) from the organization +config folder, made available in Kubernetes as the `fabric-config${org}` config map. + +Each orderer and peer creates one `Deployment`, `Pod`, and `Service` in the namespace. In addition, each org +defines an `orgN-peerM-config` `ConfigMap` with environment variable overrides replacing the default settings +in the core.yaml file. Note that each node's [environment](../kube/org1/org1-peer1.yaml) includes pointers to the +node local MSP folders, certificates, and TLS signing keys that we generated above. + +Note that the deployment yaml files include some basic template substitution and parameters. For simplicity and +clarity, we elected to use basic string substitution with sed/awk/bash/etc., rather than introduce a Kube template +binding system (e.g. Helm, Kustomize, Kapitan, Ansible, etc.) for manipulating yaml templates: + +```shell +cat kube/org0/org0-orderer1.yaml | sed 's,{{FABRIC_VERSION}},'${FABRIC_VERSION}',g' | kubectl -n $NS -f - +cat kube/org0/org0-orderer2.yaml | sed 's,{{FABRIC_VERSION}},'${FABRIC_VERSION}',g' | kubectl -n $NS -f - +cat kube/org0/org0-orderer3.yaml | sed 's,{{FABRIC_VERSION}},'${FABRIC_VERSION}',g' | kubectl -n $NS -f - + +# Wait for the orderers to completely start before launching the network peer nodes. +kubectl -n $NS rollout status deploy/org0-orderer1 +kubectl -n $NS rollout status deploy/org0-orderer2 +kubectl -n $NS rollout status deploy/org0-orderer3 + +cat kube/org1/org1-peer1.yaml | sed 's,{{FABRIC_VERSION}},'${FABRIC_VERSION}',g' | kubectl -n $NS -f - +cat kube/org1/org1-peer2.yaml | sed 's,{{FABRIC_VERSION}},'${FABRIC_VERSION}',g' | kubectl -n $NS -f - +cat kube/org2/org2-peer1.yaml | sed 's,{{FABRIC_VERSION}},'${FABRIC_VERSION}',g' | kubectl -n $NS -f - +cat kube/org2/org2-peer2.yaml | sed 's,{{FABRIC_VERSION}},'${FABRIC_VERSION}',g' | kubectl -n $NS -f - +``` + +- [x] Pro tip: Run an early-release Fabric build by setting `TEST_NETWORK_FABRIC_VERSION=2.4.0-beta` + + +## Next Steps : + +After the peers and orderers have started, the Kube namespace includes pods, deployments, and service bindings for: + +- Org0 (org0.example.com): + - TLS Certificate Authority : https://org0-tls-ca + - ECert Certificate Authority : https://org0-ecert-ca + - Orderer1 : grpcs://org0-orderer1 + - Orderer2 : grpcs://org0-orderer2 + - Orderer3 : grpcs://org0-orderer3 + + +- Org1 (org1.example.com): + - TLS Certificate Authority : https://org1-tls-ca + - ECert Certificate Authority : https://org1-ecert-ca + - Peer Node 1 : grpcs://org1-peer1 + - Peer Node 2 : grpcs://org1-peer2 + + +- Org2 (org2.example.com): + - TLS Certificate Authority : https://org2-tls-ca + - ECert Certificate Authority : https://org2-ecert-ca + - Peer Node 1 : grpcs://org2-peer1 + - Peer Node 2 : grpcs://org2-peer2 + + + +### Next : [Working With Channels](CHANNELS.md) + diff --git a/test-network-k8s/docs/images/test-network.png b/test-network-k8s/docs/images/test-network.png new file mode 100644 index 0000000000000000000000000000000000000000..b993135c67b2cdcd5a99838b6e653114783796d7 GIT binary patch literal 225031 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
  • DzGDPR5p5 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

    k73?t$R6yV67 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 Yd#CA=$?>ZA8t_j{Na|6RfX<8m0~9XE1ONa4 literal 0 HcmV?d00001 diff --git a/test-network-k8s/kube/application-deployment.yaml b/test-network-k8s/kube/application-deployment.yaml new file mode 100644 index 00000000..4e02f706 --- /dev/null +++ b/test-network-k8s/kube/application-deployment.yaml @@ -0,0 +1,48 @@ +# +# Copyright IBM Corp. All Rights Reserved. +# +# SPDX-License-Identifier: Apache-2.0 +# +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: application-deployment +spec: + replicas: 1 + selector: + matchLabels: + app: application-deployment + template: + metadata: + labels: + app: application-deployment + spec: + containers: + - name: main + image: + imagePullPolicy: Always + envFrom: + - configMapRef: + name: app-fabric-org1-v1-map + resources: + requests: + memory: "50Mi" + cpu: "0.1" + volumeMounts: + - name: fabricids + mountPath: /fabric/application/wallet + - name: fabric-ccp + mountPath: /fabric/application/gateways + - name: tlscerts + mountPath: /fabric/tlscacerts + volumes: + - name: fabric-ccp + configMap: + name: app-fabric-ccp-v1-map + - name: fabricids + configMap: + name: app-fabric-ids-v1-map + - name: tlscerts + configMap: + name: app-fabric-tls-v1-map diff --git a/test-network-k8s/kube/fabric-rest-sample.yaml b/test-network-k8s/kube/fabric-rest-sample.yaml new file mode 100644 index 00000000..2bf99d7f --- /dev/null +++ b/test-network-k8s/kube/fabric-rest-sample.yaml @@ -0,0 +1,258 @@ +# +# Copyright IBM Corp. All Rights Reserved. +# +# SPDX-License-Identifier: Apache-2.0 +# +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: fabric-rest-sample-config-example +data: + HLF_CONNECTION_PROFILE_ORG1: | + { + "name": "test-network-org1", + "version": "1.0.0", + "client": { + "organization": "Org1", + "connection": { + "timeout": { + "peer": { + "endorser": "500" + } + } + } + }, + "organizations": { + "Org1": { + "mspid": "Org1MSP", + "peers": [ + "org1-peer1" + ], + "certificateAuthorities": [ + "org1-ecert" + ] + } + }, + "peers": { + "org1-peer1": { + "url": "grpcs://org1-peer1:7051", + "tlsCACerts": { + "pem": "-----BEGIN CERTIFICATE-----\\nMIICvzCCAmWgAwIBAgIULJGws7jbEY6ruSgDuvi9L7VphvIwCgYIKoZIzj0EAwIw\\naDELMAkGA1UEBhMCVVMxFzAVBgNVBAgTDk5vcnRoIENhcm9saW5hMRQwEgYDVQQK\\nEwtIeXBlcmxlZGdlcjEPMA0GA1UECxMGRmFicmljMRkwFwYDVQQDExBmYWJyaWMt\\nY2Etc2VydmVyMB4XDTIxMDkyMDE2MDkwMFoXDTIyMDkyMDE2MTQwMFowYDELMAkG\\nA1UEBhMCVVMxFzAVBgNVBAgTDk5vcnRoIENhcm9saW5hMRQwEgYDVQQKEwtIeXBl\\ncmxlZGdlcjENMAsGA1UECxMEcGVlcjETMBEGA1UEAxMKb3JnMS1wZWVyMTBZMBMG\\nByqGSM49AgEGCCqGSM49AwEHA0IABL9e3GZBf1MeoObGxwSHkcgDEjMo+/13Qc4u\\nfSG2MKrveHBIEA4MRkHNqd+sTjoz0/1B15y2n+RiPo8uJvlyC/CjgfQwgfEwDgYD\\nVR0PAQH/BAQDAgOoMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAMBgNV\\nHRMBAf8EAjAAMB0GA1UdDgQWBBSeytspiXlEzMAsnF9/wxqc9fydETAfBgNVHSME\\nGDAWgBQwru1VH0OwH3dxfPdD8w74ZIlLRzAVBgNVHREEDjAMggpvcmcxLXBlZXIx\\nMFsGCCoDBAUGBwgBBE97ImF0dHJzIjp7ImhmLkFmZmlsaWF0aW9uIjoiIiwiaGYu\\nRW5yb2xsbWVudElEIjoib3JnMS1wZWVyMSIsImhmLlR5cGUiOiJwZWVyIn19MAoG\\nCCqGSM49BAMCA0gAMEUCIQDJEjPxceCfXU5B/emrHE4JbEzrZKxLVViBWCNMsHiR\\nFgIgY+8jsvr3rlBPkpRhl8CtT2DgaP7iWvovtMYsPKhLAqk=\\n-----END CERTIFICATE-----\\n" + }, + "grpcOptions": { + "grpc-wait-for-ready-timeout": 100000, + "ssl-target-name-override": "org1-peer1", + "hostnameOverride": "org1-peer1" + } + } + }, + "certificateAuthorities": { + "org1-ecert-ca": { + "url": "https://org1-ecert-ca", + "caName": "org1-ecert-ca", + "tlsCACerts": { + "pem": "TODO" + }, + "httpOptions": { + "verify": "false" + } + } + } + } + HLF_CERTIFICATE_ORG1: | + -----BEGIN CERTIFICATE----- + MIIC2DCCAn6gAwIBAgIUTfcXDyxCS+2EQnznfjERUo4Vri8wCgYIKoZIzj0EAwIw + aDELMAkGA1UEBhMCVVMxFzAVBgNVBAgTDk5vcnRoIENhcm9saW5hMRQwEgYDVQQK + EwtIeXBlcmxlZGdlcjEPMA0GA1UECxMGRmFicmljMRkwFwYDVQQDExBmYWJyaWMt + Y2Etc2VydmVyMB4XDTIxMDkyMDExNDEwMFoXDTIyMDkyMDExNDYwMFowYTELMAkG + A1UEBhMCVVMxFzAVBgNVBAgTDk5vcnRoIENhcm9saW5hMRQwEgYDVQQKEwtIeXBl + cmxlZGdlcjEOMAwGA1UECxMFYWRtaW4xEzARBgNVBAMTCm9yZzEtYWRtaW4wWTAT + BgcqhkjOPQIBBggqhkjOPQMBBwNCAAT8zvJEg3FgJ5iUA5GO+n/j48bL83STpz7N + TqejWIZNVTraxE4fjT6traKiswme7gT2NY9Jl0Dj4tbif9l2I9+Oo4IBCzCCAQcw + DgYDVR0PAQH/BAQDAgeAMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFO1zWPynvyER + n9ml6XV5VvC9tIjTMB8GA1UdIwQYMBaAFPbIrI+lh8KayoRpW1YStWMhzJZSMCcG + A1UdEQQgMB6CHG9yZzEtdGxzLWNhLTg1NjdiOTg5OWYtdzU3amYwfgYIKgMEBQYH + CAEEcnsiYXR0cnMiOnsiYWJhYy5pbml0IjoidHJ1ZSIsImFkbWluIjoidHJ1ZSIs + ImhmLkFmZmlsaWF0aW9uIjoiIiwiaGYuRW5yb2xsbWVudElEIjoib3JnMS1hZG1p + biIsImhmLlR5cGUiOiJhZG1pbiJ9fTAKBggqhkjOPQQDAgNIADBFAiEAv99I2J9t + WtOmIzpYix8OFl4Z+ZGRHtay83ux//sZP+MCID02hFqnNpOL/ggGFaDVpVQ/eu0t + KTfVxZEMyZnJtAhp + -----END CERTIFICATE----- + HLF_PRIVATE_KEY_ORG1: | + -----BEGIN PRIVATE KEY----- + MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg7Lb+jwZqhxT3x0lT + FpU0QSmioptgfv8TI2RP5Mjp9UKhRANCAAT8zvJEg3FgJ5iUA5GO+n/j48bL83ST + pz7NTqejWIZNVTraxE4fjT6traKiswme7gT2NY9Jl0Dj4tbif9l2I9+O + -----END PRIVATE KEY----- + HLF_CONNECTION_PROFILE_ORG2: | + { + "name": "test-network-org2", + "version": "1.0.0", + "client": { + "organization": "Org2", + "connection": { + "timeout": { + "peer": { + "endorser": "300" + } + } + } + }, + "organizations": { + "Org2": { + "mspid": "Org2MSP", + "peers": [ + "org2-peer1" + ], + "certificateAuthorities": [ + "org2-ecert-ca" + ] + } + }, + "peers": { + "org2-peer1": { + "url": "grpcs://org2-peer1:7051", + "tlsCACerts": { + "pem": "-----BEGIN CERTIFICATE-----\\nMIICKDCCAc6gAwIBAgIUJJ4wGOSCfw8XOOIx29o67wBpFB4wCgYIKoZIzj0EAwIw\\naDELMAkGA1UEBhMCVVMxFzAVBgNVBAgTDk5vcnRoIENhcm9saW5hMRQwEgYDVQQK\\nEwtIeXBlcmxlZGdlcjEPMA0GA1UECxMGRmFicmljMRkwFwYDVQQDExBmYWJyaWMt\\nY2Etc2VydmVyMB4XDTIxMDkyMDExNDEwMFoXDTM2MDkxNjExNDEwMFowaDELMAkG\\nA1UEBhMCVVMxFzAVBgNVBAgTDk5vcnRoIENhcm9saW5hMRQwEgYDVQQKEwtIeXBl\\ncmxlZGdlcjEPMA0GA1UECxMGRmFicmljMRkwFwYDVQQDExBmYWJyaWMtY2Etc2Vy\\ndmVyMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEyzGJLZX6pe59QAIBacjfzU4I\\nHezBYLyEu4ySpFx4xwxNLE4BWqLhB1VaOuenSQATM8pmSAy7i1830oM9elKWK6NW\\nMFQwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQEwHQYDVR0OBBYE\\nFEoAAhmjq/3M8CFPc7N8SL53erL5MA8GA1UdEQQIMAaHBH8AAAEwCgYIKoZIzj0E\\nAwIDSAAwRQIhAJQ5PJOT4Gg8oiBU2KthMPkZqOLeu3Li4S3yBpLFgbsgAiB960P2\\nXPMu3HLoNXrktYOL9JzWlGyYRSPAnkap5Bsj0w==\\n-----END CERTIFICATE-----\\n" + }, + "grpcOptions": { + "ssl-target-name-override": "org2-peer1", + "hostnameOverride": "org2-peer1" + } + } + }, + "certificateAuthorities": { + "org2-ecert-ca": { + "url": "https://org2-ecert-ca", + "caName": "org2-ecert-ca", + "tlsCACerts": { + "pem": ["-----BEGIN CERTIFICATE-----\\nMIICKDCCAc6gAwIBAgIUJAF4fQK1KsnvdaUjau462D/5HPYwCgYIKoZIzj0EAwIw\\naDELMAkGA1UEBhMCVVMxFzAVBgNVBAgTDk5vcnRoIENhcm9saW5hMRQwEgYDVQQK\\nEwtIeXBlcmxlZGdlcjEPMA0GA1UECxMGRmFicmljMRkwFwYDVQQDExBmYWJyaWMt\\nY2Etc2VydmVyMB4XDTIxMDkxOTExMTcwMFoXDTM2MDkxNTExMTcwMFowaDELMAkG\\nA1UEBhMCVVMxFzAVBgNVBAgTDk5vcnRoIENhcm9saW5hMRQwEgYDVQQKEwtIeXBl\\ncmxlZGdlcjEPMA0GA1UECxMGRmFicmljMRkwFwYDVQQDExBmYWJyaWMtY2Etc2Vy\\ndmVyMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE8bLvzagP3YANMGHVomZoGCQD\\nRgM3SenagZQ4IWqNQJSV3yTxzdgAWnPhwc+B/HdAOvAq2Oz54FmiSL9dAJoivqNW\\nMFQwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQEwHQYDVR0OBBYE\\nFDdBAwT47jtbj48aXdMfRvMPbD5tMA8GA1UdEQQIMAaHBH8AAAEwCgYIKoZIzj0E\\nAwIDSAAwRQIhAITSk4lYWqu12jZkR94aNoKT36ctaeKHuRvXs7m2qaHSAiAtUPO7\\nXlHtI9SDTRvI4DNSb2O7y7+B3WxVeCx50fivDw==\\n-----END CERTIFICATE-----\\n"] + }, + "httpOptions": { + "verify": "false" + } + } + } + } + HLF_CERTIFICATE_ORG2: | + -----BEGIN CERTIFICATE----- + MIIC2DCCAn6gAwIBAgIUY/B19uAV6H5zK4bgqF/BcYC79eEwCgYIKoZIzj0EAwIw + aDELMAkGA1UEBhMCVVMxFzAVBgNVBAgTDk5vcnRoIENhcm9saW5hMRQwEgYDVQQK + EwtIeXBlcmxlZGdlcjEPMA0GA1UECxMGRmFicmljMRkwFwYDVQQDExBmYWJyaWMt + Y2Etc2VydmVyMB4XDTIxMDkyMDExNDEwMFoXDTIyMDkyMDExNDYwMFowYTELMAkG + A1UEBhMCVVMxFzAVBgNVBAgTDk5vcnRoIENhcm9saW5hMRQwEgYDVQQKEwtIeXBl + cmxlZGdlcjEOMAwGA1UECxMFYWRtaW4xEzARBgNVBAMTCm9yZzItYWRtaW4wWTAT + BgcqhkjOPQIBBggqhkjOPQMBBwNCAARKTC+25gFIgbLQgSQSec3DaUJOZS6aHBAi + 0bmArVbMOxLUBT/W42ycXzfFJ9c0UAEZecDu8jxgBfEGWcbeWWMXo4IBCzCCAQcw + DgYDVR0PAQH/BAQDAgeAMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFGIXcrVhcyJ9 + WTH2zgc9/RdE1hJsMB8GA1UdIwQYMBaAFFS96ExY5RWOcsODBAfXZe+FQIq0MCcG + A1UdEQQgMB6CHG9yZzItdGxzLWNhLTY5Yzg1Zjg5YmMtNzIyZ2cwfgYIKgMEBQYH + CAEEcnsiYXR0cnMiOnsiYWJhYy5pbml0IjoidHJ1ZSIsImFkbWluIjoidHJ1ZSIs + ImhmLkFmZmlsaWF0aW9uIjoiIiwiaGYuRW5yb2xsbWVudElEIjoib3JnMi1hZG1p + biIsImhmLlR5cGUiOiJhZG1pbiJ9fTAKBggqhkjOPQQDAgNIADBFAiEAhrXwM7Ng + IGxgF8irY7NbkQp1xqphy3tv6JbK6HPF+O8CIELMkzOclVK2rRC1K5PF99G7Cmmm + KsVw31cJcV4NTDI7 + -----END CERTIFICATE----- + HLF_PRIVATE_KEY_ORG2: | + -----BEGIN PRIVATE KEY----- + MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgPJgLniT9EqcaUNbi + F3EqGyBP9LDg1baXR/5dV6xedt+hRANCAARKTC+25gFIgbLQgSQSec3DaUJOZS6a + HBAi0bmArVbMOxLUBT/W42ycXzfFJ9c0UAEZecDu8jxgBfEGWcbeWWMX + -----END PRIVATE KEY----- + + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: fabric-rest-sample +spec: + replicas: 1 + selector: + matchLabels: + app: fabric-rest-sample + template: + metadata: + labels: + app: fabric-rest-sample + spec: + containers: + - name: main + image: ghcr.io/hyperledgendary/fabric-rest-sample + imagePullPolicy: IfNotPresent + env: + - name: LOG_LEVEL + value: debug + - name: HFC_LOGGING + value: '{ "debug": "console" }' + - name: PORT + value: "3000" + - name: RETRY_DELAY + value: "3000" + - name: MAX_RETRY_COUNT + value: "5" + - name: HLF_COMMIT_TIMEOUT + value: "3000" + - name: HLF_ENDORSE_TIMEOUT + value: "30" + - name: REDIS_HOST + value: "localhost" + - name: REDIS_PORT + value: "6379" + - name: ORG1_APIKEY + value: "97834158-3224-4CE7-95F9-A148C886653E" + - name: ORG2_APIKEY + value: "BC42E734-062D-4AEE-A591-5973CB763430" + - name: AS_LOCAL_HOST + value: "false" + - name: HLF_CHAINCODE_NAME + value: "asset-transfer-basic" +# - name: REDIS_USERNAME +# value: redisuser +# - name: REDIS_PASSWORD +# value: redispasword + + envFrom: + - configMapRef: + name: fabric-rest-sample-config + ports: + - containerPort: 3000 + - name: redis + image: redis:6.2.5 + ports: + - containerPort: 6379 + +--- +apiVersion: v1 +kind: Service +metadata: + name: fabric-rest-sample +spec: + ports: + - name: http + port: 3000 + protocol: TCP + selector: + app: fabric-rest-sample + + +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: fabric-rest-sample +# annotations: +# nginx.ingress.kubernetes.io/rewrite-target: /$1 +spec: + rules: + - http: + paths: +# - path: "/fabric-rest-sample/(.*)" + - path: "/" + pathType: Prefix + backend: + service: + name: fabric-rest-sample + port: + number: 3000 diff --git a/test-network-k8s/kube/ingress-nginx.yaml b/test-network-k8s/kube/ingress-nginx.yaml new file mode 100644 index 00000000..90f5de31 --- /dev/null +++ b/test-network-k8s/kube/ingress-nginx.yaml @@ -0,0 +1,687 @@ + +apiVersion: v1 +kind: Namespace +metadata: + name: ingress-nginx + labels: + app.kubernetes.io/name: ingress-nginx + app.kubernetes.io/instance: ingress-nginx + +--- +# Source: ingress-nginx/templates/controller-serviceaccount.yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + labels: + helm.sh/chart: ingress-nginx-4.0.2 + app.kubernetes.io/name: ingress-nginx + app.kubernetes.io/instance: ingress-nginx + app.kubernetes.io/version: 1.0.1 + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/component: controller + name: ingress-nginx + namespace: ingress-nginx +automountServiceAccountToken: true +--- +# Source: ingress-nginx/templates/controller-configmap.yaml +apiVersion: v1 +kind: ConfigMap +metadata: + labels: + helm.sh/chart: ingress-nginx-4.0.2 + app.kubernetes.io/name: ingress-nginx + app.kubernetes.io/instance: ingress-nginx + app.kubernetes.io/version: 1.0.1 + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/component: controller + name: ingress-nginx-controller + namespace: ingress-nginx +data: +--- +# Source: ingress-nginx/templates/clusterrole.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + helm.sh/chart: ingress-nginx-4.0.2 + app.kubernetes.io/name: ingress-nginx + app.kubernetes.io/instance: ingress-nginx + app.kubernetes.io/version: 1.0.1 + app.kubernetes.io/managed-by: Helm + name: ingress-nginx +rules: + - apiGroups: + - '' + resources: + - configmaps + - endpoints + - nodes + - pods + - secrets + verbs: + - list + - watch + - apiGroups: + - '' + resources: + - nodes + verbs: + - get + - apiGroups: + - '' + resources: + - services + verbs: + - get + - list + - watch + - apiGroups: + - networking.k8s.io + resources: + - ingresses + verbs: + - get + - list + - watch + - apiGroups: + - '' + resources: + - events + verbs: + - create + - patch + - apiGroups: + - networking.k8s.io + resources: + - ingresses/status + verbs: + - update + - apiGroups: + - networking.k8s.io + resources: + - ingressclasses + verbs: + - get + - list + - watch +--- +# Source: ingress-nginx/templates/clusterrolebinding.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + labels: + helm.sh/chart: ingress-nginx-4.0.2 + app.kubernetes.io/name: ingress-nginx + app.kubernetes.io/instance: ingress-nginx + app.kubernetes.io/version: 1.0.1 + app.kubernetes.io/managed-by: Helm + name: ingress-nginx +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: ingress-nginx +subjects: + - kind: ServiceAccount + name: ingress-nginx + namespace: ingress-nginx +--- +# Source: ingress-nginx/templates/controller-role.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + labels: + helm.sh/chart: ingress-nginx-4.0.2 + app.kubernetes.io/name: ingress-nginx + app.kubernetes.io/instance: ingress-nginx + app.kubernetes.io/version: 1.0.1 + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/component: controller + name: ingress-nginx + namespace: ingress-nginx +rules: + - apiGroups: + - '' + resources: + - namespaces + verbs: + - get + - apiGroups: + - '' + resources: + - configmaps + - pods + - secrets + - endpoints + verbs: + - get + - list + - watch + - apiGroups: + - '' + resources: + - services + verbs: + - get + - list + - watch + - apiGroups: + - networking.k8s.io + resources: + - ingresses + verbs: + - get + - list + - watch + - apiGroups: + - networking.k8s.io + resources: + - ingresses/status + verbs: + - update + - apiGroups: + - networking.k8s.io + resources: + - ingressclasses + verbs: + - get + - list + - watch + - apiGroups: + - '' + resources: + - configmaps + resourceNames: + - ingress-controller-leader + verbs: + - get + - update + - apiGroups: + - '' + resources: + - configmaps + verbs: + - create + - apiGroups: + - '' + resources: + - events + verbs: + - create + - patch +--- +# Source: ingress-nginx/templates/controller-rolebinding.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + labels: + helm.sh/chart: ingress-nginx-4.0.2 + app.kubernetes.io/name: ingress-nginx + app.kubernetes.io/instance: ingress-nginx + app.kubernetes.io/version: 1.0.1 + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/component: controller + name: ingress-nginx + namespace: ingress-nginx +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: ingress-nginx +subjects: + - kind: ServiceAccount + name: ingress-nginx + namespace: ingress-nginx +--- +# Source: ingress-nginx/templates/controller-service-webhook.yaml +apiVersion: v1 +kind: Service +metadata: + labels: + helm.sh/chart: ingress-nginx-4.0.2 + app.kubernetes.io/name: ingress-nginx + app.kubernetes.io/instance: ingress-nginx + app.kubernetes.io/version: 1.0.1 + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/component: controller + name: ingress-nginx-controller-admission + namespace: ingress-nginx +spec: + type: ClusterIP + ports: + - name: https-webhook + port: 443 + targetPort: webhook + appProtocol: https + selector: + app.kubernetes.io/name: ingress-nginx + app.kubernetes.io/instance: ingress-nginx + app.kubernetes.io/component: controller +--- +# Source: ingress-nginx/templates/controller-service.yaml +apiVersion: v1 +kind: Service +metadata: + annotations: + labels: + helm.sh/chart: ingress-nginx-4.0.2 + app.kubernetes.io/name: ingress-nginx + app.kubernetes.io/instance: ingress-nginx + app.kubernetes.io/version: 1.0.1 + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/component: controller + name: ingress-nginx-controller + namespace: ingress-nginx +spec: + type: NodePort + ports: + - name: http + port: 80 + protocol: TCP + targetPort: http + appProtocol: http + - name: https + port: 443 + protocol: TCP + targetPort: https + appProtocol: https + selector: + app.kubernetes.io/name: ingress-nginx + app.kubernetes.io/instance: ingress-nginx + app.kubernetes.io/component: controller +--- +# Source: ingress-nginx/templates/controller-deployment.yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + helm.sh/chart: ingress-nginx-4.0.2 + app.kubernetes.io/name: ingress-nginx + app.kubernetes.io/instance: ingress-nginx + app.kubernetes.io/version: 1.0.1 + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/component: controller + name: ingress-nginx-controller + namespace: ingress-nginx +spec: + selector: + matchLabels: + app.kubernetes.io/name: ingress-nginx + app.kubernetes.io/instance: ingress-nginx + app.kubernetes.io/component: controller + revisionHistoryLimit: 10 + strategy: + rollingUpdate: + maxUnavailable: 1 + type: RollingUpdate + minReadySeconds: 0 + template: + metadata: + labels: + app.kubernetes.io/name: ingress-nginx + app.kubernetes.io/instance: ingress-nginx + app.kubernetes.io/component: controller + spec: + dnsPolicy: ClusterFirst + containers: + - name: controller + image: k8s.gcr.io/ingress-nginx/controller:v1.0.1@sha256:26bbd57f32bac3b30f90373005ef669aae324a4de4c19588a13ddba399c6664e + imagePullPolicy: IfNotPresent + lifecycle: + preStop: + exec: + command: + - /wait-shutdown + args: + - /nginx-ingress-controller + - --election-id=ingress-controller-leader + - --controller-class=k8s.io/ingress-nginx + - --configmap=$(POD_NAMESPACE)/ingress-nginx-controller + - --validating-webhook=:8443 + - --validating-webhook-certificate=/usr/local/certificates/cert + - --validating-webhook-key=/usr/local/certificates/key + - --publish-status-address=localhost + - --watch-ingress-without-class + - --enable-ssl-passthrough + securityContext: + capabilities: + drop: + - ALL + add: + - NET_BIND_SERVICE + runAsUser: 101 + allowPrivilegeEscalation: true + env: + - name: POD_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name + - name: POD_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: LD_PRELOAD + value: /usr/local/lib/libmimalloc.so + livenessProbe: + failureThreshold: 5 + httpGet: + path: /healthz + port: 10254 + scheme: HTTP + initialDelaySeconds: 10 + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 + readinessProbe: + failureThreshold: 3 + httpGet: + path: /healthz + port: 10254 + scheme: HTTP + initialDelaySeconds: 10 + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 + ports: + - name: http + containerPort: 80 + protocol: TCP + hostPort: 80 + - name: https + containerPort: 443 + protocol: TCP + hostPort: 443 + - name: webhook + containerPort: 8443 + protocol: TCP + volumeMounts: + - name: webhook-cert + mountPath: /usr/local/certificates/ + readOnly: true + resources: + requests: + cpu: 100m + memory: 90Mi + nodeSelector: + ingress-ready: 'true' + kubernetes.io/os: linux + tolerations: + - effect: NoSchedule + key: node-role.kubernetes.io/master + operator: Equal + serviceAccountName: ingress-nginx + terminationGracePeriodSeconds: 0 + volumes: + - name: webhook-cert + secret: + secretName: ingress-nginx-admission +--- +# Source: ingress-nginx/templates/controller-ingressclass.yaml +# We don't support namespaced ingressClass yet +# So a ClusterRole and a ClusterRoleBinding is required +apiVersion: networking.k8s.io/v1 +kind: IngressClass +metadata: + labels: + helm.sh/chart: ingress-nginx-4.0.2 + app.kubernetes.io/name: ingress-nginx + app.kubernetes.io/instance: ingress-nginx + app.kubernetes.io/version: 1.0.1 + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/component: controller + name: nginx + namespace: ingress-nginx +spec: + controller: k8s.io/ingress-nginx +--- +# Source: ingress-nginx/templates/admission-webhooks/validating-webhook.yaml +# before changing this value, check the required kubernetes version +# https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#prerequisites +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingWebhookConfiguration +metadata: + labels: + helm.sh/chart: ingress-nginx-4.0.2 + app.kubernetes.io/name: ingress-nginx + app.kubernetes.io/instance: ingress-nginx + app.kubernetes.io/version: 1.0.1 + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/component: admission-webhook + name: ingress-nginx-admission +webhooks: + - name: validate.nginx.ingress.kubernetes.io + matchPolicy: Equivalent + rules: + - apiGroups: + - networking.k8s.io + apiVersions: + - v1 + operations: + - CREATE + - UPDATE + resources: + - ingresses + failurePolicy: Fail + sideEffects: None + admissionReviewVersions: + - v1 + clientConfig: + service: + namespace: ingress-nginx + name: ingress-nginx-controller-admission + path: /networking/v1/ingresses +--- +# Source: ingress-nginx/templates/admission-webhooks/job-patch/serviceaccount.yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + name: ingress-nginx-admission + namespace: ingress-nginx + annotations: + helm.sh/hook: pre-install,pre-upgrade,post-install,post-upgrade + helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded + labels: + helm.sh/chart: ingress-nginx-4.0.2 + app.kubernetes.io/name: ingress-nginx + app.kubernetes.io/instance: ingress-nginx + app.kubernetes.io/version: 1.0.1 + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/component: admission-webhook +--- +# Source: ingress-nginx/templates/admission-webhooks/job-patch/clusterrole.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: ingress-nginx-admission + annotations: + helm.sh/hook: pre-install,pre-upgrade,post-install,post-upgrade + helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded + labels: + helm.sh/chart: ingress-nginx-4.0.2 + app.kubernetes.io/name: ingress-nginx + app.kubernetes.io/instance: ingress-nginx + app.kubernetes.io/version: 1.0.1 + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/component: admission-webhook +rules: + - apiGroups: + - admissionregistration.k8s.io + resources: + - validatingwebhookconfigurations + verbs: + - get + - update +--- +# Source: ingress-nginx/templates/admission-webhooks/job-patch/clusterrolebinding.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: ingress-nginx-admission + annotations: + helm.sh/hook: pre-install,pre-upgrade,post-install,post-upgrade + helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded + labels: + helm.sh/chart: ingress-nginx-4.0.2 + app.kubernetes.io/name: ingress-nginx + app.kubernetes.io/instance: ingress-nginx + app.kubernetes.io/version: 1.0.1 + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/component: admission-webhook +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: ingress-nginx-admission +subjects: + - kind: ServiceAccount + name: ingress-nginx-admission + namespace: ingress-nginx +--- +# Source: ingress-nginx/templates/admission-webhooks/job-patch/role.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: ingress-nginx-admission + namespace: ingress-nginx + annotations: + helm.sh/hook: pre-install,pre-upgrade,post-install,post-upgrade + helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded + labels: + helm.sh/chart: ingress-nginx-4.0.2 + app.kubernetes.io/name: ingress-nginx + app.kubernetes.io/instance: ingress-nginx + app.kubernetes.io/version: 1.0.1 + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/component: admission-webhook +rules: + - apiGroups: + - '' + resources: + - secrets + verbs: + - get + - create +--- +# Source: ingress-nginx/templates/admission-webhooks/job-patch/rolebinding.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: ingress-nginx-admission + namespace: ingress-nginx + annotations: + helm.sh/hook: pre-install,pre-upgrade,post-install,post-upgrade + helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded + labels: + helm.sh/chart: ingress-nginx-4.0.2 + app.kubernetes.io/name: ingress-nginx + app.kubernetes.io/instance: ingress-nginx + app.kubernetes.io/version: 1.0.1 + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/component: admission-webhook +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: ingress-nginx-admission +subjects: + - kind: ServiceAccount + name: ingress-nginx-admission + namespace: ingress-nginx +--- +# Source: ingress-nginx/templates/admission-webhooks/job-patch/job-createSecret.yaml +apiVersion: batch/v1 +kind: Job +metadata: + name: ingress-nginx-admission-create + namespace: ingress-nginx + annotations: + helm.sh/hook: pre-install,pre-upgrade + helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded + labels: + helm.sh/chart: ingress-nginx-4.0.2 + app.kubernetes.io/name: ingress-nginx + app.kubernetes.io/instance: ingress-nginx + app.kubernetes.io/version: 1.0.1 + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/component: admission-webhook +spec: + template: + metadata: + name: ingress-nginx-admission-create + labels: + helm.sh/chart: ingress-nginx-4.0.2 + app.kubernetes.io/name: ingress-nginx + app.kubernetes.io/instance: ingress-nginx + app.kubernetes.io/version: 1.0.1 + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/component: admission-webhook + spec: + containers: + - name: create + image: k8s.gcr.io/ingress-nginx/kube-webhook-certgen:v1.0@sha256:f3b6b39a6062328c095337b4cadcefd1612348fdd5190b1dcbcb9b9e90bd8068 + imagePullPolicy: IfNotPresent + args: + - create + - --host=ingress-nginx-controller-admission,ingress-nginx-controller-admission.$(POD_NAMESPACE).svc + - --namespace=$(POD_NAMESPACE) + - --secret-name=ingress-nginx-admission + env: + - name: POD_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + restartPolicy: OnFailure + serviceAccountName: ingress-nginx-admission + nodeSelector: + kubernetes.io/os: linux + securityContext: + runAsNonRoot: true + runAsUser: 2000 +--- +# Source: ingress-nginx/templates/admission-webhooks/job-patch/job-patchWebhook.yaml +apiVersion: batch/v1 +kind: Job +metadata: + name: ingress-nginx-admission-patch + namespace: ingress-nginx + annotations: + helm.sh/hook: post-install,post-upgrade + helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded + labels: + helm.sh/chart: ingress-nginx-4.0.2 + app.kubernetes.io/name: ingress-nginx + app.kubernetes.io/instance: ingress-nginx + app.kubernetes.io/version: 1.0.1 + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/component: admission-webhook +spec: + template: + metadata: + name: ingress-nginx-admission-patch + labels: + helm.sh/chart: ingress-nginx-4.0.2 + app.kubernetes.io/name: ingress-nginx + app.kubernetes.io/instance: ingress-nginx + app.kubernetes.io/version: 1.0.1 + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/component: admission-webhook + spec: + containers: + - name: patch + image: k8s.gcr.io/ingress-nginx/kube-webhook-certgen:v1.0@sha256:f3b6b39a6062328c095337b4cadcefd1612348fdd5190b1dcbcb9b9e90bd8068 + imagePullPolicy: IfNotPresent + args: + - patch + - --webhook-name=ingress-nginx-admission + - --namespace=$(POD_NAMESPACE) + - --patch-mutating=false + - --secret-name=ingress-nginx-admission + - --patch-failure-policy=Fail + env: + - name: POD_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + restartPolicy: OnFailure + serviceAccountName: ingress-nginx-admission + nodeSelector: + kubernetes.io/os: linux + securityContext: + runAsNonRoot: true + runAsUser: 2000 diff --git a/test-network-k8s/kube/job-scrub-fabric-volumes.yaml b/test-network-k8s/kube/job-scrub-fabric-volumes.yaml new file mode 100644 index 00000000..2acddc92 --- /dev/null +++ b/test-network-k8s/kube/job-scrub-fabric-volumes.yaml @@ -0,0 +1,43 @@ +# +# Copyright IBM Corp. All Rights Reserved. +# +# SPDX-License-Identifier: Apache-2.0 +# +--- +apiVersion: batch/v1 +kind: Job +metadata: + name: job-scrub-fabric-volumes +spec: + backoffLimit: 0 + completions: 1 + template: + metadata: + name: job-scrub-fabric-volumes + spec: + restartPolicy: "Never" + containers: + - name: main + image: busybox + command: + - sh + - -c + - "rm -rvf /mnt/fabric-*/*" + volumeMounts: + - name: fabric-org0-volume + mountPath: /mnt/fabric-org0 + - name: fabric-org1-volume + mountPath: /mnt/fabric-org1 + - name: fabric-org2-volume + mountPath: /mnt/fabric-org2 + volumes: + - name: fabric-org0-volume + persistentVolumeClaim: + claimName: fabric-org0 + - name: fabric-org1-volume + persistentVolumeClaim: + claimName: fabric-org1 + - name: fabric-org2-volume + persistentVolumeClaim: + claimName: fabric-org2 + diff --git a/test-network-k8s/kube/ns-test-network.yaml b/test-network-k8s/kube/ns-test-network.yaml new file mode 100644 index 00000000..f9ef39e0 --- /dev/null +++ b/test-network-k8s/kube/ns-test-network.yaml @@ -0,0 +1,10 @@ +# +# Copyright IBM Corp. All Rights Reserved. +# +# SPDX-License-Identifier: Apache-2.0 +# +--- +apiVersion: v1 +kind: Namespace +metadata: + name: test-network diff --git a/test-network-k8s/kube/org0/org0-admin-cli.yaml b/test-network-k8s/kube/org0/org0-admin-cli.yaml new file mode 100644 index 00000000..c7bf84bb --- /dev/null +++ b/test-network-k8s/kube/org0/org0-admin-cli.yaml @@ -0,0 +1,61 @@ +# +# Copyright IBM Corp. All Rights Reserved. +# +# SPDX-License-Identifier: Apache-2.0 +# +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: org0-admin-cli +spec: + replicas: 1 + selector: + matchLabels: + app: org0-admin-cli + template: + metadata: + labels: + app: org0-admin-cli + spec: + containers: + - name: main + image: hyperledger/fabric-tools:{{FABRIC_VERSION}} + imagePullPolicy: IfNotPresent + env: + - name: FABRIC_CFG_PATH + value: /var/hyperledger/fabric/config + args: + - sleep + - "2147483647" + workingDir: /root + volumeMounts: + - name: fabric-volume + mountPath: /var/hyperledger + - name: fabric-config + mountPath: /var/hyperledger/fabric/config + + # This init container will unfurl all of the MSP archives listed in the msp-config config map. + initContainers: + - name: msp-unfurl + image: busybox + command: + - sh + - -c + - "for msp in $(ls /msp/msp-*.tgz); do echo $msp && tar zxvf $msp -C /var/hyperledger/fabric; done" + volumeMounts: + - name: msp-config + mountPath: /msp + - name: fabric-volume + mountPath: /var/hyperledger + + volumes: + - name: fabric-volume + persistentVolumeClaim: + claimName: fabric-org0 + - name: fabric-config + configMap: + name: org0-config + - name: msp-config + configMap: + name: msp-config diff --git a/test-network-k8s/kube/org0/org0-ecert-ca.yaml b/test-network-k8s/kube/org0/org0-ecert-ca.yaml new file mode 100644 index 00000000..40c2edeb --- /dev/null +++ b/test-network-k8s/kube/org0/org0-ecert-ca.yaml @@ -0,0 +1,69 @@ +# +# Copyright IBM Corp. All Rights Reserved. +# +# SPDX-License-Identifier: Apache-2.0 +# +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: org0-ecert-ca +spec: + replicas: 1 + selector: + matchLabels: + app: org0-ecert-ca + template: + metadata: + labels: + app: org0-ecert-ca + spec: + containers: + - name: main + image: hyperledger/fabric-ca:1.5.2 + env: + - name: FABRIC_CA_SERVER_CA_NAME + value: "org0-ecert-ca" + - name: FABRIC_CA_SERVER_DEBUG + value: "false" + - name: FABRIC_CA_SERVER_HOME + value: "/var/hyperledger/fabric-ca-server" + - name: FABRIC_CA_SERVER_TLS_CERTFILE + value: "/var/hyperledger/fabric-ca-client/tls-ca/rcaadmin/msp/signcerts/cert.pem" + - name: FABRIC_CA_SERVER_TLS_KEYFILE + value: "/var/hyperledger/fabric-ca-client/tls-ca/rcaadmin/msp/keystore/key.pem" + - name: FABRIC_CA_CLIENT_HOME + value: "/var/hyperledger/fabric-ca-client" + ports: + - containerPort: 443 + volumeMounts: + - name: fabric-volume + mountPath: /var/hyperledger + - name: fabric-config + mountPath: /var/hyperledger/fabric-ca-server/fabric-ca-server-config.yaml + subPath: fabric-ecert-ca-server-config.yaml + readinessProbe: + tcpSocket: + port: 443 + initialDelaySeconds: 2 + periodSeconds: 5 + volumes: + - name: fabric-volume + persistentVolumeClaim: + claimName: fabric-org0 + - name: fabric-config + configMap: + name: org0-config + +--- +apiVersion: v1 +kind: Service +metadata: + name: org0-ecert-ca +spec: + ports: + - name: tls + port: 443 + protocol: TCP + selector: + app: org0-ecert-ca \ No newline at end of file diff --git a/test-network-k8s/kube/org0/org0-orderer1.yaml b/test-network-k8s/kube/org0/org0-orderer1.yaml new file mode 100644 index 00000000..a37b274d --- /dev/null +++ b/test-network-k8s/kube/org0/org0-orderer1.yaml @@ -0,0 +1,85 @@ +# +# Copyright IBM Corp. All Rights Reserved. +# +# SPDX-License-Identifier: Apache-2.0 +# +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: org0-orderer1-env +data: + FABRIC_CFG_PATH: /var/hyperledger/fabric/config + FABRIC_LOGGING_SPEC: INFO # debug:cauthdsl,policies,msp,common.configtx,common.channelconfig=info + ORDERER_GENERAL_LISTENADDRESS: 0.0.0.0 + ORDERER_GENERAL_LISTENPORT: "6050" + ORDERER_GENERAL_LOCALMSPID: OrdererMSP + ORDERER_GENERAL_LOCALMSPDIR: /var/hyperledger/fabric/organizations/ordererOrganizations/org0.example.com/orderers/org0-orderer1.org0.example.com/msp + ORDERER_GENERAL_TLS_ENABLED: "true" + ORDERER_GENERAL_TLS_CERTIFICATE: /var/hyperledger/fabric/organizations/ordererOrganizations/org0.example.com/orderers/org0-orderer1.org0.example.com/tls/signcerts/cert.pem + ORDERER_GENERAL_TLS_ROOTCAS: /var/hyperledger/fabric/organizations/ordererOrganizations/org0.example.com/orderers/org0-orderer1.org0.example.com/tls/cacerts/org0-tls-ca.pem + ORDERER_GENERAL_TLS_PRIVATEKEY: /var/hyperledger/fabric/organizations/ordererOrganizations/org0.example.com/orderers/org0-orderer1.org0.example.com/tls/keystore/server.key + ORDERER_GENERAL_BOOTSTRAPMETHOD: none + ORDERER_FILELEDGER_LOCATION: /var/hyperledger/fabric/data/orderer1 + ORDERER_CONSENSUS_WALDIR: /var/hyperledger/fabric/data/orderer1/etcdraft/wal + ORDERER_CONSENSUS_SNAPDIR: /var/hyperledger/fabric/data/orderer1/etcdraft/wal + ORDERER_OPERATIONS_LISTENADDRESS: 0.0.0.0:8443 + ORDERER_ADMIN_LISTENADDRESS: 0.0.0.0:9443 + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: org0-orderer1 +spec: + replicas: 1 + selector: + matchLabels: + app: org0-orderer1 + template: + metadata: + labels: + app: org0-orderer1 + spec: + containers: + - name: main + image: hyperledger/fabric-orderer:{{FABRIC_VERSION}} + imagePullPolicy: IfNotPresent + envFrom: + - configMapRef: + name: org0-orderer1-env + ports: + - containerPort: 6050 + - containerPort: 8443 + - containerPort: 9443 + volumeMounts: + - name: fabric-volume + mountPath: /var/hyperledger + - name: fabric-config + mountPath: /var/hyperledger/fabric/config + volumes: + - name: fabric-volume + persistentVolumeClaim: + claimName: fabric-org0 + - name: fabric-config + configMap: + name: org0-config + +--- +apiVersion: v1 +kind: Service +metadata: + name: org0-orderer1 +spec: + ports: + - name: general + port: 6050 + protocol: TCP + - name: operations + port: 8443 + protocol: TCP + - name: admin + port: 9443 + protocol: TCP + selector: + app: org0-orderer1 \ No newline at end of file diff --git a/test-network-k8s/kube/org0/org0-orderer2.yaml b/test-network-k8s/kube/org0/org0-orderer2.yaml new file mode 100644 index 00000000..0070fda9 --- /dev/null +++ b/test-network-k8s/kube/org0/org0-orderer2.yaml @@ -0,0 +1,85 @@ +# +# Copyright IBM Corp. All Rights Reserved. +# +# SPDX-License-Identifier: Apache-2.0 +# +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: org0-orderer2-env +data: + FABRIC_CFG_PATH: /var/hyperledger/fabric/config + FABRIC_LOGGING_SPEC: INFO # debug:cauthdsl,policies,msp,common.configtx,common.channelconfig=info + ORDERER_GENERAL_LISTENADDRESS: 0.0.0.0 + ORDERER_GENERAL_LISTENPORT: "6050" + ORDERER_GENERAL_LOCALMSPID: OrdererMSP + ORDERER_GENERAL_LOCALMSPDIR: /var/hyperledger/fabric/organizations/ordererOrganizations/org0.example.com/orderers/org0-orderer2.org0.example.com/msp + ORDERER_GENERAL_TLS_ENABLED: "true" + ORDERER_GENERAL_TLS_CERTIFICATE: /var/hyperledger/fabric/organizations/ordererOrganizations/org0.example.com/orderers/org0-orderer2.org0.example.com/tls/signcerts/cert.pem + ORDERER_GENERAL_TLS_ROOTCAS: /var/hyperledger/fabric/organizations/ordererOrganizations/org0.example.com/orderers/org0-orderer2.org0.example.com/tls/cacerts/org0-tls-ca.pem + ORDERER_GENERAL_TLS_PRIVATEKEY: /var/hyperledger/fabric/organizations/ordererOrganizations/org0.example.com/orderers/org0-orderer2.org0.example.com/tls/keystore/server.key + ORDERER_GENERAL_BOOTSTRAPMETHOD: none + ORDERER_FILELEDGER_LOCATION: /var/hyperledger/fabric/data/orderer2 + ORDERER_CONSENSUS_WALDIR: /var/hyperledger/fabric/data/orderer2/etcdraft/wal + ORDERER_CONSENSUS_SNAPDIR: /var/hyperledger/fabric/data/orderer2/etcdraft/wal + ORDERER_OPERATIONS_LISTENADDRESS: 0.0.0.0:8443 + ORDERER_ADMIN_LISTENADDRESS: 0.0.0.0:9443 + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: org0-orderer2 +spec: + replicas: 1 + selector: + matchLabels: + app: org0-orderer2 + template: + metadata: + labels: + app: org0-orderer2 + spec: + containers: + - name: main + image: hyperledger/fabric-orderer:{{FABRIC_VERSION}} + imagePullPolicy: IfNotPresent + envFrom: + - configMapRef: + name: org0-orderer2-env + ports: + - containerPort: 6050 + - containerPort: 8443 + - containerPort: 9443 + volumeMounts: + - name: fabric-volume + mountPath: /var/hyperledger + - name: fabric-config + mountPath: /var/hyperledger/fabric/config + volumes: + - name: fabric-volume + persistentVolumeClaim: + claimName: fabric-org0 + - name: fabric-config + configMap: + name: org0-config + +--- +apiVersion: v1 +kind: Service +metadata: + name: org0-orderer2 +spec: + ports: + - name: general + port: 6050 + protocol: TCP + - name: operations + port: 8443 + protocol: TCP + - name: admin + port: 9443 + protocol: TCP + selector: + app: org0-orderer2 \ No newline at end of file diff --git a/test-network-k8s/kube/org0/org0-orderer3.yaml b/test-network-k8s/kube/org0/org0-orderer3.yaml new file mode 100644 index 00000000..4db6edbf --- /dev/null +++ b/test-network-k8s/kube/org0/org0-orderer3.yaml @@ -0,0 +1,85 @@ +# +# Copyright IBM Corp. All Rights Reserved. +# +# SPDX-License-Identifier: Apache-2.0 +# +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: org0-orderer3-env +data: + FABRIC_CFG_PATH: /var/hyperledger/fabric/config + FABRIC_LOGGING_SPEC: INFO # debug:cauthdsl,policies,msp,common.configtx,common.channelconfig=info + ORDERER_GENERAL_LISTENADDRESS: 0.0.0.0 + ORDERER_GENERAL_LISTENPORT: "6050" + ORDERER_GENERAL_LOCALMSPID: OrdererMSP + ORDERER_GENERAL_LOCALMSPDIR: /var/hyperledger/fabric/organizations/ordererOrganizations/org0.example.com/orderers/org0-orderer3.org0.example.com/msp + ORDERER_GENERAL_TLS_ENABLED: "true" + ORDERER_GENERAL_TLS_CERTIFICATE: /var/hyperledger/fabric/organizations/ordererOrganizations/org0.example.com/orderers/org0-orderer3.org0.example.com/tls/signcerts/cert.pem + ORDERER_GENERAL_TLS_ROOTCAS: /var/hyperledger/fabric/organizations/ordererOrganizations/org0.example.com/orderers/org0-orderer3.org0.example.com/tls/cacerts/org0-tls-ca.pem + ORDERER_GENERAL_TLS_PRIVATEKEY: /var/hyperledger/fabric/organizations/ordererOrganizations/org0.example.com/orderers/org0-orderer3.org0.example.com/tls/keystore/server.key + ORDERER_GENERAL_BOOTSTRAPMETHOD: none + ORDERER_FILELEDGER_LOCATION: /var/hyperledger/fabric/data/orderer3 + ORDERER_CONSENSUS_WALDIR: /var/hyperledger/fabric/data/orderer3/etcdraft/wal + ORDERER_CONSENSUS_SNAPDIR: /var/hyperledger/fabric/data/orderer3/etcdraft/wal + ORDERER_OPERATIONS_LISTENADDRESS: 0.0.0.0:8443 + ORDERER_ADMIN_LISTENADDRESS: 0.0.0.0:9443 + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: org0-orderer3 +spec: + replicas: 1 + selector: + matchLabels: + app: org0-orderer3 + template: + metadata: + labels: + app: org0-orderer3 + spec: + containers: + - name: main + image: hyperledger/fabric-orderer:{{FABRIC_VERSION}} + imagePullPolicy: IfNotPresent + envFrom: + - configMapRef: + name: org0-orderer3-env + ports: + - containerPort: 6050 + - containerPort: 8443 + - containerPort: 9443 + volumeMounts: + - name: fabric-volume + mountPath: /var/hyperledger + - name: fabric-config + mountPath: /var/hyperledger/fabric/config + volumes: + - name: fabric-volume + persistentVolumeClaim: + claimName: fabric-org0 + - name: fabric-config + configMap: + name: org0-config + +--- +apiVersion: v1 +kind: Service +metadata: + name: org0-orderer3 +spec: + ports: + - name: general + port: 6050 + protocol: TCP + - name: operations + port: 8443 + protocol: TCP + - name: admin + port: 9443 + protocol: TCP + selector: + app: org0-orderer3 \ No newline at end of file diff --git a/test-network-k8s/kube/org0/org0-tls-ca.yaml b/test-network-k8s/kube/org0/org0-tls-ca.yaml new file mode 100644 index 00000000..179d9c44 --- /dev/null +++ b/test-network-k8s/kube/org0/org0-tls-ca.yaml @@ -0,0 +1,65 @@ +# +# Copyright IBM Corp. All Rights Reserved. +# +# SPDX-License-Identifier: Apache-2.0 +# +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: org0-tls-ca +spec: + replicas: 1 + selector: + matchLabels: + app: org0-tls-ca + template: + metadata: + labels: + app: org0-tls-ca + spec: + containers: + - name: main + image: hyperledger/fabric-ca:1.5.2 + env: + - name: FABRIC_CA_SERVER_CA_NAME + value: "org0-tls-ca" + - name: FABRIC_CA_SERVER_DEBUG + value: "false" + - name: FABRIC_CA_SERVER_HOME + value: "/var/hyperledger/fabric-tls-ca-server" + - name: FABRIC_CA_CLIENT_HOME + value: "/var/hyperledger/fabric-ca-client" + ports: + - containerPort: 443 + volumeMounts: + - name: fabric-volume + mountPath: /var/hyperledger + - name: fabric-config + mountPath: /var/hyperledger/fabric-tls-ca-server/fabric-ca-server-config.yaml + subPath: fabric-tls-ca-server-config.yaml + readinessProbe: + tcpSocket: + port: 443 + initialDelaySeconds: 2 + periodSeconds: 5 + volumes: + - name: fabric-volume + persistentVolumeClaim: + claimName: fabric-org0 + - name: fabric-config + configMap: + name: org0-config + +--- +apiVersion: v1 +kind: Service +metadata: + name: org0-tls-ca +spec: + ports: + - name: tls + port: 443 + protocol: TCP + selector: + app: org0-tls-ca \ No newline at end of file diff --git a/test-network-k8s/kube/org1/org1-admin-cli.yaml b/test-network-k8s/kube/org1/org1-admin-cli.yaml new file mode 100644 index 00000000..665f3595 --- /dev/null +++ b/test-network-k8s/kube/org1/org1-admin-cli.yaml @@ -0,0 +1,65 @@ +# +# Copyright IBM Corp. All Rights Reserved. +# +# SPDX-License-Identifier: Apache-2.0 +# +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: org1-admin-cli +spec: + replicas: 1 + selector: + matchLabels: + app: org1-admin-cli + template: + metadata: + labels: + app: org1-admin-cli + spec: + containers: + - name: main + image: hyperledger/fabric-tools:{{FABRIC_VERSION}} + imagePullPolicy: IfNotPresent + env: + - name: FABRIC_CFG_PATH + value: /var/hyperledger/fabric/config + - name: CORE_PEER_MSPCONFIGPATH + value: /var/hyperledger/fabric/organizations/peerOrganizations/org1.example.com/users/Admin@org1.example.com/msp + - name: CORE_PEER_TLS_ROOTCERT_FILE + value: /var/hyperledger/fabric/organizations/peerOrganizations/org1.example.com/msp/tlscacerts/org1-tls-ca.pem + args: + - sleep + - "2147483647" + workingDir: /root + volumeMounts: + - name: fabric-volume + mountPath: /var/hyperledger + - name: fabric-config + mountPath: /var/hyperledger/fabric/config + + # This init container will unfurl all of the MSP archives listed in the msp-config config map. + initContainers: + - name: msp-unfurl + image: busybox + command: + - sh + - -c + - "for msp in $(ls /msp/msp-*.tgz); do echo $msp && tar zxvf $msp -C /var/hyperledger/fabric; done" + volumeMounts: + - name: msp-config + mountPath: /msp + - name: fabric-volume + mountPath: /var/hyperledger + + volumes: + - name: fabric-volume + persistentVolumeClaim: + claimName: fabric-org1 + - name: fabric-config + configMap: + name: org1-config + - name: msp-config + configMap: + name: msp-config diff --git a/test-network-k8s/kube/org1/org1-cc-template.yaml b/test-network-k8s/kube/org1/org1-cc-template.yaml new file mode 100644 index 00000000..4c72bb4e --- /dev/null +++ b/test-network-k8s/kube/org1/org1-cc-template.yaml @@ -0,0 +1,45 @@ +# +# Copyright IBM Corp. All Rights Reserved. +# +# SPDX-License-Identifier: Apache-2.0 +# +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: org1-cc-{{CHAINCODE_NAME}} +spec: + replicas: 1 + selector: + matchLabels: + app: org1-cc-{{CHAINCODE_NAME}} + template: + metadata: + labels: + app: org1-cc-{{CHAINCODE_NAME}} + spec: + containers: + - name: main + image: {{CHAINCODE_IMAGE}} + env: + - name: CHAINCODE_SERVER_ADDRESS + value: 0.0.0.0:9999 + + # todo: load with an envFrom and a dynamic config map with the ID. + - name: CHAINCODE_ID + value: {{CHAINCODE_ID}} + ports: + - containerPort: 9999 + +--- +apiVersion: v1 +kind: Service +metadata: + name: org1-cc-{{CHAINCODE_NAME}} +spec: + ports: + - name: chaincode + port: 9999 + protocol: TCP + selector: + app: org1-cc-{{CHAINCODE_NAME}} \ No newline at end of file diff --git a/test-network-k8s/kube/org1/org1-ecert-ca.yaml b/test-network-k8s/kube/org1/org1-ecert-ca.yaml new file mode 100644 index 00000000..e3da8d11 --- /dev/null +++ b/test-network-k8s/kube/org1/org1-ecert-ca.yaml @@ -0,0 +1,69 @@ +# +# Copyright IBM Corp. All Rights Reserved. +# +# SPDX-License-Identifier: Apache-2.0 +# +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: org1-ecert-ca +spec: + replicas: 1 + selector: + matchLabels: + app: org1-ecert-ca + template: + metadata: + labels: + app: org1-ecert-ca + spec: + containers: + - name: main + image: hyperledger/fabric-ca:1.5.2 + env: + - name: FABRIC_CA_SERVER_CA_NAME + value: "org1-ecert-ca" + - name: FABRIC_CA_SERVER_DEBUG + value: "false" + - name: FABRIC_CA_SERVER_HOME + value: "/var/hyperledger/fabric-ca-server" + - name: FABRIC_CA_SERVER_TLS_CERTFILE + value: "/var/hyperledger/fabric-ca-client/tls-ca/rcaadmin/msp/signcerts/cert.pem" + - name: FABRIC_CA_SERVER_TLS_KEYFILE + value: "/var/hyperledger/fabric-ca-client/tls-ca/rcaadmin/msp/keystore/key.pem" + - name: FABRIC_CA_CLIENT_HOME + value: "/var/hyperledger/fabric-ca-client" + ports: + - containerPort: 443 + volumeMounts: + - name: fabric-volume + mountPath: /var/hyperledger + - name: fabric-config + mountPath: /var/hyperledger/fabric-ca-server/fabric-ca-server-config.yaml + subPath: fabric-ecert-ca-server-config.yaml + readinessProbe: + tcpSocket: + port: 443 + initialDelaySeconds: 2 + periodSeconds: 5 + volumes: + - name: fabric-volume + persistentVolumeClaim: + claimName: fabric-org1 + - name: fabric-config + configMap: + name: org1-config + +--- +apiVersion: v1 +kind: Service +metadata: + name: org1-ecert-ca +spec: + ports: + - name: tls + port: 443 + protocol: TCP + selector: + app: org1-ecert-ca \ No newline at end of file diff --git a/test-network-k8s/kube/org1/org1-peer1.yaml b/test-network-k8s/kube/org1/org1-peer1.yaml new file mode 100644 index 00000000..0f756a40 --- /dev/null +++ b/test-network-k8s/kube/org1/org1-peer1.yaml @@ -0,0 +1,103 @@ +# +# Copyright IBM Corp. All Rights Reserved. +# +# SPDX-License-Identifier: Apache-2.0 +# +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: org1-peer1-config +data: + FABRIC_CFG_PATH: /var/hyperledger/fabric/config + FABRIC_LOGGING_SPEC: "debug:cauthdsl,policies,msp,grpc,peer.gossip.mcs,gossip,leveldbhelper=info" + CORE_PEER_TLS_ENABLED: "true" + CORE_PEER_TLS_CERT_FILE: /var/hyperledger/fabric/organizations/peerOrganizations/org1.example.com/peers/org1-peer1.org1.example.com/tls/signcerts/cert.pem + CORE_PEER_TLS_KEY_FILE: /var/hyperledger/fabric/organizations/peerOrganizations/org1.example.com/peers/org1-peer1.org1.example.com/tls/keystore/server.key + CORE_PEER_TLS_ROOTCERT_FILE: /var/hyperledger/fabric/organizations/peerOrganizations/org1.example.com/peers/org1-peer1.org1.example.com/tls/cacerts/org1-tls-ca.pem + CORE_PEER_ID: org1-peer1.org1.example.com + CORE_PEER_ADDRESS: org1-peer1:7051 + CORE_PEER_LISTENADDRESS: 0.0.0.0:7051 + CORE_PEER_CHAINCODEADDRESS: org1-peer1:7052 + CORE_PEER_CHAINCODELISTENADDRESS: 0.0.0.0:7052 + # bootstrap peer is the other peer in the same org + CORE_PEER_GOSSIP_BOOTSTRAP: org1-peer2:7051 + CORE_PEER_GOSSIP_EXTERNALENDPOINT: org1-peer1:7051 + CORE_PEER_LOCALMSPID: Org1MSP + CORE_PEER_MSPCONFIGPATH: /var/hyperledger/fabric/organizations/peerOrganizations/org1.example.com/peers/org1-peer1.org1.example.com/msp + CORE_OPERATIONS_LISTENADDRESS: 0.0.0.0:9443 + CORE_PEER_FILESYSTEMPATH: /var/hyperledger/fabric/data/org1-peer1.org1.example.com + CORE_LEDGER_SNAPSHOTS_ROOTDIR: /var/hyperledger/fabric/data/org1-peer1.org1.example.com/snapshots + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: org1-peer1 +spec: + replicas: 1 + selector: + matchLabels: + app: org1-peer1 + template: + metadata: + labels: + app: org1-peer1 + spec: + containers: + - name: main + image: hyperledger/fabric-peer:{{FABRIC_VERSION}} + imagePullPolicy: IfNotPresent + envFrom: + - configMapRef: + name: org1-peer1-config + ports: + - containerPort: 7051 + - containerPort: 7052 + - containerPort: 9443 + volumeMounts: + - name: fabric-volume + mountPath: /var/hyperledger + - name: fabric-config + mountPath: /var/hyperledger/fabric/config + - name: ccs-builder + mountPath: /var/hyperledger/fabric/chaincode/ccs-builder/bin + + # load the external chaincode builder into the peer image prior to peer launch. + initContainers: + - name: fabric-ccs-builder + image: ghcr.io/hyperledgendary/fabric-ccs-builder + imagePullPolicy: IfNotPresent + command: [sh, -c] + args: ["cp /go/bin/* /var/hyperledger/fabric/chaincode/ccs-builder/bin/"] + volumeMounts: + - name: ccs-builder + mountPath: /var/hyperledger/fabric/chaincode/ccs-builder/bin + + volumes: + - name: fabric-volume + persistentVolumeClaim: + claimName: fabric-org1 + - name: fabric-config + configMap: + name: org1-config + - name: ccs-builder + emptyDir: {} +--- +apiVersion: v1 +kind: Service +metadata: + name: org1-peer1 +spec: + ports: + - name: gossip + port: 7051 + protocol: TCP + - name: chaincode + port: 7052 + protocol: TCP + - name: operations + port: 9443 + protocol: TCP + selector: + app: org1-peer1 \ No newline at end of file diff --git a/test-network-k8s/kube/org1/org1-peer2.yaml b/test-network-k8s/kube/org1/org1-peer2.yaml new file mode 100644 index 00000000..152c950e --- /dev/null +++ b/test-network-k8s/kube/org1/org1-peer2.yaml @@ -0,0 +1,89 @@ +# +# Copyright IBM Corp. All Rights Reserved. +# +# SPDX-License-Identifier: Apache-2.0 +# +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: org1-peer2-config +data: + FABRIC_CFG_PATH: /var/hyperledger/fabric/config + FABRIC_LOGGING_SPEC: "debug:cauthdsl,policies,msp,grpc,peer.gossip.mcs,gossip,leveldbhelper=info" + CORE_PEER_TLS_ENABLED: "true" + CORE_PEER_TLS_CERT_FILE: /var/hyperledger/fabric/organizations/peerOrganizations/org1.example.com/peers/org1-peer2.org1.example.com/tls/signcerts/cert.pem + CORE_PEER_TLS_KEY_FILE: /var/hyperledger/fabric/organizations/peerOrganizations/org1.example.com/peers/org1-peer2.org1.example.com/tls/keystore/server.key + CORE_PEER_TLS_ROOTCERT_FILE: /var/hyperledger/fabric/organizations/peerOrganizations/org1.example.com/peers/org1-peer2.org1.example.com/tls/cacerts/org1-tls-ca.pem + CORE_PEER_ID: org1-peer2.org1.example.com + CORE_PEER_ADDRESS: org1-peer2:7051 + CORE_PEER_LISTENADDRESS: 0.0.0.0:7051 + CORE_PEER_CHAINCODEADDRESS: org1-peer2:7052 + CORE_PEER_CHAINCODELISTENADDRESS: 0.0.0.0:7052 + # bootstrap peer is the other peer in the same org + CORE_PEER_GOSSIP_BOOTSTRAP: org1-peer1:7051 + CORE_PEER_GOSSIP_EXTERNALENDPOINT: org1-peer2:7051 + CORE_PEER_LOCALMSPID: Org1MSP + CORE_PEER_MSPCONFIGPATH: /var/hyperledger/fabric/organizations/peerOrganizations/org1.example.com/peers/org1-peer2.org1.example.com/msp + CORE_OPERATIONS_LISTENADDRESS: 0.0.0.0:9443 + CORE_PEER_FILESYSTEMPATH: /var/hyperledger/fabric/data/org1-peer2.org1.example.com + CORE_LEDGER_SNAPSHOTS_ROOTDIR: /var/hyperledger/fabric/data/org1-peer2.org1.example.com/snapshots + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: org1-peer2 +spec: + replicas: 1 + selector: + matchLabels: + app: org1-peer2 + template: + metadata: + labels: + app: org1-peer2 + spec: + containers: + - name: main + image: hyperledger/fabric-peer:{{FABRIC_VERSION}} + imagePullPolicy: IfNotPresent + envFrom: + - configMapRef: + name: org1-peer2-config + ports: + - containerPort: 7051 + - containerPort: 7052 + - containerPort: 9443 + volumeMounts: + - name: fabric-volume + mountPath: /var/hyperledger + - name: fabric-config + mountPath: /var/hyperledger/fabric/config + + volumes: + - name: fabric-volume + persistentVolumeClaim: + claimName: fabric-org1 + - name: fabric-config + configMap: + name: org1-config + +--- +apiVersion: v1 +kind: Service +metadata: + name: org1-peer2 +spec: + ports: + - name: gossip + port: 7051 + protocol: TCP + - name: chaincode + port: 7052 + protocol: TCP + - name: operations + port: 9443 + protocol: TCP + selector: + app: org1-peer2 \ No newline at end of file diff --git a/test-network-k8s/kube/org1/org1-tls-ca.yaml b/test-network-k8s/kube/org1/org1-tls-ca.yaml new file mode 100644 index 00000000..afedbe4d --- /dev/null +++ b/test-network-k8s/kube/org1/org1-tls-ca.yaml @@ -0,0 +1,65 @@ +# +# Copyright IBM Corp. All Rights Reserved. +# +# SPDX-License-Identifier: Apache-2.0 +# +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: org1-tls-ca +spec: + replicas: 1 + selector: + matchLabels: + app: org1-tls-ca + template: + metadata: + labels: + app: org1-tls-ca + spec: + containers: + - name: main + image: hyperledger/fabric-ca:1.5.2 + env: + - name: FABRIC_CA_SERVER_CA_NAME + value: "org1-tls-ca" + - name: FABRIC_CA_SERVER_DEBUG + value: "false" + - name: FABRIC_CA_SERVER_HOME + value: "/var/hyperledger/fabric-tls-ca-server" + - name: FABRIC_CA_CLIENT_HOME + value: "/var/hyperledger/fabric-ca-client" + ports: + - containerPort: 443 + volumeMounts: + - name: fabric-volume + mountPath: /var/hyperledger + - name: fabric-config + mountPath: /var/hyperledger/fabric-tls-ca-server/fabric-ca-server-config.yaml + subPath: fabric-tls-ca-server-config.yaml + readinessProbe: + tcpSocket: + port: 443 + initialDelaySeconds: 2 + periodSeconds: 5 + volumes: + - name: fabric-volume + persistentVolumeClaim: + claimName: fabric-org1 + - name: fabric-config + configMap: + name: org1-config + +--- +apiVersion: v1 +kind: Service +metadata: + name: org1-tls-ca +spec: + ports: + - name: tls + port: 443 + protocol: TCP + selector: + app: org1-tls-ca \ No newline at end of file diff --git a/test-network-k8s/kube/org2/org2-admin-cli.yaml b/test-network-k8s/kube/org2/org2-admin-cli.yaml new file mode 100644 index 00000000..be47123a --- /dev/null +++ b/test-network-k8s/kube/org2/org2-admin-cli.yaml @@ -0,0 +1,65 @@ +# +# Copyright IBM Corp. All Rights Reserved. +# +# SPDX-License-Identifier: Apache-2.0 +# +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: org2-admin-cli +spec: + replicas: 1 + selector: + matchLabels: + app: org2-admin-cli + template: + metadata: + labels: + app: org2-admin-cli + spec: + containers: + - name: main + image: hyperledger/fabric-tools:{{FABRIC_VERSION}} + imagePullPolicy: IfNotPresent + env: + - name: FABRIC_CFG_PATH + value: /var/hyperledger/fabric/config + - name: CORE_PEER_MSPCONFIGPATH + value: /var/hyperledger/fabric/organizations/peerOrganizations/org2.example.com/users/Admin@org2.example.com/msp + - name: CORE_PEER_TLS_ROOTCERT_FILE + value: /var/hyperledger/fabric/organizations/peerOrganizations/org2.example.com/msp/tlscacerts/org2-tls-ca.pem + args: + - sleep + - "2147483647" + workingDir: /root + volumeMounts: + - name: fabric-volume + mountPath: /var/hyperledger + - name: fabric-config + mountPath: /var/hyperledger/fabric/config + + # This init container will unfurl all of the MSP archives listed in the msp-config config map. + initContainers: + - name: msp-unfurl + image: busybox + command: + - sh + - -c + - "for msp in $(ls /msp/msp-*.tgz); do echo $msp && tar zxvf $msp -C /var/hyperledger/fabric; done" + volumeMounts: + - name: msp-config + mountPath: /msp + - name: fabric-volume + mountPath: /var/hyperledger + + volumes: + - name: fabric-volume + persistentVolumeClaim: + claimName: fabric-org2 + - name: fabric-config + configMap: + name: org2-config + - name: msp-config + configMap: + name: msp-config diff --git a/test-network-k8s/kube/org2/org2-cc.yaml b/test-network-k8s/kube/org2/org2-cc.yaml new file mode 100644 index 00000000..5075f2ba --- /dev/null +++ b/test-network-k8s/kube/org2/org2-cc.yaml @@ -0,0 +1,6 @@ +# +# Copyright IBM Corp. All Rights Reserved. +# +# SPDX-License-Identifier: Apache-2.0 +# + diff --git a/test-network-k8s/kube/org2/org2-ecert-ca.yaml b/test-network-k8s/kube/org2/org2-ecert-ca.yaml new file mode 100644 index 00000000..56c3350f --- /dev/null +++ b/test-network-k8s/kube/org2/org2-ecert-ca.yaml @@ -0,0 +1,69 @@ +# +# Copyright IBM Corp. All Rights Reserved. +# +# SPDX-License-Identifier: Apache-2.0 +# +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: org2-ecert-ca +spec: + replicas: 1 + selector: + matchLabels: + app: org2-ecert-ca + template: + metadata: + labels: + app: org2-ecert-ca + spec: + containers: + - name: main + image: hyperledger/fabric-ca:1.5.2 + env: + - name: FABRIC_CA_SERVER_CA_NAME + value: "org2-ecert-ca" + - name: FABRIC_CA_SERVER_DEBUG + value: "false" + - name: FABRIC_CA_SERVER_HOME + value: "/var/hyperledger/fabric-ca-server" + - name: FABRIC_CA_SERVER_TLS_CERTFILE + value: "/var/hyperledger/fabric-ca-client/tls-ca/rcaadmin/msp/signcerts/cert.pem" + - name: FABRIC_CA_SERVER_TLS_KEYFILE + value: "/var/hyperledger/fabric-ca-client/tls-ca/rcaadmin/msp/keystore/key.pem" + - name: FABRIC_CA_CLIENT_HOME + value: "/var/hyperledger/fabric-ca-client" + ports: + - containerPort: 443 + volumeMounts: + - name: fabric-volume + mountPath: /var/hyperledger + - name: fabric-config + mountPath: /var/hyperledger/fabric-ca-server/fabric-ca-server-config.yaml + subPath: fabric-ecert-ca-server-config.yaml + readinessProbe: + tcpSocket: + port: 443 + initialDelaySeconds: 2 + periodSeconds: 5 + volumes: + - name: fabric-volume + persistentVolumeClaim: + claimName: fabric-org2 + - name: fabric-config + configMap: + name: org2-config + +--- +apiVersion: v1 +kind: Service +metadata: + name: org2-ecert-ca +spec: + ports: + - name: tls + port: 443 + protocol: TCP + selector: + app: org2-ecert-ca \ No newline at end of file diff --git a/test-network-k8s/kube/org2/org2-peer1.yaml b/test-network-k8s/kube/org2/org2-peer1.yaml new file mode 100644 index 00000000..193dd2cc --- /dev/null +++ b/test-network-k8s/kube/org2/org2-peer1.yaml @@ -0,0 +1,104 @@ +# +# Copyright IBM Corp. All Rights Reserved. +# +# SPDX-License-Identifier: Apache-2.0 +# +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: org2-peer1-config +data: + FABRIC_CFG_PATH: /var/hyperledger/fabric/config + FABRIC_LOGGING_SPEC: "debug:cauthdsl,policies,msp,grpc,peer.gossip.mcs,gossip,leveldbhelper=info" + CORE_PEER_TLS_ENABLED: "true" + CORE_PEER_TLS_CERT_FILE: /var/hyperledger/fabric/organizations/peerOrganizations/org2.example.com/peers/org2-peer1.org2.example.com/tls/signcerts/cert.pem + CORE_PEER_TLS_KEY_FILE: /var/hyperledger/fabric/organizations/peerOrganizations/org2.example.com/peers/org2-peer1.org2.example.com/tls/keystore/server.key + CORE_PEER_TLS_ROOTCERT_FILE: /var/hyperledger/fabric/organizations/peerOrganizations/org2.example.com/peers/org2-peer1.org2.example.com/tls/cacerts/org2-tls-ca.pem + CORE_PEER_ID: org2-peer1.org2.example.com + CORE_PEER_ADDRESS: org2-peer1:7051 + CORE_PEER_LISTENADDRESS: 0.0.0.0:7051 + CORE_PEER_CHAINCODEADDRESS: org2-peer1:7052 + CORE_PEER_CHAINCODELISTENADDRESS: 0.0.0.0:7052 + # bootstrap peer is the other peer in the same org + CORE_PEER_GOSSIP_BOOTSTRAP: org2-peer2:7051 + CORE_PEER_GOSSIP_EXTERNALENDPOINT: org2-peer1:7051 + CORE_PEER_LOCALMSPID: Org2MSP + CORE_PEER_MSPCONFIGPATH: /var/hyperledger/fabric/organizations/peerOrganizations/org2.example.com/peers/org2-peer1.org2.example.com/msp + CORE_OPERATIONS_LISTENADDRESS: 0.0.0.0:9443 + CORE_PEER_FILESYSTEMPATH: /var/hyperledger/fabric/data/org2-peer1.org2.example.com + CORE_LEDGER_SNAPSHOTS_ROOTDIR: /var/hyperledger/fabric/data/org2-peer1.org2.example.com/snapshots + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: org2-peer1 +spec: + replicas: 1 + selector: + matchLabels: + app: org2-peer1 + template: + metadata: + labels: + app: org2-peer1 + spec: + containers: + - name: main + image: hyperledger/fabric-peer:{{FABRIC_VERSION}} + imagePullPolicy: IfNotPresent + envFrom: + - configMapRef: + name: org2-peer1-config + ports: + - containerPort: 7051 + - containerPort: 7052 + - containerPort: 9443 + volumeMounts: + - name: fabric-volume + mountPath: /var/hyperledger + - name: fabric-config + mountPath: /var/hyperledger/fabric/config + - name: ccs-builder + mountPath: /var/hyperledger/fabric/chaincode/ccs-builder/bin + + # load the external chaincode builder into the peer image prior to peer launch. + initContainers: + - name: fabric-ccs-builder + image: ghcr.io/hyperledgendary/fabric-ccs-builder + imagePullPolicy: IfNotPresent + command: [sh, -c] + args: ["cp /go/bin/* /var/hyperledger/fabric/chaincode/ccs-builder/bin/"] + volumeMounts: + - name: ccs-builder + mountPath: /var/hyperledger/fabric/chaincode/ccs-builder/bin + + volumes: + - name: fabric-volume + persistentVolumeClaim: + claimName: fabric-org2 + - name: fabric-config + configMap: + name: org2-config + - name: ccs-builder + emptyDir: {} + +--- +apiVersion: v1 +kind: Service +metadata: + name: org2-peer1 +spec: + ports: + - name: gossip + port: 7051 + protocol: TCP + - name: chaincode + port: 7052 + protocol: TCP + - name: operations + port: 9443 + protocol: TCP + selector: + app: org2-peer1 \ No newline at end of file diff --git a/test-network-k8s/kube/org2/org2-peer2.yaml b/test-network-k8s/kube/org2/org2-peer2.yaml new file mode 100644 index 00000000..c0df9b4e --- /dev/null +++ b/test-network-k8s/kube/org2/org2-peer2.yaml @@ -0,0 +1,89 @@ +# +# Copyright IBM Corp. All Rights Reserved. +# +# SPDX-License-Identifier: Apache-2.0 +# +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: org2-peer2-config +data: + FABRIC_CFG_PATH: /var/hyperledger/fabric/config + FABRIC_LOGGING_SPEC: "debug:cauthdsl,policies,msp,grpc,peer.gossip.mcs,gossip,leveldbhelper=info" + CORE_PEER_TLS_ENABLED: "true" + CORE_PEER_TLS_CERT_FILE: /var/hyperledger/fabric/organizations/peerOrganizations/org2.example.com/peers/org2-peer2.org2.example.com/tls/signcerts/cert.pem + CORE_PEER_TLS_KEY_FILE: /var/hyperledger/fabric/organizations/peerOrganizations/org2.example.com/peers/org2-peer2.org2.example.com/tls/keystore/server.key + CORE_PEER_TLS_ROOTCERT_FILE: /var/hyperledger/fabric/organizations/peerOrganizations/org2.example.com/peers/org2-peer2.org2.example.com/tls/cacerts/org2-tls-ca.pem + CORE_PEER_ID: org2-peer2.org2.example.com + CORE_PEER_ADDRESS: org2-peer2:7051 + CORE_PEER_LISTENADDRESS: 0.0.0.0:7051 + CORE_PEER_CHAINCODEADDRESS: org2-peer2:7052 + CORE_PEER_CHAINCODELISTENADDRESS: 0.0.0.0:7052 + # bootstrap peer is the other peer in the same org + CORE_PEER_GOSSIP_BOOTSTRAP: org2-peer1:7051 + CORE_PEER_GOSSIP_EXTERNALENDPOINT: org2-peer2:7051 + CORE_PEER_LOCALMSPID: Org2MSP + CORE_PEER_MSPCONFIGPATH: /var/hyperledger/fabric/organizations/peerOrganizations/org2.example.com/peers/org2-peer2.org2.example.com/msp + CORE_OPERATIONS_LISTENADDRESS: 0.0.0.0:9443 + CORE_PEER_FILESYSTEMPATH: /var/hyperledger/fabric/data/org2-peer2.org2.example.com + CORE_LEDGER_SNAPSHOTS_ROOTDIR: /var/hyperledger/fabric/data/org2-peer2.org2.example.com/snapshots + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: org2-peer2 +spec: + replicas: 1 + selector: + matchLabels: + app: org2-peer2 + template: + metadata: + labels: + app: org2-peer2 + spec: + containers: + - name: main + image: hyperledger/fabric-peer:{{FABRIC_VERSION}} + imagePullPolicy: IfNotPresent + envFrom: + - configMapRef: + name: org2-peer2-config + ports: + - containerPort: 7051 + - containerPort: 7052 + - containerPort: 9443 + volumeMounts: + - name: fabric-volume + mountPath: /var/hyperledger + - name: fabric-config + mountPath: /var/hyperledger/fabric/config + + volumes: + - name: fabric-volume + persistentVolumeClaim: + claimName: fabric-org2 + - name: fabric-config + configMap: + name: org2-config + +--- +apiVersion: v1 +kind: Service +metadata: + name: org2-peer2 +spec: + ports: + - name: gossip + port: 7051 + protocol: TCP + - name: chaincode + port: 7052 + protocol: TCP + - name: operations + port: 9443 + protocol: TCP + selector: + app: org2-peer2 \ No newline at end of file diff --git a/test-network-k8s/kube/org2/org2-tls-ca.yaml b/test-network-k8s/kube/org2/org2-tls-ca.yaml new file mode 100644 index 00000000..b28a7aa0 --- /dev/null +++ b/test-network-k8s/kube/org2/org2-tls-ca.yaml @@ -0,0 +1,65 @@ +# +# Copyright IBM Corp. All Rights Reserved. +# +# SPDX-License-Identifier: Apache-2.0 +# +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: org2-tls-ca +spec: + replicas: 1 + selector: + matchLabels: + app: org2-tls-ca + template: + metadata: + labels: + app: org2-tls-ca + spec: + containers: + - name: main + image: hyperledger/fabric-ca:1.5.2 + env: + - name: FABRIC_CA_SERVER_CA_NAME + value: "org2-tls-ca" + - name: FABRIC_CA_SERVER_DEBUG + value: "false" + - name: FABRIC_CA_SERVER_HOME + value: "/var/hyperledger/fabric-tls-ca-server" + - name: FABRIC_CA_CLIENT_HOME + value: "/var/hyperledger/fabric-ca-client" + ports: + - containerPort: 443 + volumeMounts: + - name: fabric-volume + mountPath: /var/hyperledger + - name: fabric-config + mountPath: /var/hyperledger/fabric-tls-ca-server/fabric-ca-server-config.yaml + subPath: fabric-tls-ca-server-config.yaml + readinessProbe: + tcpSocket: + port: 443 + initialDelaySeconds: 2 + periodSeconds: 5 + volumes: + - name: fabric-volume + persistentVolumeClaim: + claimName: fabric-org2 + - name: fabric-config + configMap: + name: org2-config + +--- +apiVersion: v1 +kind: Service +metadata: + name: org2-tls-ca +spec: + ports: + - name: tls + port: 443 + protocol: TCP + selector: + app: org2-tls-ca \ No newline at end of file diff --git a/test-network-k8s/kube/pv-fabric-org0.yaml b/test-network-k8s/kube/pv-fabric-org0.yaml new file mode 100644 index 00000000..085872a4 --- /dev/null +++ b/test-network-k8s/kube/pv-fabric-org0.yaml @@ -0,0 +1,18 @@ +# +# Copyright IBM Corp. All Rights Reserved. +# +# SPDX-License-Identifier: Apache-2.0 +# +--- +apiVersion: v1 +kind: PersistentVolume +metadata: + name: fabric-org0 +spec: + storageClassName: standard + accessModes: + - ReadWriteOnce + capacity: + storage: 2Gi + hostPath: + path: /var/hyperledger/example.com diff --git a/test-network-k8s/kube/pv-fabric-org1.yaml b/test-network-k8s/kube/pv-fabric-org1.yaml new file mode 100644 index 00000000..58de91af --- /dev/null +++ b/test-network-k8s/kube/pv-fabric-org1.yaml @@ -0,0 +1,18 @@ +# +# Copyright IBM Corp. All Rights Reserved. +# +# SPDX-License-Identifier: Apache-2.0 +# +--- +apiVersion: v1 +kind: PersistentVolume +metadata: + name: fabric-org1 +spec: + storageClassName: standard + accessModes: + - ReadWriteOnce + capacity: + storage: 2Gi + hostPath: + path: /var/hyperledger/org1.example.com diff --git a/test-network-k8s/kube/pv-fabric-org2.yaml b/test-network-k8s/kube/pv-fabric-org2.yaml new file mode 100644 index 00000000..aa3660c7 --- /dev/null +++ b/test-network-k8s/kube/pv-fabric-org2.yaml @@ -0,0 +1,18 @@ +# +# Copyright IBM Corp. All Rights Reserved. +# +# SPDX-License-Identifier: Apache-2.0 +# +--- +apiVersion: v1 +kind: PersistentVolume +metadata: + name: fabric-org2 +spec: + storageClassName: standard + accessModes: + - ReadWriteOnce + capacity: + storage: 2Gi + hostPath: + path: /var/hyperledger/org2.example.com diff --git a/test-network-k8s/kube/pvc-fabric-org0.yaml b/test-network-k8s/kube/pvc-fabric-org0.yaml new file mode 100644 index 00000000..c3d64208 --- /dev/null +++ b/test-network-k8s/kube/pvc-fabric-org0.yaml @@ -0,0 +1,17 @@ +# +# Copyright IBM Corp. All Rights Reserved. +# +# SPDX-License-Identifier: Apache-2.0 +# +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: fabric-org0 +spec: + volumeName: fabric-org0 + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 1Gi diff --git a/test-network-k8s/kube/pvc-fabric-org1.yaml b/test-network-k8s/kube/pvc-fabric-org1.yaml new file mode 100644 index 00000000..d06bc01c --- /dev/null +++ b/test-network-k8s/kube/pvc-fabric-org1.yaml @@ -0,0 +1,17 @@ +# +# Copyright IBM Corp. All Rights Reserved. +# +# SPDX-License-Identifier: Apache-2.0 +# +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: fabric-org1 +spec: + volumeName: fabric-org1 + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 1Gi diff --git a/test-network-k8s/kube/pvc-fabric-org2.yaml b/test-network-k8s/kube/pvc-fabric-org2.yaml new file mode 100644 index 00000000..40d2e48f --- /dev/null +++ b/test-network-k8s/kube/pvc-fabric-org2.yaml @@ -0,0 +1,17 @@ +# +# Copyright IBM Corp. All Rights Reserved. +# +# SPDX-License-Identifier: Apache-2.0 +# +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: fabric-org2 +spec: + volumeName: fabric-org2 + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 1Gi diff --git a/test-network-k8s/network b/test-network-k8s/network new file mode 100755 index 00000000..ecbef2a3 --- /dev/null +++ b/test-network-k8s/network @@ -0,0 +1,157 @@ +#!/bin/bash +# +# Copyright IBM Corp All Rights Reserved +# +# SPDX-License-Identifier: Apache-2.0 +# +set -o errexit + +# todo: better handling for input parameters. +# todo: skip storage volume init if deploying to a remote / cloud cluster (ICP IKS ROKS etc...) +# todo: for logging, set up a stack and allow multi-line status output codes +# todo: refactor - lots of for-org-in-0-to-2-... +# todo: find a better technique for passing input commands to a remote kube exec +# todo: register tls csr.hosts w/ kube DNS domain .NS.svc.cluster.local +# todo: user:pass auth for tls and ecert bootstrap admins. here and in the server-config.yaml +# todo: set tls.certfiles= ... arg in deployment env / yaml +# todo: refactor chaincode install to support other chaincode routines +# todo: actually compile the chaincode archive, rather than reading a pre-canned one from chaincode/*.tgz (use sha256 to get CC ID) +# todo: consider using templates for boilerplate network nodes (orderers, peers, ...) +# todo: allow the user to specify the chaincode name (hardcoded as 'basic') both in install and invoke/query +# todo: track down a nasty bug whereby the CA service endpoints (kube services) will occasionally reject TCP connections after network down/up. This is patched by introducing a 10s sleep after the deployments are up... +# todo: refactor query/invoke to specify chaincode name (-n param) + +FABRIC_VERSION=${TEST_NETWORK_FABRIC_VERSION:-2.3.2} +NETWORK_NAME=${TEST_NETWORK_NAME:-test-network} +CLUSTER_NAME=${TEST_NETWORK_KIND_CLUSTER_NAME:-kind} +NS=${TEST_NETWORK_KUBE_NAMESPACE:-${NETWORK_NAME}} +CHANNEL_NAME=${TEST_NETWORK_CHANNEL_NAME:-mychannel} +LOG_FILE=${TEST_NETWORK_LOG_FILE:-network.log} +DEBUG_FILE=${TEST_NETWORK_DEBUG_FILE:-network-debug.log} +CONTAINER_REGISTRY_NAME=${TEST_NETWORK_CONTAINER_REGISTRY_NAME:-kind-registry} +CONTAINER_REGISTRY_PORT=${TEST_NETWORK_CONTAINER_REGISTRY_PORT:-5000} +NGINX_HTTP_PORT=${TEST_NETWORK_INGRESS_HTTP_PORT:-80} +NGINX_HTTPS_PORT=${TEST_NETWORK_INGRESS_HTTPS_PORT:-443} +CHAINCODE_NAME=${TEST_NETWORK_CHAINCODE_NAME:-asset-transfer-basic} +CHAINCODE_IMAGE=${TEST_NETWORK_CHAINCODE_IMAGE:-ghcr.io/hyperledgendary/fabric-ccaas-asset-transfer-basic} +CHAINCODE_LABEL=${TEST_NETWORK_CHAINCODE_LABEL:-basic_1.0} + +# todo: more complicated config, as these bleed into the yaml descriptors (sed? kustomize? helm (no)? tkn? ansible?...) or other script locations +FABRIC_CA_VERSION=1.5.2 +TLSADMIN_AUTH=tlsadmin:tlsadminpw +RCAADMIN_AUTH=rcaadmin:rcaadminpw + +function print_help() { + echo todo: help output, parse mode, flags, env, etc. +} + +. scripts/utils.sh +. scripts/prereqs.sh +. scripts/kind.sh +. scripts/fabric_config.sh +. scripts/fabric_CAs.sh +. scripts/test_network.sh +. scripts/channel.sh +. scripts/chaincode.sh +. scripts/rest_sample.sh +. scripts/application_connection.sh + +# check for kind, kubectl, etc. +check_prereqs + +## Parse mode +if [[ $# -lt 1 ]] ; then + print_help + exit 0 +else + MODE=$1 + shift +fi + +# Initialize the logging system - control output to 'network.log' and everything else to 'network-debug.log' +logging_init + +if [ "${MODE}" == "kind" ]; then + log "Initializing KIND cluster \"${CLUSTER_NAME}\":" + kind_init + log "🏁 - Cluster is ready." + +elif [ "${MODE}" == "unkind" ]; then + log "Deleting cluster \"${CLUSTER_NAME}\":" + kind_unkind + log "🏁 - Cluster is gone." + +elif [ "${MODE}" == "up" ]; then + log "Launching network \"${NETWORK_NAME}\":" + network_up + log "🏁 - Network is ready." + +elif [ "${MODE}" == "down" ]; then + log "Shutting down test network \"${NETWORK_NAME}\":" + network_down + log "🏁 - Fabric network is down." + +elif [ "${MODE}" == "channel" ]; then + ACTION=$1 + shift + + if [ "${ACTION}" == "create" ]; then + log "Creating channel \"${CHANNEL_NAME}\":" + channel_up + log "🏁 - Channel is ready." + + else + print_help + exit 1 + fi + +elif [ "${MODE}" == "chaincode" ]; then + ACTION=$1 + shift + + if [ "${ACTION}" == "deploy" ]; then + log "Deploying chaincode \"${CHAINCODE_NAME}\":" + deploy_chaincode + log "🏁 - Chaincode is ready." + + elif [ "${ACTION}" == "install" ]; then + log "Installing chaincode \"${CHAINCODE_NAME}\":" + install_chaincode + log "🏁 - Chaincode is installed with CHAINCODE_ID=${CHAINCODE_ID}" + + elif [ "${ACTION}" == "activate" ]; then + log "Activating chaincode \"${CHAINCODE_NAME}\":" + activate_chaincode + log "🏁 - Chaincode is activated with CHAINCODE_ID=${CHAINCODE_ID}" + + elif [ "${ACTION}" == "invoke" ]; then + invoke_chaincode $@ 2>> ${LOG_FILE} + + elif [ "${ACTION}" == "query" ]; then + query_chaincode $@ >> ${LOG_FILE} + + elif [ "${ACTION}" == "metadata" ]; then + query_chaincode_metadata >> ${LOG_FILE} + else + print_help + exit 1 + fi + +elif [ "${MODE}" == "anchor" ]; then + update_anchor_peers $@ + +elif [ "${MODE}" == "rest-easy" ]; then + log "Launching fabric-rest-sample application:" + launch_rest_sample + log "🏁 - Fabric REST sample is ready." + +elif [ "${MODE}" == "application" ]; then + log "Getting application connection information:" + application_connection + log "🏁 - Output in...xxx" + +else + print_help + exit 1 +fi + diff --git a/test-network-k8s/scripts/application_connection.sh b/test-network-k8s/scripts/application_connection.sh new file mode 100755 index 00000000..3f73dd20 --- /dev/null +++ b/test-network-k8s/scripts/application_connection.sh @@ -0,0 +1,114 @@ +#!/bin/bash +# +# Copyright IBM Corp All Rights Reserved +# +# SPDX-License-Identifier: Apache-2.0 +# + +function app_extract_MSP_archives() { + mkdir -p build/msp + set -ex + kubectl -n $NS exec deploy/org1-ecert-ca -- tar zcf - -C /var/hyperledger/fabric organizations/peerOrganizations/org1.example.com/msp | tar zxf - -C build/msp + kubectl -n $NS exec deploy/org2-ecert-ca -- tar zcf - -C /var/hyperledger/fabric organizations/peerOrganizations/org2.example.com/msp | tar zxf - -C build/msp + + kubectl -n $NS exec deploy/org1-ecert-ca -- tar zcf - -C /var/hyperledger/fabric organizations/peerOrganizations/org1.example.com/users/Admin@org1.example.com/msp | tar zxf - -C build/msp + kubectl -n $NS exec deploy/org2-ecert-ca -- tar zcf - -C /var/hyperledger/fabric organizations/peerOrganizations/org2.example.com/users/Admin@org2.example.com/msp | tar zxf - -C build/msp +} + +function app_one_line_pem { + echo "`awk 'NF {sub(/\\n/, ""); printf "%s\\\\\\\n",$0;}' $1`" +} + +function app_json_ccp { + local ORG=$1 + local PP=$(one_line_pem $2) + local CP=$(one_line_pem $3) + sed -e "s/\${ORG}/$ORG/" \ + -e "s#\${PEERPEM}#$PP#" \ + -e "s#\${CAPEM}#$CP#" \ + scripts/ccp-template.json +} + +function app_id { + local MSP=$1 + local CERT=$(one_line_pem $2) + local PK=$(one_line_pem $3) + + sed -e "s#\${CERTIFICATE}#$CERT#" \ + -e "s#\${PRIVATE_KEY}#$PK#" \ + -e "s#\${MSPID}#$MSP#" \ + scripts/appuser.id.template +} + +function construct_application_configmap() { + push_fn "Constructing application connection profiles" + + app_extract_MSP_archives + + mkdir -p build/application/wallet + mkdir -p build/application/gateways + + local peer_pem=build/msp/organizations/peerOrganizations/org1.example.com/msp/tlscacerts/org1-tls-ca.pem + local ca_pem=build/msp/organizations/peerOrganizations/org1.example.com/msp/cacerts/org1-ecert-ca.pem + + echo "$(json_ccp 1 $peer_pem $ca_pem)" > build/application/gateways/org1_ccp.json + + peer_pem=build/msp/organizations/peerOrganizations/org2.example.com/msp/tlscacerts/org2-tls-ca.pem + ca_pem=build/msp/organizations/peerOrganizations/org2.example.com/msp/cacerts/org2-ecert-ca.pem + + echo "$(json_ccp 2 $peer_pem $ca_pem)" > build/application/gateways/org2_ccp.json + + local cert=build/msp/organizations/peerOrganizations/org1.example.com/users/Admin\@org1.example.com/msp/signcerts/cert.pem + local pk=build/msp/organizations/peerOrganizations/org1.example.com/users/Admin\@org1.example.com/msp/keystore/server.key + + echo "$(app_id Org1MSP $cert $pk)" > build/application/wallet/appuser_org1.id + + local cert=build/msp/organizations/peerOrganizations/org2.example.com/users/Admin\@org2.example.com/msp/signcerts/cert.pem + local pk=build/msp/organizations/peerOrganizations/org2.example.com/users/Admin\@org2.example.com/msp/keystore/server.key + + echo "$(app_id Org2MSP $cert $pk)" > build/application/wallet/appuser_org2.id + + kubectl -n $NS delete configmap app-fabric-tls-v1-map || log "app-fabric-tls-v1-map not present" + kubectl -n $NS create configmap app-fabric-tls-v1-map --from-file=./build/msp/organizations/peerOrganizations/org1.example.com/msp/tlscacerts + + kubectl -n $NS delete configmap app-fabric-ids-v1-map || log "app-fabric-id-v1-map not present" + kubectl -n $NS create configmap app-fabric-ids-v1-map --from-file=./build/application/wallet + + kubectl -n $NS delete configmap app-fabric-ccp-v1-map || log "app-fabric-id-v1-map not present" + kubectl -n $NS create configmap app-fabric-ccp-v1-map --from-file=./build/application/gateways + +cat < build/app-fabric-org1-v1-map.yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: app-fabric-org1-v1-map +data: + fabric_channel: ${CHANNEL_NAME} + fabric_contract: ${CHAINCODE_NAME} + fabric_wallet_dir: /fabric/application/wallet + fabric_gateway_hostport: org1-peer1:7051 + fabric_gateway_sslHostOverride: org1-peer1 + fabric_user: appuser_org1 + fabric_gateway_tlsCertPath: /fabric/tlscacerts/org1-tls-ca.pem +EOF + + kubectl -n $NS apply -f build/app-fabric-org1-v1-map.yaml + + # todo: could add the second org here + + pop_fn +} + + +function application_connection() { + + construct_application_configmap + + log "" + log "Config Maps created for the application" + log "To deploy your application updated the image name and issue these commands" + log "" + log "kubectl -n $NS apply -f kube/application-deployment.yaml" + log "kubectl -n $NS rollout status deploy/application-deployment" + +} \ No newline at end of file diff --git a/test-network-k8s/scripts/appuser.id.template b/test-network-k8s/scripts/appuser.id.template new file mode 100644 index 00000000..1c67576c --- /dev/null +++ b/test-network-k8s/scripts/appuser.id.template @@ -0,0 +1,9 @@ +{ + "credentials": { + "certificate": "${CERTIFICATE}", + "privateKey": "${PRIVATE_KEY}" + }, + "mspId": "${MSPID}", + "type": "X.509", + "version": 1 +} \ No newline at end of file diff --git a/test-network-k8s/scripts/ccp-template.json b/test-network-k8s/scripts/ccp-template.json new file mode 100755 index 00000000..4bd0a26e --- /dev/null +++ b/test-network-k8s/scripts/ccp-template.json @@ -0,0 +1,49 @@ +{ + "name": "test-network-org${ORG}", + "version": "1.0.0", + "client": { + "organization": "Org${ORG}", + "connection": { + "timeout": { + "peer": { + "endorser": "300" + } + } + } + }, + "organizations": { + "Org${ORG}": { + "mspid": "Org${ORG}MSP", + "peers": [ + "org${ORG}-peer1" + ], + "certificateAuthorities": [ + "org${ORG}-ecert-ca" + ] + } + }, + "peers": { + "org${ORG}-peer1": { + "url": "grpcs://org${ORG}-peer1:7051", + "tlsCACerts": { + "pem": "${PEERPEM}" + }, + "grpcOptions": { + "ssl-target-name-override": "org${ORG}-peer1", + "hostnameOverride": "org${ORG}-peer1" + } + } + }, + "certificateAuthorities": { + "org${ORG}-ecert-ca": { + "url": "https://org${ORG}-ecert-ca", + "caName": "org${ORG}-ecert-ca", + "tlsCACerts": { + "pem": ["${CAPEM}"] + }, + "httpOptions": { + "verify": false + } + } + } +} diff --git a/test-network-k8s/scripts/chaincode.sh b/test-network-k8s/scripts/chaincode.sh new file mode 100755 index 00000000..c0fec221 --- /dev/null +++ b/test-network-k8s/scripts/chaincode.sh @@ -0,0 +1,172 @@ +#!/bin/bash +# +# Copyright IBM Corp All Rights Reserved +# +# SPDX-License-Identifier: Apache-2.0 +# + +function package_chaincode_for() { + local org=$1 + local cc_folder="chaincode/${CHAINCODE_NAME}" + local build_folder="build/chaincode" + local cc_archive="${build_folder}/${CHAINCODE_NAME}.tgz" + push_fn "Packaging chaincode folder ${cc_folder}" + + mkdir -p ${build_folder} + + tar -C ${cc_folder} -zcf ${cc_folder}/code.tar.gz connection.json + tar -C ${cc_folder} -zcf ${cc_archive} code.tar.gz metadata.json + + rm ${cc_folder}/code.tar.gz + + pop_fn +} + +# Copy the chaincode archive from the local host to the org admin +function transfer_chaincode_archive_for() { + local org=$1 + local cc_archive="build/chaincode/${CHAINCODE_NAME}.tgz" + push_fn "Transferring chaincode archive to ${org}" + + # Like kubectl cp, but targeted to a deployment rather than an individual pod. + tar cf - ${cc_archive} | kubectl -n $NS exec -i deploy/${org}-admin-cli -c main -- tar xvf - + + pop_fn +} + +function install_chaincode_for() { + local org=$1 + push_fn "Installing chaincode for org ${org}" + + # Install the chaincode + echo 'set -x + export CORE_PEER_ADDRESS='${org}'-peer1:7051 + peer lifecycle chaincode install build/chaincode/'${CHAINCODE_NAME}'.tgz + ' | exec kubectl -n $NS exec deploy/${org}-admin-cli -c main -i -- /bin/bash + + pop_fn +} + +function launch_chaincode_service() { + local org=$1 + local cc_id=$2 + local cc_image=$3 + push_fn "Launching chaincode container \"${cc_image}\"" + + # The chaincode endpoint needs to have the generated chaincode ID available in the environment. + # This could be from a config map, a secret, or by directly editing the deployment spec. Here we'll keep + # things simple by using sed to substitute script variables into a yaml template. + cat kube/${org}/${org}-cc-template.yaml \ + | sed 's,{{CHAINCODE_NAME}},'${CHAINCODE_NAME}',g' \ + | sed 's,{{CHAINCODE_ID}},'${cc_id}',g' \ + | sed 's,{{CHAINCODE_IMAGE}},'${cc_image}',g' \ + | exec kubectl -n $NS apply -f - + + kubectl -n $NS rollout status deploy/${org}-cc-${CHAINCODE_NAME} + + pop_fn +} + +function activate_chaincode_for() { + local org=$1 + local cc_id=$2 + push_fn "Activating chaincode ${CHAINCODE_ID}" + + echo 'set -x + export CORE_PEER_ADDRESS='${org}'-peer1:7051 + + peer lifecycle \ + chaincode approveformyorg \ + --channelID '${CHANNEL_NAME}' \ + --name '${CHAINCODE_NAME}' \ + --version 1 \ + --package-id '${cc_id}' \ + --sequence 1 \ + -o org0-orderer1:6050 \ + --tls --cafile /var/hyperledger/fabric/organizations/ordererOrganizations/org0.example.com/msp/tlscacerts/org0-tls-ca.pem + + peer lifecycle \ + chaincode commit \ + --channelID '${CHANNEL_NAME}' \ + --name '${CHAINCODE_NAME}' \ + --version 1 \ + --sequence 1 \ + -o org0-orderer1:6050 \ + --tls --cafile /var/hyperledger/fabric/organizations/ordererOrganizations/org0.example.com/msp/tlscacerts/org0-tls-ca.pem + ' | exec kubectl -n $NS exec deploy/${org}-admin-cli -c main -i -- /bin/bash + + pop_fn +} + +function query_chaincode() { + set -x + # todo: mangle additional $@ parameters with bash escape quotations + echo ' + export CORE_PEER_ADDRESS=org1-peer1:7051 + peer chaincode query -n '${CHAINCODE_NAME}' -C '${CHANNEL_NAME}' -c '"'$@'"' + ' | exec kubectl -n $NS exec deploy/org1-admin-cli -c main -i -- /bin/bash +} + +function query_chaincode_metadata() { + set -x + local args='{"Args":["org.hyperledger.fabric:GetMetadata"]}' + # todo: mangle additional $@ parameters with bash escape quotations + echo ' + export CORE_PEER_ADDRESS=org1-peer1:7051 + peer chaincode query -n '${CHAINCODE_NAME}' -C '${CHANNEL_NAME}' -c '"'$args'"' + ' | exec kubectl -n $NS exec deploy/org1-admin-cli -c main -i -- /bin/bash +} + +function invoke_chaincode() { + # set -x + # todo: mangle additional $@ parameters with bash escape quotations + echo ' + export CORE_PEER_ADDRESS=org1-peer1:7051 + peer chaincode \ + invoke \ + -o org0-orderer1:6050 \ + --tls --cafile /var/hyperledger/fabric/organizations/ordererOrganizations/org0.example.com/msp/tlscacerts/org0-tls-ca.pem \ + -n '${CHAINCODE_NAME}' \ + -C '${CHANNEL_NAME}' \ + -c '"'$@'"' + ' | exec kubectl -n $NS exec deploy/org1-admin-cli -c main -i -- /bin/bash + + sleep 2 +} + +# Normally the chaincode ID is emitted by the peer install command. In this case, we'll generate the +# package ID as the sha-256 checksum of the chaincode archive. +function set_chaincode_id() { + local cc_sha256=$(shasum -a 256 build/chaincode/${CHAINCODE_NAME}.tgz | tr -s ' ' | cut -d ' ' -f 1) + + CHAINCODE_ID=${CHAINCODE_LABEL}:${cc_sha256} +} + +# Package and install the chaincode, but do not activate. +function install_chaincode() { + local org=org1 + + package_chaincode_for ${org} + transfer_chaincode_archive_for ${org} + install_chaincode_for ${org} + + set_chaincode_id +} + +# Activate the installed chaincode but do not package/install a new archive. +function activate_chaincode() { + set -x + + set_chaincode_id + activate_chaincode_for org1 $CHAINCODE_ID +} + +# Install, launch, and activate the chaincode +function deploy_chaincode() { + set -x + + install_chaincode + launch_chaincode_service org1 $CHAINCODE_ID $CHAINCODE_IMAGE + activate_chaincode +} + diff --git a/test-network-k8s/scripts/channel.sh b/test-network-k8s/scripts/channel.sh new file mode 100755 index 00000000..f8a79555 --- /dev/null +++ b/test-network-k8s/scripts/channel.sh @@ -0,0 +1,201 @@ +#!/bin/bash +# +# Copyright IBM Corp All Rights Reserved +# +# SPDX-License-Identifier: Apache-2.0 +# + +function create_channel_org_MSP() { + local org=$1 + local org_type=$2 + local ecert_ca=${org}-ecert-ca + + echo 'set -x + + mkdir -p /var/hyperledger/fabric/organizations/'${org_type}'Organizations/'${org}'.example.com/msp/cacerts + cp \ + $FABRIC_CA_CLIENT_HOME/'${ecert_ca}'/rcaadmin/msp/cacerts/'${ecert_ca}'.pem \ + /var/hyperledger/fabric/organizations/'${org_type}'Organizations/'${org}'.example.com/msp/cacerts + + mkdir -p /var/hyperledger/fabric/organizations/'${org_type}'Organizations/'${org}'.example.com/msp/tlscacerts + cp \ + $FABRIC_CA_CLIENT_HOME/tls-ca/tlsadmin/msp/cacerts/'${org}'-tls-ca.pem \ + /var/hyperledger/fabric/organizations/'${org_type}'Organizations/'${org}'.example.com/msp/tlscacerts + + echo "NodeOUs: + Enable: true + ClientOUIdentifier: + Certificate: cacerts/'${ecert_ca}'.pem + OrganizationalUnitIdentifier: client + PeerOUIdentifier: + Certificate: cacerts/'${ecert_ca}'.pem + OrganizationalUnitIdentifier: peer + AdminOUIdentifier: + Certificate: cacerts/'${ecert_ca}'.pem + OrganizationalUnitIdentifier: admin + OrdererOUIdentifier: + Certificate: cacerts/'${ecert_ca}'.pem + OrganizationalUnitIdentifier: orderer "> /var/hyperledger/fabric/organizations/'${org_type}'Organizations/'${org}'.example.com/msp/config.yaml + + ' | exec kubectl -n $NS exec deploy/${ecert_ca} -i -- /bin/sh +} + +function create_channel_MSP() { + push_fn "Creating channel MSP" + + create_channel_org_MSP org0 orderer + create_channel_org_MSP org1 peer + create_channel_org_MSP org2 peer + + pop_fn +} + +function aggregate_channel_MSP() { + push_fn "Aggregating channel MSP" + + rm -rf ./build/msp/ + mkdir -p ./build/msp + + kubectl -n $NS exec deploy/org0-ecert-ca -- tar zcvf - -C /var/hyperledger/fabric organizations/ordererOrganizations/org0.example.com/msp > build/msp/msp-org0.example.com.tgz + kubectl -n $NS exec deploy/org1-ecert-ca -- tar zcvf - -C /var/hyperledger/fabric organizations/peerOrganizations/org1.example.com/msp > build/msp/msp-org1.example.com.tgz + kubectl -n $NS exec deploy/org2-ecert-ca -- tar zcvf - -C /var/hyperledger/fabric organizations/peerOrganizations/org2.example.com/msp > build/msp/msp-org2.example.com.tgz + + kubectl -n $NS delete configmap msp-config || true + kubectl -n $NS create configmap msp-config --from-file=build/msp/ + + pop_fn +} + +function launch_admin_CLIs() { + push_fn "Launching admin CLIs" + + cat kube/org0/org0-admin-cli.yaml | sed 's,{{FABRIC_VERSION}},'${FABRIC_VERSION}',g' | kubectl -n $NS apply -f - + cat kube/org1/org1-admin-cli.yaml | sed 's,{{FABRIC_VERSION}},'${FABRIC_VERSION}',g' | kubectl -n $NS apply -f - + cat kube/org2/org2-admin-cli.yaml | sed 's,{{FABRIC_VERSION}},'${FABRIC_VERSION}',g' | kubectl -n $NS apply -f - + + kubectl -n $NS rollout status deploy/org0-admin-cli + kubectl -n $NS rollout status deploy/org1-admin-cli + kubectl -n $NS rollout status deploy/org2-admin-cli + + pop_fn +} + +function create_genesis_block() { + push_fn "Creating channel \"${CHANNEL_NAME}\"" + + echo 'set -x + configtxgen -profile TwoOrgsApplicationGenesis -channelID '${CHANNEL_NAME}' -outputBlock genesis_block.pb + # configtxgen -inspectBlock genesis_block.pb + + osnadmin channel join --orderer-address org0-orderer1:9443 --channelID '${CHANNEL_NAME}' --config-block genesis_block.pb + osnadmin channel join --orderer-address org0-orderer2:9443 --channelID '${CHANNEL_NAME}' --config-block genesis_block.pb + osnadmin channel join --orderer-address org0-orderer3:9443 --channelID '${CHANNEL_NAME}' --config-block genesis_block.pb + + ' | exec kubectl -n $NS exec deploy/org0-admin-cli -i -- /bin/bash + + # todo: readiness / liveiness equivalent for channel ? Needs a little bit to settle before peers can join. + sleep 10 + + pop_fn +} + +function join_org_peers() { + local org=$1 + push_fn "Joining ${org} peers to channel \"${CHANNEL_NAME}\"" + + echo 'set -x + # Fetch the genesis block from an orderer + peer channel \ + fetch oldest \ + genesis_block.pb \ + -c '${CHANNEL_NAME}' \ + -o org0-orderer1:6050 \ + --tls --cafile /var/hyperledger/fabric/organizations/ordererOrganizations/org0.example.com/msp/tlscacerts/org0-tls-ca.pem + + # Join peer1 to the channel. + CORE_PEER_ADDRESS='${org}'-peer1:7051 \ + peer channel \ + join \ + -b genesis_block.pb \ + -o org0-orderer1:6050 \ + --tls --cafile /var/hyperledger/fabric/organizations/ordererOrganizations/org0.example.com/msp/tlscacerts/org0-tls-ca.pem + + # Join peer2 to the channel. + CORE_PEER_ADDRESS='${org}'-peer2:7051 \ + peer channel \ + join \ + -b genesis_block.pb \ + -o org0-orderer1:6050 \ + --tls --cafile /var/hyperledger/fabric/organizations/ordererOrganizations/org0.example.com/msp/tlscacerts/org0-tls-ca.pem + + ' | exec kubectl -n $NS exec deploy/${org}-admin-cli -i -- /bin/bash + + pop_fn +} + +function join_peers() { + join_org_peers org1 + join_org_peers org2 +} + +# Copy the scripts/anchor_peers.sh to a remote volume +function push_anchor_peer_script() { + local org=$1 + + tar cf - scripts/ | kubectl -n $NS exec -i -c main deploy/${org}-admin-cli -- tar xf - -C /var/hyperledger/fabric +} + +verify_result() { + if [ $1 -ne 0 ]; then + echo $2 + exit $1 + fi +} + +# Launch the anchor peer update script on a remote org admin CLI +function invoke_anchor_peer_update() { + local org_num=$1 + local peer_name=$2 + + kubectl exec \ + -n $NS \ + -c main \ + deploy/org${org_num}-admin-cli \ + -i \ + /bin/bash -c "/var/hyperledger/fabric/scripts/set_anchor_peer.sh ${org_num} ${CHANNEL_NAME} ${peer_name}" + + verify_result $? "Error updating anchor peer for org ${org_num}" +} + +# +# To update the anchor peers we will need to execute a script on each of the peer admin CLI containers. These +# commands can be individually piped into kubectl exec ... but it will be simpler if we transfer the anchor +# peer update script over to the org volume and then trigger it from kubectl. +# +function update_anchor_peers() { + local peer_name=$1 + push_fn "Updating anchor peers to ${peer_name}" + + push_anchor_peer_script org1 + push_anchor_peer_script org2 + + invoke_anchor_peer_update 1 ${peer_name} + invoke_anchor_peer_update 2 ${peer_name} + + pop_fn +} + +function channel_up() { + + create_channel_MSP + aggregate_channel_MSP + launch_admin_CLIs + + create_genesis_block + join_peers + + # peer1 was set as the anchor peer in configtx.yaml. Setting this again will force an + # error to be returned from the channel up. We might want to render the warning in + # this case to indicate that the call was made but had a nonzero exit. + # update_anchor_peers peer1 +} \ No newline at end of file diff --git a/test-network-k8s/scripts/fabric_CAs.sh b/test-network-k8s/scripts/fabric_CAs.sh new file mode 100755 index 00000000..6ec4708a --- /dev/null +++ b/test-network-k8s/scripts/fabric_CAs.sh @@ -0,0 +1,137 @@ +#!/bin/bash +# +# Copyright IBM Corp All Rights Reserved +# +# SPDX-License-Identifier: Apache-2.0 +# + +function launch_TLS_CAs() { + push_fn "Launching TLS CAs" + + kubectl -n $NS apply -f kube/org0/org0-tls-ca.yaml + kubectl -n $NS apply -f kube/org1/org1-tls-ca.yaml + kubectl -n $NS apply -f kube/org2/org2-tls-ca.yaml + + kubectl -n $NS rollout status deploy/org0-tls-ca + kubectl -n $NS rollout status deploy/org1-tls-ca + kubectl -n $NS rollout status deploy/org2-tls-ca + + # todo: this papers over a nasty bug whereby the CAs are ready, but sporadically refuse connections after a down / up + sleep 10 + + pop_fn +} + +function launch_ECert_CAs() { + push_fn "Launching ECert CAs" + + kubectl -n $NS apply -f kube/org0/org0-ecert-ca.yaml + kubectl -n $NS apply -f kube/org1/org1-ecert-ca.yaml + kubectl -n $NS apply -f kube/org2/org2-ecert-ca.yaml + + kubectl -n $NS rollout status deploy/org0-ecert-ca + kubectl -n $NS rollout status deploy/org1-ecert-ca + kubectl -n $NS rollout status deploy/org2-ecert-ca + + # todo: this papers over a nasty bug whereby the CAs are ready, but sporadically refuse connections after a down / up + sleep 10 + + pop_fn +} + +# Enroll bootstrap user with TLS CA +# https://hyperledger-fabric-ca.readthedocs.io/en/latest/deployguide/cadeploy.html#enroll-bootstrap-user-with-tls-ca +function enroll_bootstrap_TLS_CA_user() { + local org=$1 + local auth=$2 + local tlsca=${org}-tls-ca + + # todo: get rid of export here - put in yaml + + echo 'set -x + + mkdir -p $FABRIC_CA_CLIENT_HOME/tls-root-cert + cp $FABRIC_CA_SERVER_HOME/ca-cert.pem $FABRIC_CA_CLIENT_HOME/tls-root-cert/tls-ca-cert.pem + + fabric-ca-client enroll \ + --url https://'$auth'@'${tlsca}' \ + --tls.certfiles $FABRIC_CA_CLIENT_HOME/tls-root-cert/tls-ca-cert.pem \ + --csr.hosts '${tlsca}' \ + --mspdir $FABRIC_CA_CLIENT_HOME/tls-ca/tlsadmin/msp + + ' | exec kubectl -n $NS exec deploy/${tlsca} -i -- /bin/sh +} + +function enroll_bootstrap_TLS_CA_users() { + push_fn "Enrolling bootstrap TLS CA users" + + enroll_bootstrap_TLS_CA_user org0 $TLSADMIN_AUTH + enroll_bootstrap_TLS_CA_user org1 $TLSADMIN_AUTH + enroll_bootstrap_TLS_CA_user org2 $TLSADMIN_AUTH + + pop_fn +} + +function register_enroll_ECert_CA_bootstrap_user() { + local org=$1 + local tlsauth=$2 + local tlsca=${org}-tls-ca + local ecertca=${org}-ecert-ca + + echo 'set -x + + fabric-ca-client register \ + --id.name rcaadmin \ + --id.secret rcaadminpw \ + --url https://'${tlsca}' \ + --tls.certfiles $FABRIC_CA_CLIENT_HOME/tls-root-cert/tls-ca-cert.pem \ + --mspdir $FABRIC_CA_CLIENT_HOME/tls-ca/tlsadmin/msp + + fabric-ca-client enroll \ + --url https://'${tlsauth}'@'${tlsca}' \ + --tls.certfiles $FABRIC_CA_CLIENT_HOME/tls-root-cert/tls-ca-cert.pem \ + --csr.hosts '${ecertca}' \ + --mspdir $FABRIC_CA_CLIENT_HOME/tls-ca/rcaadmin/msp + + # Important: the rcaadmin signing certificate is referenced by the ECert CA FABRIC_CA_SERVER_TLS_CERTFILE config attribute. + # For simplicity, reference the key at a fixed, known location + cp $FABRIC_CA_CLIENT_HOME/tls-ca/rcaadmin/msp/keystore/*_sk $FABRIC_CA_CLIENT_HOME/tls-ca/rcaadmin/msp/keystore/key.pem + + ' | exec kubectl -n $NS exec deploy/${tlsca} -i -- /bin/sh +} + +# https://hyperledger-fabric-ca.readthedocs.io/en/latest/deployguide/cadeploy.html#register-and-enroll-the-organization-ca-bootstrap-identity-with-the-tls-ca +function register_enroll_ECert_CA_bootstrap_users() { + push_fn "Registering and enrolling ECert CA bootstrap users" + + register_enroll_ECert_CA_bootstrap_user org0 $TLSADMIN_AUTH + register_enroll_ECert_CA_bootstrap_user org1 $TLSADMIN_AUTH + register_enroll_ECert_CA_bootstrap_user org2 $TLSADMIN_AUTH + + pop_fn +} + +function enroll_bootstrap_ECert_CA_user() { + local org=$1 + local auth=$2 + local ecert_ca=${org}-ecert-ca + + echo 'set -x + + fabric-ca-client enroll \ + --url https://'${auth}'@'${ecert_ca}' \ + --tls.certfiles $FABRIC_CA_CLIENT_HOME/tls-root-cert/tls-ca-cert.pem \ + --mspdir $FABRIC_CA_CLIENT_HOME/'${ecert_ca}'/rcaadmin/msp + + ' | exec kubectl -n $NS exec deploy/${ecert_ca} -i -- /bin/sh +} + +function enroll_bootstrap_ECert_CA_users() { + push_fn "Enrolling bootstrap ECert CA users" + + enroll_bootstrap_ECert_CA_user org0 $RCAADMIN_AUTH + enroll_bootstrap_ECert_CA_user org1 $RCAADMIN_AUTH + enroll_bootstrap_ECert_CA_user org2 $RCAADMIN_AUTH + + pop_fn +} \ No newline at end of file diff --git a/test-network-k8s/scripts/fabric_config.sh b/test-network-k8s/scripts/fabric_config.sh new file mode 100755 index 00000000..03ef9d9e --- /dev/null +++ b/test-network-k8s/scripts/fabric_config.sh @@ -0,0 +1,42 @@ +#!/bin/bash +# +# Copyright IBM Corp All Rights Reserved +# +# SPDX-License-Identifier: Apache-2.0 +# + +function init_namespace() { + push_fn "Creating namespace \"$NS\"" + + kubectl create namespace $NS || true + + pop_fn +} + +function init_storage_volumes() { + push_fn "Provisioning volume storage" + + kubectl create -f kube/pv-fabric-org0.yaml || true + kubectl create -f kube/pv-fabric-org1.yaml || true + kubectl create -f kube/pv-fabric-org2.yaml || true + + kubectl -n $NS create -f kube/pvc-fabric-org0.yaml || true + kubectl -n $NS create -f kube/pvc-fabric-org1.yaml || true + kubectl -n $NS create -f kube/pvc-fabric-org2.yaml || true + + pop_fn +} + +function load_org_config() { + push_fn "Creating fabric config maps" + + kubectl -n $NS delete configmap org0-config || true + kubectl -n $NS delete configmap org1-config || true + kubectl -n $NS delete configmap org2-config || true + + kubectl -n $NS create configmap org0-config --from-file=config/org0 + kubectl -n $NS create configmap org1-config --from-file=config/org1 + kubectl -n $NS create configmap org2-config --from-file=config/org2 + + pop_fn +} \ No newline at end of file diff --git a/test-network-k8s/scripts/kind.sh b/test-network-k8s/scripts/kind.sh new file mode 100755 index 00000000..1784204d --- /dev/null +++ b/test-network-k8s/scripts/kind.sh @@ -0,0 +1,134 @@ +#!/bin/bash +# +# Copyright IBM Corp All Rights Reserved +# +# SPDX-License-Identifier: Apache-2.0 +# + +function pull_docker_images() { + push_fn "Pulling docker images for Fabric ${FABRIC_VERSION}" + + docker pull hyperledger/fabric-ca:$FABRIC_CA_VERSION + docker pull hyperledger/fabric-orderer:$FABRIC_VERSION + docker pull hyperledger/fabric-peer:$FABRIC_VERSION + docker pull hyperledger/fabric-tools:$FABRIC_VERSION + + pop_fn +} + +function apply_nginx_ingress() { + push_fn "Launching Nginx ingress controller" + + # This ingress-nginx.yaml was generated 9/24 from https://raw.githubusercontent.com/kubernetes/ingress-nginx/main/deploy/static/provider/kind/deploy.yaml + # with modifications for ssl-passthrough required to launch IBP-support with the nginx ingress. + # It may be preferable to always load from the remote mainline? + # kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/main/deploy/static/provider/kind/deploy.yaml + kubectl apply -f kube/ingress-nginx.yaml + + pop_fn +} + +function kind_create() { + push_fn "Creating cluster \"${CLUSTER_NAME}\"" + + # todo: always delete? Maybe return no-op if the cluster already exists? + kind delete cluster --name $CLUSTER_NAME + + local reg_name=${CONTAINER_REGISTRY_NAME} + local reg_port=${CONTAINER_REGISTRY_PORT} + local ingress_http_port=${NGINX_HTTP_PORT} + local ingress_https_port=${NGINX_HTTPS_PORT} + + cat </dev/null || true)" + if [ "${running}" != 'true' ]; then + docker run \ + -d --restart=always -p "127.0.0.1:${reg_port}:5000" --name "${reg_name}" \ + registry:2 + fi + + # connect the registry to the cluster network + # (the network may already be connected) + docker network connect "kind" "${reg_name}" || true + + # Document the local registry + # https://github.com/kubernetes/enhancements/tree/master/keps/sig-cluster-lifecycle/generic/1755-communicating-a-local-registry + cat < /dev/null + if [[ $? -ne 0 ]]; then + echo "No 'docker' binary available? (https://www.docker.com)" + exit 1 + fi + + kind version > /dev/null + if [[ $? -ne 0 ]]; then + echo "No 'kind' binary available? (https://kind.sigs.k8s.io/docs/user/quick-start/#installation)" + exit 1 + fi + + kubectl > /dev/null + if [[ $? -ne 0 ]]; then + echo "No 'kubectl' binary available? (https://kubernetes.io/docs/tasks/tools/)" + exit 1 + fi +} \ No newline at end of file diff --git a/test-network-k8s/scripts/rest_sample.sh b/test-network-k8s/scripts/rest_sample.sh new file mode 100755 index 00000000..fe397164 --- /dev/null +++ b/test-network-k8s/scripts/rest_sample.sh @@ -0,0 +1,91 @@ +#!/bin/bash +# +# Copyright IBM Corp All Rights Reserved +# +# SPDX-License-Identifier: Apache-2.0 +# + +function extract_MSP_archives() { + mkdir -p build/msp + + kubectl -n $NS exec deploy/org1-ecert-ca -- tar zcf - -C /var/hyperledger/fabric organizations/peerOrganizations/org1.example.com/msp | tar zxf - -C build/msp + kubectl -n $NS exec deploy/org2-ecert-ca -- tar zcf - -C /var/hyperledger/fabric organizations/peerOrganizations/org2.example.com/msp | tar zxf - -C build/msp + + kubectl -n $NS exec deploy/org1-ecert-ca -- tar zcf - -C /var/hyperledger/fabric organizations/peerOrganizations/org1.example.com/users/Admin@org1.example.com/msp | tar zxf - -C build/msp + kubectl -n $NS exec deploy/org2-ecert-ca -- tar zcf - -C /var/hyperledger/fabric organizations/peerOrganizations/org2.example.com/users/Admin@org2.example.com/msp | tar zxf - -C build/msp +} + +function one_line_pem { + echo "`awk 'NF {sub(/\\n/, ""); printf "%s\\\\\\\n",$0;}' $1`" +} + +function json_ccp { + local ORG=$1 + local PP=$(one_line_pem $2) + local CP=$(one_line_pem $3) + sed -e "s/\${ORG}/$ORG/" \ + -e "s#\${PEERPEM}#$PP#" \ + -e "s#\${CAPEM}#$CP#" \ + scripts/ccp-template.json +} + +function construct_rest_sample_configmap() { + push_fn "Constructing fabric-rest-sample connection profiles" + + extract_MSP_archives + + mkdir -p build/fabric-rest-sample-config + + local peer_pem=build/msp/organizations/peerOrganizations/org1.example.com/msp/tlscacerts/org1-tls-ca.pem + local ca_pem=build/msp/organizations/peerOrganizations/org1.example.com/msp/cacerts/org1-ecert-ca.pem + + echo "$(json_ccp 1 $peer_pem $ca_pem)" > build/fabric-rest-sample-config/HLF_CONNECTION_PROFILE_ORG1 + + peer_pem=build/msp/organizations/peerOrganizations/org2.example.com/msp/tlscacerts/org2-tls-ca.pem + ca_pem=build/msp/organizations/peerOrganizations/org2.example.com/msp/cacerts/org2-ecert-ca.pem + + echo "$(json_ccp 2 $peer_pem $ca_pem)" > build/fabric-rest-sample-config/HLF_CONNECTION_PROFILE_ORG2 + + cat build/msp/organizations/peerOrganizations/org1.example.com/users/Admin\@org1.example.com/msp/signcerts/cert.pem > build/fabric-rest-sample-config/HLF_CERTIFICATE_ORG1 + cat build/msp/organizations/peerOrganizations/org2.example.com/users/Admin\@org2.example.com/msp/signcerts/cert.pem > build/fabric-rest-sample-config/HLF_CERTIFICATE_ORG2 + + cat build/msp/organizations/peerOrganizations/org1.example.com/users/Admin\@org1.example.com/msp/keystore/server.key > build/fabric-rest-sample-config/HLF_PRIVATE_KEY_ORG1 + cat build/msp/organizations/peerOrganizations/org2.example.com/users/Admin\@org2.example.com/msp/keystore/server.key > build/fabric-rest-sample-config/HLF_PRIVATE_KEY_ORG2 + + kubectl -n $NS delete configmap fabric-rest-sample-config || true + kubectl -n $NS create configmap fabric-rest-sample-config --from-file=build/fabric-rest-sample-config/ + + pop_fn +} + +# todo: Make sure to port this to IKS / ICP +function ensure_rest_sample_image() { + push_fn "Ensuring fabric-rest-sample image" + + # todo: apply a tag / label to avoid pulling :latest from ghcr.io + + pop_fn 0 +} + +function rollout_rest_sample() { + push_fn "Starting fabric-rest-sample" + + kubectl -n $NS apply -f kube/fabric-rest-sample.yaml + kubectl -n $NS rollout status deploy/fabric-rest-sample + + pop_fn +} + +function launch_rest_sample() { + ensure_rest_sample_image + construct_rest_sample_configmap + rollout_rest_sample + + log "" + log "The fabric-rest-sample has started. See https://github.com/hyperledgendary/fabric-rest-sample/tree/main/asset-transfer-basic/rest-api-typescript#rest-api for additional usage." + log "To access the endpoint:" + log "" + log "export SAMPLE_APIKEY=97834158-3224-4CE7-95F9-A148C886653E" + log 'curl -s --header "X-Api-Key: ${SAMPLE_APIKEY}" http://localhost/api/assets' + log "" +} \ No newline at end of file diff --git a/test-network-k8s/scripts/set_anchor_peer.sh b/test-network-k8s/scripts/set_anchor_peer.sh new file mode 100755 index 00000000..59551f5e --- /dev/null +++ b/test-network-k8s/scripts/set_anchor_peer.sh @@ -0,0 +1,110 @@ +#!/bin/bash +# +# Copyright IBM Corp. All Rights Reserved. +# +# SPDX-License-Identifier: Apache-2.0 +# + +function fetch_channel_config() { + local output=$1 + + echo "Fetching the most recent configuration block for channel ${CHANNEL_NAME}" + peer channel \ + fetch config config_block.pb \ + -o org0-orderer1:6050 \ + -c ${CHANNEL_NAME} \ + --tls --cafile ${ORDERER_TLS_CA_FILE} + + echo "Decoding config block to JSON and isolating config to ${output}" + configtxlator proto_decode \ + --input config_block.pb \ + --type common.Block \ + | jq .data.data[0].payload.data.config > ${output} +} + +verify_result() { + if [ $1 -ne 0 ]; then + echo $2 + exit $1 + fi +} + +function create_config_update() { + local original=$1 + local modified=$2 + local output=$3 + + configtxlator proto_encode --input "${original}" --type common.Config > original_config.pb + configtxlator proto_encode --input "${modified}" --type common.Config > modified_config.pb + + # returns non-zero if no updates were detected between current and new config + configtxlator compute_update --channel_id "${CHANNEL_NAME}" --original original_config.pb --updated modified_config.pb > config_update.pb + if [ $? -ne 0 ]; then + echo "Anchor peer has already been set to ${ANCHOR_PEER_HOST}:${ANCHOR_PEER_PORT} - no update required." + return 1 + fi + + configtxlator proto_decode --input config_update.pb --type common.ConfigUpdate > config_update.json + echo '{"payload":{"header":{"channel_header":{"channel_id":"'${CHANNEL_NAME}'", "type":2}},"data":{"config_update":'$(cat config_update.json)'}}}' | jq . > config_update_in_envelope.json + configtxlator proto_encode --input config_update_in_envelope.json --type common.Envelope > ${output} + + return 0 +} + +function create_anchor_peer_update() { + echo "Generating anchor peer update transaction for Org${ORG_NUM} on channel ${CHANNEL_NAME}" + fetch_channel_config config.json + + set -x + # Modify the configuration to append the anchor peer + jq '.channel_group.groups.Application.groups.'${CORE_PEER_LOCALMSPID}'.values += {"AnchorPeers":{"mod_policy": "Admins","value":{"anchor_peers": [{"host": "'${ANCHOR_PEER_HOST}'","port": '${ANCHOR_PEER_PORT}'}]},"version": "0"}}' config.json > modified_config.json + { set +x; } 2>/dev/null + + # Compute a config update, based on the differences between + # config.json and modified_config.json, write + # it as a transaction to anchors.tx + create_config_update config.json modified_config.json anchors.tx + return $? +} + +function update_anchor_peer() { + peer channel \ + update -f anchors.tx \ + -o org0-orderer1:6050 \ + -c ${CHANNEL_NAME} \ + --tls --cafile ${ORDERER_TLS_CA_FILE} >& log.txt + + res=$? + cat log.txt + + verify_result $res "Anchor peer update failed" + + echo "Anchor peer set for org ${ORG_NAME} on channel ${CHANNEL_NAME} to ${ANCHOR_PEER_HOST}:${ANCHOR_PEER_PORT}" +} + +function set_anchor_peer() { + echo "Updating org ${ORG_NUM} anchor peer for channel ${CHANNEL_NAME} to ${ANCHOR_PEER_HOST}:${ANCHOR_PEER_PORT}" + + create_anchor_peer_update + res=$? + + if [ $res -eq 0 ]; then + update_anchor_peer + fi +} + +set -x + +ORG_NUM=$1 +CHANNEL_NAME=$2 +PEER_NAME=$3 +ORG_NAME="org${ORG_NUM}" +ANCHOR_PEER_HOST=${ORG_NAME}-${PEER_NAME} +ANCHOR_PEER_PORT=7051 +ORDERER_TLS_CA_FILE=/var/hyperledger/fabric/organizations/ordererOrganizations/org0.example.com/msp/tlscacerts/org0-tls-ca.pem + +export CORE_PEER_LOCALMSPID="Org${ORG_NUM}MSP" + +set_anchor_peer + +{ set +x; } 2>/dev/null diff --git a/test-network-k8s/scripts/test_network.sh b/test-network-k8s/scripts/test_network.sh new file mode 100755 index 00000000..a0a0c87a --- /dev/null +++ b/test-network-k8s/scripts/test_network.sh @@ -0,0 +1,259 @@ +#!/bin/bash +# +# Copyright IBM Corp All Rights Reserved +# +# SPDX-License-Identifier: Apache-2.0 +# + +# todo: oof this is rough. + + +function launch() { + local yaml=$1 + cat ${yaml} | sed 's,{{FABRIC_VERSION}},'${FABRIC_VERSION}',g' | kubectl -n $NS apply -f - +} + +function launch_orderers() { + push_fn "Launching orderers" + + launch kube/org0/org0-orderer1.yaml + launch kube/org0/org0-orderer2.yaml + launch kube/org0/org0-orderer3.yaml + + kubectl -n $NS rollout status deploy/org0-orderer1 + kubectl -n $NS rollout status deploy/org0-orderer2 + kubectl -n $NS rollout status deploy/org0-orderer3 + + pop_fn +} + +function launch_peers() { + push_fn "Launching peers" + + launch kube/org1/org1-peer1.yaml + launch kube/org1/org1-peer2.yaml + launch kube/org2/org2-peer1.yaml + launch kube/org2/org2-peer2.yaml + + kubectl -n $NS rollout status deploy/org1-peer1 + kubectl -n $NS rollout status deploy/org1-peer2 + kubectl -n $NS rollout status deploy/org2-peer1 + kubectl -n $NS rollout status deploy/org2-peer2 + + pop_fn +} + +function create_org0_local_MSP() { + echo 'set -x + export FABRIC_CA_CLIENT_HOME=/var/hyperledger/fabric-ca-client + export FABRIC_CA_CLIENT_TLS_CERTFILES=$FABRIC_CA_CLIENT_HOME/tls-root-cert/tls-ca-cert.pem + + # Each identity in the network needs a registration and enrollment. + fabric-ca-client register --id.name org0-orderer1 --id.secret ordererpw --id.type orderer --url https://org0-ecert-ca --mspdir $FABRIC_CA_CLIENT_HOME/org0-ecert-ca/rcaadmin/msp + fabric-ca-client register --id.name org0-orderer2 --id.secret ordererpw --id.type orderer --url https://org0-ecert-ca --mspdir $FABRIC_CA_CLIENT_HOME/org0-ecert-ca/rcaadmin/msp + fabric-ca-client register --id.name org0-orderer3 --id.secret ordererpw --id.type orderer --url https://org0-ecert-ca --mspdir $FABRIC_CA_CLIENT_HOME/org0-ecert-ca/rcaadmin/msp + fabric-ca-client register --id.name org0-admin --id.secret org0adminpw --id.type admin --url https://org0-ecert-ca --mspdir $FABRIC_CA_CLIENT_HOME/org0-ecert-ca/rcaadmin/msp --id.attrs "hf.Registrar.Roles=client,hf.Registrar.Attributes=*,hf.Revoker=true,hf.GenCRL=true,admin=true:ecert,abac.init=true:ecert" + + fabric-ca-client enroll --url https://org0-orderer1:ordererpw@org0-ecert-ca --csr.hosts org0-orderer1 --mspdir /var/hyperledger/fabric/organizations/ordererOrganizations/org0.example.com/orderers/org0-orderer1.org0.example.com/msp + fabric-ca-client enroll --url https://org0-orderer2:ordererpw@org0-ecert-ca --csr.hosts org0-orderer2 --mspdir /var/hyperledger/fabric/organizations/ordererOrganizations/org0.example.com/orderers/org0-orderer2.org0.example.com/msp + fabric-ca-client enroll --url https://org0-orderer3:ordererpw@org0-ecert-ca --csr.hosts org0-orderer3 --mspdir /var/hyperledger/fabric/organizations/ordererOrganizations/org0.example.com/orderers/org0-orderer3.org0.example.com/msp + fabric-ca-client enroll --url https://org0-admin:org0adminpw@org0-ecert-ca --mspdir /var/hyperledger/fabric/organizations/ordererOrganizations/org0.example.com/users/Admin@org0.example.com/msp + + # Each node in the network needs a TLS registration and enrollment. + fabric-ca-client register --id.name org0-orderer1 --id.secret ordererpw --id.type orderer --url https://org0-tls-ca --mspdir $FABRIC_CA_CLIENT_HOME/tls-ca/tlsadmin/msp + fabric-ca-client register --id.name org0-orderer2 --id.secret ordererpw --id.type orderer --url https://org0-tls-ca --mspdir $FABRIC_CA_CLIENT_HOME/tls-ca/tlsadmin/msp + fabric-ca-client register --id.name org0-orderer3 --id.secret ordererpw --id.type orderer --url https://org0-tls-ca --mspdir $FABRIC_CA_CLIENT_HOME/tls-ca/tlsadmin/msp + + fabric-ca-client enroll --url https://org0-orderer1:ordererpw@org0-tls-ca --csr.hosts org0-orderer1 --mspdir /var/hyperledger/fabric/organizations/ordererOrganizations/org0.example.com/orderers/org0-orderer1.org0.example.com/tls + fabric-ca-client enroll --url https://org0-orderer2:ordererpw@org0-tls-ca --csr.hosts org0-orderer2 --mspdir /var/hyperledger/fabric/organizations/ordererOrganizations/org0.example.com/orderers/org0-orderer2.org0.example.com/tls + fabric-ca-client enroll --url https://org0-orderer3:ordererpw@org0-tls-ca --csr.hosts org0-orderer3 --mspdir /var/hyperledger/fabric/organizations/ordererOrganizations/org0.example.com/orderers/org0-orderer3.org0.example.com/tls + + # Copy the TLS signing keys to a fixed path for convenience when starting the orderers. + cp /var/hyperledger/fabric/organizations/ordererOrganizations/org0.example.com/orderers/org0-orderer1.org0.example.com/tls/keystore/*_sk /var/hyperledger/fabric/organizations/ordererOrganizations/org0.example.com/orderers/org0-orderer1.org0.example.com/tls/keystore/server.key + cp /var/hyperledger/fabric/organizations/ordererOrganizations/org0.example.com/orderers/org0-orderer2.org0.example.com/tls/keystore/*_sk /var/hyperledger/fabric/organizations/ordererOrganizations/org0.example.com/orderers/org0-orderer2.org0.example.com/tls/keystore/server.key + cp /var/hyperledger/fabric/organizations/ordererOrganizations/org0.example.com/orderers/org0-orderer3.org0.example.com/tls/keystore/*_sk /var/hyperledger/fabric/organizations/ordererOrganizations/org0.example.com/orderers/org0-orderer3.org0.example.com/tls/keystore/server.key + + # Create an MSP config.yaml (why is this not generated by the enrollment by fabric-ca-client?) + echo "NodeOUs: + Enable: true + ClientOUIdentifier: + Certificate: cacerts/org0-ecert-ca.pem + OrganizationalUnitIdentifier: client + PeerOUIdentifier: + Certificate: cacerts/org0-ecert-ca.pem + OrganizationalUnitIdentifier: peer + AdminOUIdentifier: + Certificate: cacerts/org0-ecert-ca.pem + OrganizationalUnitIdentifier: admin + OrdererOUIdentifier: + Certificate: cacerts/org0-ecert-ca.pem + OrganizationalUnitIdentifier: orderer" > /var/hyperledger/fabric/organizations/ordererOrganizations/org0.example.com/orderers/org0-orderer1.org0.example.com/msp/config.yaml + + cp /var/hyperledger/fabric/organizations/ordererOrganizations/org0.example.com/orderers/org0-orderer1.org0.example.com/msp/config.yaml /var/hyperledger/fabric/organizations/ordererOrganizations/org0.example.com/orderers/org0-orderer2.org0.example.com/msp/config.yaml + cp /var/hyperledger/fabric/organizations/ordererOrganizations/org0.example.com/orderers/org0-orderer1.org0.example.com/msp/config.yaml /var/hyperledger/fabric/organizations/ordererOrganizations/org0.example.com/orderers/org0-orderer3.org0.example.com/msp/config.yaml + ' | exec kubectl -n $NS exec deploy/org0-ecert-ca -i -- /bin/sh +} + +function create_org1_local_MSP() { + + echo 'set -x + export FABRIC_CA_CLIENT_HOME=/var/hyperledger/fabric-ca-client + export FABRIC_CA_CLIENT_TLS_CERTFILES=$FABRIC_CA_CLIENT_HOME/tls-root-cert/tls-ca-cert.pem + + # Each identity in the network needs a registration and enrollment. + fabric-ca-client register --id.name org1-peer1 --id.secret peerpw --id.type peer --url https://org1-ecert-ca --mspdir $FABRIC_CA_CLIENT_HOME/org1-ecert-ca/rcaadmin/msp + fabric-ca-client register --id.name org1-peer2 --id.secret peerpw --id.type peer --url https://org1-ecert-ca --mspdir $FABRIC_CA_CLIENT_HOME/org1-ecert-ca/rcaadmin/msp + fabric-ca-client register --id.name org1-admin --id.secret org1adminpw --id.type admin --url https://org1-ecert-ca --mspdir $FABRIC_CA_CLIENT_HOME/org1-ecert-ca/rcaadmin/msp --id.attrs "hf.Registrar.Roles=client,hf.Registrar.Attributes=*,hf.Revoker=true,hf.GenCRL=true,admin=true:ecert,abac.init=true:ecert" + + fabric-ca-client enroll --url https://org1-peer1:peerpw@org1-ecert-ca --csr.hosts org1-peer1 --mspdir /var/hyperledger/fabric/organizations/peerOrganizations/org1.example.com/peers/org1-peer1.org1.example.com/msp + fabric-ca-client enroll --url https://org1-peer2:peerpw@org1-ecert-ca --csr.hosts org1-peer2 --mspdir /var/hyperledger/fabric/organizations/peerOrganizations/org1.example.com/peers/org1-peer2.org1.example.com/msp + fabric-ca-client enroll --url https://org1-admin:org1adminpw@org1-ecert-ca --mspdir /var/hyperledger/fabric/organizations/peerOrganizations/org1.example.com/users/Admin@org1.example.com/msp + + # Each node in the network needs a TLS registration and enrollment. + fabric-ca-client register --id.name org1-peer1 --id.secret peerpw --id.type peer --url https://org1-tls-ca --mspdir $FABRIC_CA_CLIENT_HOME/tls-ca/tlsadmin/msp + fabric-ca-client register --id.name org1-peer2 --id.secret peerpw --id.type peer --url https://org1-tls-ca --mspdir $FABRIC_CA_CLIENT_HOME/tls-ca/tlsadmin/msp + + fabric-ca-client enroll --url https://org1-peer1:peerpw@org1-tls-ca --csr.hosts org1-peer1 --mspdir /var/hyperledger/fabric/organizations/peerOrganizations/org1.example.com/peers/org1-peer1.org1.example.com/tls + fabric-ca-client enroll --url https://org1-peer2:peerpw@org1-tls-ca --csr.hosts org1-peer2 --mspdir /var/hyperledger/fabric/organizations/peerOrganizations/org1.example.com/peers/org1-peer2.org1.example.com/tls + + # Copy the TLS signing keys to a fixed path for convenience when launching the peers + cp /var/hyperledger/fabric/organizations/peerOrganizations/org1.example.com/peers/org1-peer1.org1.example.com/tls/keystore/*_sk /var/hyperledger/fabric/organizations/peerOrganizations/org1.example.com/peers/org1-peer1.org1.example.com/tls/keystore/server.key + cp /var/hyperledger/fabric/organizations/peerOrganizations/org1.example.com/peers/org1-peer2.org1.example.com/tls/keystore/*_sk /var/hyperledger/fabric/organizations/peerOrganizations/org1.example.com/peers/org1-peer2.org1.example.com/tls/keystore/server.key + + cp /var/hyperledger/fabric/organizations/peerOrganizations/org1.example.com/users/Admin@org1.example.com/msp/keystore/*_sk /var/hyperledger/fabric/organizations/peerOrganizations/org1.example.com/users/Admin@org1.example.com/msp/keystore/server.key + + # Create local MSP config.yaml + echo "NodeOUs: + Enable: true + ClientOUIdentifier: + Certificate: cacerts/org1-ecert-ca.pem + OrganizationalUnitIdentifier: client + PeerOUIdentifier: + Certificate: cacerts/org1-ecert-ca.pem + OrganizationalUnitIdentifier: peer + AdminOUIdentifier: + Certificate: cacerts/org1-ecert-ca.pem + OrganizationalUnitIdentifier: admin + OrdererOUIdentifier: + Certificate: cacerts/org1-ecert-ca.pem + OrganizationalUnitIdentifier: orderer" > /var/hyperledger/fabric/organizations/peerOrganizations/org1.example.com/peers/org1-peer1.org1.example.com/msp/config.yaml + + + cp /var/hyperledger/fabric/organizations/peerOrganizations/org1.example.com/peers/org1-peer1.org1.example.com/msp/config.yaml /var/hyperledger/fabric/organizations/peerOrganizations/org1.example.com/peers/org1-peer2.org1.example.com/msp/config.yaml + cp /var/hyperledger/fabric/organizations/peerOrganizations/org1.example.com/peers/org1-peer1.org1.example.com/msp/config.yaml /var/hyperledger/fabric/organizations/peerOrganizations/org1.example.com/users/Admin@org1.example.com/msp/config.yaml + ' | exec kubectl -n $NS exec deploy/org1-ecert-ca -i -- /bin/sh + +} + +function create_org2_local_MSP() { + echo 'set -x + export FABRIC_CA_CLIENT_HOME=/var/hyperledger/fabric-ca-client + export FABRIC_CA_CLIENT_TLS_CERTFILES=$FABRIC_CA_CLIENT_HOME/tls-root-cert/tls-ca-cert.pem + + # Each identity in the network needs a registration and enrollment. + fabric-ca-client register --id.name org2-peer1 --id.secret peerpw --id.type peer --url https://org2-ecert-ca --mspdir $FABRIC_CA_CLIENT_HOME/org2-ecert-ca/rcaadmin/msp + fabric-ca-client register --id.name org2-peer2 --id.secret peerpw --id.type peer --url https://org2-ecert-ca --mspdir $FABRIC_CA_CLIENT_HOME/org2-ecert-ca/rcaadmin/msp + fabric-ca-client register --id.name org2-admin --id.secret org2adminpw --id.type admin --url https://org2-ecert-ca --mspdir $FABRIC_CA_CLIENT_HOME/org2-ecert-ca/rcaadmin/msp --id.attrs "hf.Registrar.Roles=client,hf.Registrar.Attributes=*,hf.Revoker=true,hf.GenCRL=true,admin=true:ecert,abac.init=true:ecert" + + fabric-ca-client enroll --url https://org2-peer1:peerpw@org2-ecert-ca --csr.hosts org2-peer1 --mspdir /var/hyperledger/fabric/organizations/peerOrganizations/org2.example.com/peers/org2-peer1.org2.example.com/msp + fabric-ca-client enroll --url https://org2-peer2:peerpw@org2-ecert-ca --csr.hosts org2-peer2 --mspdir /var/hyperledger/fabric/organizations/peerOrganizations/org2.example.com/peers/org2-peer2.org2.example.com/msp + fabric-ca-client enroll --url https://org2-admin:org2adminpw@org2-ecert-ca --mspdir /var/hyperledger/fabric/organizations/peerOrganizations/org2.example.com/users/Admin@org2.example.com/msp + + # Each node in the network needs a TLS registration and enrollment. + fabric-ca-client register --id.name org2-peer1 --id.secret peerpw --id.type peer --url https://org2-tls-ca --mspdir $FABRIC_CA_CLIENT_HOME/tls-ca/tlsadmin/msp + fabric-ca-client register --id.name org2-peer2 --id.secret peerpw --id.type peer --url https://org2-tls-ca --mspdir $FABRIC_CA_CLIENT_HOME/tls-ca/tlsadmin/msp + + fabric-ca-client enroll --url https://org2-peer1:peerpw@org2-tls-ca --csr.hosts org2-peer1 --mspdir /var/hyperledger/fabric/organizations/peerOrganizations/org2.example.com/peers/org2-peer1.org2.example.com/tls + fabric-ca-client enroll --url https://org2-peer2:peerpw@org2-tls-ca --csr.hosts org2-peer2 --mspdir /var/hyperledger/fabric/organizations/peerOrganizations/org2.example.com/peers/org2-peer2.org2.example.com/tls + + # Copy the TLS signing keys to a fixed path for convenience when launching the peers + cp /var/hyperledger/fabric/organizations/peerOrganizations/org2.example.com/peers/org2-peer1.org2.example.com/tls/keystore/*_sk /var/hyperledger/fabric/organizations/peerOrganizations/org2.example.com/peers/org2-peer1.org2.example.com/tls/keystore/server.key + cp /var/hyperledger/fabric/organizations/peerOrganizations/org2.example.com/peers/org2-peer2.org2.example.com/tls/keystore/*_sk /var/hyperledger/fabric/organizations/peerOrganizations/org2.example.com/peers/org2-peer2.org2.example.com/tls/keystore/server.key + + cp /var/hyperledger/fabric/organizations/peerOrganizations/org2.example.com/users/Admin@org2.example.com/msp/keystore/*_sk /var/hyperledger/fabric/organizations/peerOrganizations/org2.example.com/users/Admin@org2.example.com/msp/keystore/server.key + + # Create local MSP config.yaml + echo "NodeOUs: + Enable: true + ClientOUIdentifier: + Certificate: cacerts/org2-ecert-ca.pem + OrganizationalUnitIdentifier: client + PeerOUIdentifier: + Certificate: cacerts/org2-ecert-ca.pem + OrganizationalUnitIdentifier: peer + AdminOUIdentifier: + Certificate: cacerts/org2-ecert-ca.pem + OrganizationalUnitIdentifier: admin + OrdererOUIdentifier: + Certificate: cacerts/org2-ecert-ca.pem + OrganizationalUnitIdentifier: orderer" > /var/hyperledger/fabric/organizations/peerOrganizations/org2.example.com/peers/org2-peer1.org2.example.com/msp/config.yaml + + cp /var/hyperledger/fabric/organizations/peerOrganizations/org2.example.com/peers/org2-peer1.org2.example.com/msp/config.yaml /var/hyperledger/fabric/organizations/peerOrganizations/org2.example.com/peers/org2-peer2.org2.example.com/msp/config.yaml + cp /var/hyperledger/fabric/organizations/peerOrganizations/org2.example.com/peers/org2-peer1.org2.example.com/msp/config.yaml /var/hyperledger/fabric/organizations/peerOrganizations/org2.example.com/users/Admin@org2.example.com/msp/config.yaml + ' | exec kubectl -n $NS exec deploy/org2-ecert-ca -i -- /bin/sh +} + +function create_local_MSP() { + push_fn "Creating local node MSP" + + create_org0_local_MSP + create_org1_local_MSP + create_org2_local_MSP + + pop_fn +} + +function network_up() { + + # Kube config + init_namespace + init_storage_volumes + load_org_config + + # Network TLS CAs + launch_TLS_CAs + enroll_bootstrap_TLS_CA_users + + # Network ECert CAs + register_enroll_ECert_CA_bootstrap_users + launch_ECert_CAs + enroll_bootstrap_ECert_CA_users + + # Test Network + create_local_MSP + launch_orderers + launch_peers +} + +function stop_services() { + push_fn "Stopping Fabric services" + + # These pods are busy executing `sleep MAX_INT` and do not shut down very quickly... +# kubectl -n $NS delete deployment/org0-admin-cli --grace-period=0 --force +# kubectl -n $NS delete deployment/org1-admin-cli --grace-period=0 --force +# kubectl -n $NS delete deployment/org2-admin-cli --grace-period=0 --force + + kubectl -n $NS delete deployment --all + kubectl -n $NS delete pod --all + kubectl -n $NS delete service --all + kubectl -n $NS delete configmap --all + kubectl -n $NS delete secret --all + + pop_fn +} + +function scrub_org_volumes() { + push_fn "Scrubbing Fabric volumes" + + # scrub all pv contents + kubectl -n $NS create -f kube/job-scrub-fabric-volumes.yaml + kubectl -n $NS wait --for=condition=complete --timeout=60s job/job-scrub-fabric-volumes + kubectl -n $NS delete jobs --all + + pop_fn +} + +function network_down() { + stop_services + scrub_org_volumes +} \ No newline at end of file diff --git a/test-network-k8s/scripts/utils.sh b/test-network-k8s/scripts/utils.sh new file mode 100644 index 00000000..a1631aa4 --- /dev/null +++ b/test-network-k8s/scripts/utils.sh @@ -0,0 +1,66 @@ +#!/bin/bash +# +# Copyright IBM Corp All Rights Reserved +# +# SPDX-License-Identifier: Apache-2.0 +# + +function logging_init() { + # Reset the output and debug log files + printf '' > ${LOG_FILE} > ${DEBUG_FILE} + + # Write all output to the control flow log to STDOUT + tail -f ${LOG_FILE} & + + # Call the exit handler when we exit. + trap "exit_fn" EXIT + + # Send stdout and stderr from child programs to the debug log file + exec 1>>${DEBUG_FILE} 2>>${DEBUG_FILE} +} + +function exit_fn() { + rc=$? + + # Write an error icon to the current logging statement. + if [ "0" -ne $rc ]; then + pop_fn $rc + fi + + # always remove the log trailer when the process exits. + pkill -P $$ +} + +function push_fn() { + #echo -ne " - entering ${FUNCNAME[1]} with arguments $@" + + echo -ne " - $@ ..." >> ${LOG_FILE} +} + +function log() { + echo -e $@ >> ${LOG_FILE} +} + +function pop_fn() { +# echo exiting ${FUNCNAME[1]} + + local res=$1 + if [ $# -eq 0 ]; then + echo -ne "\r✅" >> ${LOG_FILE} + + elif [ $res -eq 0 ]; then + echo -ne "\r✅" >> ${LOG_FILE} + + elif [ $res -eq 1 ]; then + echo -ne "\r⚠️" >> ${LOG_FILE} + + elif [ $res -eq 2 ]; then + echo -ne "\r☠️" >> ${LOG_FILE} + + else + echo -ne "\r" >> ${LOG_FILE} + fi + + echo "" >> ${LOG_FILE} +} +