diff --git a/test/chaincodes/server/.dockerignore b/test/chaincodes/server/.dockerignore new file mode 100644 index 00000000..f8b6bbd6 --- /dev/null +++ b/test/chaincodes/server/.dockerignore @@ -0,0 +1,3 @@ +node_modules +package.lock.json +package diff --git a/test/chaincodes/server/Dockerfile b/test/chaincodes/server/Dockerfile new file mode 100644 index 00000000..8dbb1a7e --- /dev/null +++ b/test/chaincodes/server/Dockerfile @@ -0,0 +1,7 @@ +FROM hyperledger/fabric-nodeenv:latest + +ADD . /opt/chaincode +RUN cd /opt/chaincode; npm install + +WORKDIR /opt/chaincode +ENTRYPOINT ["npm", "start"] diff --git a/test/chaincodes/server/index.js b/test/chaincodes/server/index.js new file mode 100644 index 00000000..7d1accc5 --- /dev/null +++ b/test/chaincodes/server/index.js @@ -0,0 +1,30 @@ +/* +# Copyright Hitachi America, Ltd. All Rights Reserved. +# +# SPDX-License-Identifier: Apache-2.0 +*/ +"use strict"; + +const { Contract } = require('fabric-contract-api'); + +class ServerTestChaincode extends Contract { + async unknownTransaction({stub}) { + const {fcn, params} = stub.getFunctionAndParameters(); + throw new Error(`Could not find chaincode function: ${fcn}`); + } + + constructor() { + super('org.mynamespace.server'); + } + + async putValue(ctx, value) { + await ctx.stub.putState('state1', Buffer.from(JSON.stringify(value))); + } + + async getValue(ctx) { + const value = await ctx.stub.getState('state1'); + return JSON.parse(value.toString()); + } +} + +exports.contracts = [ ServerTestChaincode ]; diff --git a/test/chaincodes/server/package.json b/test/chaincodes/server/package.json new file mode 100644 index 00000000..997f8322 --- /dev/null +++ b/test/chaincodes/server/package.json @@ -0,0 +1,21 @@ +{ + "name": "chaincode", + "description": "Chaincode server", + "engines": { + "node": "^12.13.0", + "npm": ">=5.3.0" + }, + "scripts": { + "start": "fabric-chaincode-node server" + }, + "main": "index.js", + "engine-strict": true, + "engineStrict": true, + "version": "1.0.0", + "author": "", + "license": "Apache-2.0", + "dependencies": { + "fabric-shim": "2.1.3-unstable", + "fabric-contract-api": "2.1.3-unstable" + } +} diff --git a/test/chaincodes/server/package/connection.json b/test/chaincodes/server/package/connection.json new file mode 100644 index 00000000..96f2571f --- /dev/null +++ b/test/chaincodes/server/package/connection.json @@ -0,0 +1,5 @@ +{ + "address": "cc-server:9999", + "dial_timeout": "10s", + "tls_required": false +} diff --git a/test/chaincodes/server/package/metadata.json b/test/chaincodes/server/package/metadata.json new file mode 100644 index 00000000..31b9bba2 --- /dev/null +++ b/test/chaincodes/server/package/metadata.json @@ -0,0 +1,5 @@ +{ + "path": "", + "type": "external", + "label": "server_v0" +} diff --git a/test/e2e/scenario.js b/test/e2e/scenario.js index 86c7aafd..a825327c 100644 --- a/test/e2e/scenario.js +++ b/test/e2e/scenario.js @@ -185,4 +185,7 @@ const installChaincode = async () => { ]); }; -exports.default = series(installChaincode, instantiateChaincode, invokeFunctions, queryFunctions); +const clientTests = series(installChaincode, instantiateChaincode, invokeFunctions, queryFunctions); +const serverTests = require('./server').default; + +exports.default = series(clientTests, serverTests); diff --git a/test/e2e/server.js b/test/e2e/server.js new file mode 100644 index 00000000..e3cddc28 --- /dev/null +++ b/test/e2e/server.js @@ -0,0 +1,165 @@ +/* +# Copyright Hitachi America, Ltd. All Rights Reserved. +# +# SPDX-License-Identifier: Apache-2.0 +*/ +'use strict'; + +const {series} = require('gulp'); + +const util = require('util'); +const path = require('path'); + +const { shell: runcmds , getTLSArgs, getPeerAddresses } = require('toolchain'); +const ip = require('ip'); + +const CHANNEL_NAME = 'mychannel'; + +const chaincodeDir = path.join(__dirname, '..', '..', 'test', 'chaincodes', 'server'); + +async function packageChaincode() { + await runcmds([ + util.format( + 'tar -C %s/package -cvzf %s/package/code.tar.gz connection.json', + chaincodeDir, chaincodeDir + ), + util.format( + 'tar -C %s/package -cvzf %s/package/chaincode.tar.gz code.tar.gz metadata.json', + chaincodeDir, chaincodeDir + ), + ]); +} + +async function buildChaincode() { + const npmrc = path.join(chaincodeDir, '.npmrc'); + + await runcmds([ + `echo "registry=http://${ip.address()}:4873" > ${npmrc}`, + util.format( + 'docker build --no-cache -t chaincode-e2e-server %s', + chaincodeDir + ), + `rm -f ${npmrc}` + ]); +} + +async function installChaincode() { + const peerInstall = 'peer lifecycle chaincode install /opt/gopath/src/github.com/chaincode/server/package/chaincode.tar.gz'; + + await runcmds([ + util.format( + 'docker exec %s %s', + 'org1_cli', + peerInstall + ), + util.format( + 'docker exec %s %s', + 'org2_cli', + peerInstall + ) + ]); +}; + +function findPackageId(queryOutput, label) { + const output = JSON.parse(queryOutput); + + const cc = output.installed_chaincodes.filter((chaincode) => chaincode.label === label); + if (cc.length !== 1) { + throw new Error('Failed to find installed chaincode'); + } + + return cc[0].package_id; +} + +async function instantiateChaincode() { + const endorsementPolicy = '"OR (\'Org1MSP.member\', \'Org2MSP.member\')"'; + const queryInstalled = util.format( + 'peer lifecycle chaincode queryinstalled --output json' + ); + const sequence = 1; + + const approveChaincode = util.format( + 'peer lifecycle chaincode approveformyorg -o %s %s -C %s -n %s -v %s --package-id %s --sequence %d --signature-policy %s', + 'orderer.example.com:7050', + getTLSArgs(), + CHANNEL_NAME, + 'server', + 'v0', + '%s', // To be filled in for each org + sequence, + endorsementPolicy + ); + + const outputs = await runcmds([ + util.format( + 'docker exec %s %s', + 'org1_cli', + queryInstalled + ), + util.format( + 'docker exec %s %s', + 'org2_cli', + queryInstalled + ), + ]); + + const packageIdOrg1 = findPackageId(outputs[0], 'server_v0'); + const packageIdOrg2 = findPackageId(outputs[1], 'server_v0'); + + // TODO: Assuming the two package IDs are the same + await runcmds([ + // Start the CC Server container + `docker run -e CORE_CHAINCODE_ID=${packageIdOrg1} -e CORE_CHAINCODE_ADDRESS=0.0.0.0:9999 -h cc-server --name cc-server -d --network node_default chaincode-e2e-server`, + // Approve the chaincode definition by each org + util.format('docker exec %s %s', + 'org1_cli', + util.format(approveChaincode, packageIdOrg1) + ), + util.format('docker exec %s %s', + 'org2_cli', + util.format(approveChaincode, packageIdOrg2) + ), + // Commit the chaincode definition + util.format('docker exec org1_cli peer lifecycle chaincode commit -o %s %s -C %s -n %s -v %s --sequence %d --signature-policy %s %s', + 'orderer.example.com:7050', + getTLSArgs(), + CHANNEL_NAME, + 'server', + 'v0', + sequence, + endorsementPolicy, + getPeerAddresses() + ) + ]); +} + +const invokeFunctions = async () => { + const args = util.format('docker exec org1_cli peer chaincode invoke %s -C %s -n %s -c %s --waitForEvent', + getTLSArgs(), + CHANNEL_NAME, + 'server', + '\'{"Args":["putValue","\'42\'"]}\''); + + await runcmds([args]); +}; + +const queryFunctions = async () => { + const args = util.format('docker exec org1_cli peer chaincode query %s -C %s -n %s -c %s', + getTLSArgs(), + CHANNEL_NAME, + 'server', + '\'{"Args":["getValue"]}\''); + + const ret = await runcmds([args]); + + const response = JSON.parse(ret[0]); + + if (response !== 42) { + throw new Error("Unexpected result from chaincode"); + } +} + +exports.default = series( + packageChaincode, buildChaincode, installChaincode, instantiateChaincode, + invokeFunctions, queryFunctions +); diff --git a/tools/toolchain/fabric.js b/tools/toolchain/fabric.js index 66b79846..d1b44f44 100644 --- a/tools/toolchain/fabric.js +++ b/tools/toolchain/fabric.js @@ -78,9 +78,12 @@ const _docker_clean = async () => { // stop and remove chaincode docker instances 'docker kill $(docker ps | grep "dev-peer0.org[12].example.com" | awk \'{print $1}\') || echo ok', 'docker rm $(docker ps -a | grep "dev-peer0.org[12].example.com" | awk \'{print $1}\') || echo ok', + 'docker kill $(docker ps | grep "cc-server" | awk \'{print $1}\') || echo ok', + 'docker rm $(docker ps -a | grep "cc-server" | awk \'{print $1}\') || echo ok', // remove chaincode images so that they get rebuilt during test 'docker rmi $(docker images | grep "^dev-peer0.org[12].example.com" | awk \'{print $3}\') || echo ok', + 'docker rmi $(docker images | grep "^chaincode-e2e-server" | awk \'{print $3}\') || echo ok', // clean up all the containers created by docker-compose util.format('docker-compose -f %s down --volumes', fs.realpathSync(path.join(dockerComposeDir, 'docker-compose-cli.yaml'))), @@ -140,6 +143,10 @@ const _generate_config = async () => { 'docker exec cli cp /etc/hyperledger/fabric/core.yaml %s', dockerCfgPath ), + util.format( + 'docker exec cli sed -i \'s/externalBuilders: \\[\\]/externalBuilders: [{path: \\/opt\\/chaincode, name: test}]/\' %s/core.yaml', + dockerCfgPath + ), util.format( 'docker exec cli sh %s/rename_sk.sh', dockerCfgPath @@ -197,10 +204,18 @@ async function _channel_create() { ]); } +async function _peer_setup() { + // Install the 'jq' command in the peer containers to run external builder scripts. + await runcmds([ + 'docker exec peer0.org1.example.com apk add jq', + 'docker exec peer0.org2.example.com apk add jq', + ]); +} + const channelSetup = series(_channel_create, _channel_init); // -- -const startFabric = series(dockerReady, channelSetup); +const startFabric = series(dockerReady, _peer_setup, channelSetup); exports.default = startFabric; exports.stopFabric = series(_docker_clean); diff --git a/tools/toolchain/network/docker-compose/docker-compose-base.yaml b/tools/toolchain/network/docker-compose/docker-compose-base.yaml index a1047260..02e4a6eb 100644 --- a/tools/toolchain/network/docker-compose/docker-compose-base.yaml +++ b/tools/toolchain/network/docker-compose/docker-compose-base.yaml @@ -99,6 +99,8 @@ services: command: peer node start --peer-chaincodedev=${DOCKER_DEVMODE} volumes: - /var/run/:/host/var/run/ + - ../external:/opt/chaincode/bin:ro + - ../crypto-material/core.yaml:/etc/hyperledger/fabric/core.yaml:ro clibase: extends: diff --git a/tools/toolchain/network/external/build b/tools/toolchain/network/external/build new file mode 100755 index 00000000..6773ec16 --- /dev/null +++ b/tools/toolchain/network/external/build @@ -0,0 +1,23 @@ +#!/bin/sh +# +# Copyright Hitachi America, Ltd. All Rights Reserved. +# +# SPDX-License-Identifier: Apache-2.0 + +set -e + +SOURCE="$1" +OUTPUT="$3" + +if [ ! -f "${SOURCE}/connection.json" ]; then + echo "Error: ${SOURCE}/connection.json not found" 1>&2 + exit 1 +fi + +cp "${SOURCE}/connection.json" "${OUTPUT}/connection.json" + +if [ -d "${SOURCE}/metadata" ]; then + cp -a ${SOURCE}/metadata ${OUTPUT}/metadata +fi + +exit 0 diff --git a/tools/toolchain/network/external/detect b/tools/toolchain/network/external/detect new file mode 100755 index 00000000..6ef69b54 --- /dev/null +++ b/tools/toolchain/network/external/detect @@ -0,0 +1,15 @@ +#!/bin/sh +# +# Copyright Hitachi America, Ltd. All Rights Reserved. +# +# SPDX-License-Identifier: Apache-2.0 + +set -e + +METADIR="$2" + +if [ `jq -r .type "${METADIR}/metadata.json"` = "external" ]; then + exit 0 +fi + +exit 1 diff --git a/tools/toolchain/network/external/release b/tools/toolchain/network/external/release new file mode 100755 index 00000000..b4ea54c1 --- /dev/null +++ b/tools/toolchain/network/external/release @@ -0,0 +1,23 @@ +#!/bin/sh +# +# Copyright Hitachi America, Ltd. All Rights Reserved. +# +# SPDX-License-Identifier: Apache-2.0 + +set -e + +BUILD="$1" +RELEASE="$2" + +if [ -d "${BUILD}/metadata" ]; then + cp -a "${BUILD}/metadata/*" "${RELEASE}/" +fi + +if [ -f "${BUILD}/connection.json" ]; then + mkdir -p "${RELEASE}/chaincode/server" + cp "${BUILD}/connection.json" "${RELEASE}/chaincode/server" + + # TODO: TLS + + exit 0 +fi