diff --git a/libraries/fabric-shim/lib/cmds/serverCommand.js b/libraries/fabric-shim/lib/cmds/serverCommand.js new file mode 100644 index 00000000..93024ba4 --- /dev/null +++ b/libraries/fabric-shim/lib/cmds/serverCommand.js @@ -0,0 +1,56 @@ +/* +# Copyright Hitachi America, Ltd. All Rights Reserved. +# +# SPDX-License-Identifier: Apache-2.0 +*/ + +'use strict'; + +exports.command = 'server [options]'; +exports.desc = 'Start the chaincode as a server'; + +const validOptions = { + 'chaincode-address': {type: 'string', required: true}, + 'grpc.max_send_message_length': {type: 'number', default: -1}, + 'grpc.max_receive_message_length': {type: 'number', default: -1}, + 'grpc.keepalive_time_ms': {type: 'number', default: 110000}, + 'grpc.http2.min_time_between_pings_ms': {type: 'number', default: 110000}, + 'grpc.keepalive_timeout_ms': {type: 'number', default: 20000}, + 'grpc.http2.max_pings_without_data': {type: 'number', default: 0}, + 'grpc.keepalive_permit_without_calls': {type: 'number', default: 1}, + 'chaincode-id': {type: 'string', required: true}, + 'module-path': {type: 'string', default: process.cwd()} +}; + +exports.validOptions = validOptions; + +exports.builder = function (yargs) { + yargs.options(validOptions); + + yargs.usage('fabric-chaincode-node server --chaincode-address 0.0.0.0:9999 --chaincode-id mycc_v0:abcdef12345678...'); + + return yargs; +}; + +exports.handler = function (argv) { + const Bootstrap = require('../contract-spi/bootstrap'); + + return argv.thePromise = Bootstrap.bootstrap(true); +}; + +exports.getArgs = function (yargs) { + const argv = {}; + + for (const name in validOptions) { + argv[name] = yargs.argv[name]; + } + + // Translate the options to server options + argv.ccid = argv['chaincode-id']; + argv.address = argv['chaincode-address']; + + delete argv['chaincode-id']; + delete argv['chaincode-address']; + + return argv; +}; diff --git a/libraries/fabric-shim/lib/contract-spi/bootstrap.js b/libraries/fabric-shim/lib/contract-spi/bootstrap.js index f682018e..12affc42 100644 --- a/libraries/fabric-shim/lib/contract-spi/bootstrap.js +++ b/libraries/fabric-shim/lib/contract-spi/bootstrap.js @@ -14,6 +14,7 @@ const shim = require('../chaincode'); const ChaincodeFromContract = require('./chaincodefromcontract'); const Logger = require('../logger'); const StartCommand = require('../cmds/startCommand.js'); +const ServerCommand = require('../cmds/serverCommand.js'); const logger = Logger.getLogger('contracts-spi/bootstrap.js'); @@ -28,25 +29,31 @@ class Bootstrap { * @ignore * @param {Contract} contracts contract to register to use */ - static register(contracts, serializers, fileMetadata, title, version) { + static register(contracts, serializers, fileMetadata, title, version, opts, serverMode = false) { // load up the meta data that the user may have specified // this will need to passed in and rationalized with the // code as implemented const chaincode = new ChaincodeFromContract(contracts, serializers, fileMetadata, title, version); - // say hello to the peer - shim.start(chaincode); + if (serverMode) { + const server = shim.server(chaincode, opts); + server.start(); + } else { + // say hello to the peer + shim.start(chaincode); + } } /** * * @ignore + * @param {boolean} serverMode set true if the chaincode should be started as a server */ - static async bootstrap() { - const opts = StartCommand.getArgs(yargs); + static async bootstrap(serverMode = false) { + const opts = serverMode ? ServerCommand.getArgs(yargs) : StartCommand.getArgs(yargs); const {contracts, serializers, title, version} = this.getInfoFromContract(opts['module-path']); const fileMetadata = await Bootstrap.getMetadata(opts['module-path']); - Bootstrap.register(contracts, serializers, fileMetadata, title, version); + Bootstrap.register(contracts, serializers, fileMetadata, title, version, opts, serverMode); } static getInfoFromContract(modulePath) { diff --git a/libraries/fabric-shim/test/unit/cmds/serverCommand.js b/libraries/fabric-shim/test/unit/cmds/serverCommand.js new file mode 100644 index 00000000..e0320b5e --- /dev/null +++ b/libraries/fabric-shim/test/unit/cmds/serverCommand.js @@ -0,0 +1,87 @@ +/* +# Copyright Hitachi America, Ltd. All Rights Reserved. +# +# SPDX-License-Identifier: Apache-2.0 +*/ + +'use strict'; + +const sinon = require('sinon'); + +const chai = require('chai'); +const expect = chai.expect; + +const yargs = require('yargs'); +const Bootstrap = require('../../../lib/contract-spi/bootstrap'); +const chaincodeServerCommand = require('../../../lib/cmds/serverCommand.js'); + +describe('server cmd', () => { + let sandbox; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + }); + + afterEach(() => { + sandbox.restore(); + }); + + describe('.builder', () => { + it('should configure the builder function', () => { + sandbox.stub(yargs, 'options'); + sandbox.stub(yargs, 'usage'); + + chaincodeServerCommand.builder(yargs); + + expect(yargs.options.calledOnce).to.be.true; + + const args = yargs.options.getCall(0).args[0]; + + expect(args['chaincode-address'].required).to.be.true; + expect(args['chaincode-id'].required).to.be.true; + expect(args['grpc.max_send_message_length'].default).to.deep.equal(-1); + expect(args['grpc.max_receive_message_length'].default).to.deep.equal(-1); + expect(args['grpc.keepalive_time_ms'].default).to.deep.equal(110000); + expect(args['grpc.http2.min_time_between_pings_ms'].default).to.deep.equal(110000); + expect(args['grpc.keepalive_timeout_ms'].default).to.deep.equal(20000); + expect(args['grpc.http2.max_pings_without_data'].default).to.deep.equal(0); + expect(args['grpc.keepalive_permit_without_calls'].default).to.deep.equal(1); + expect(args['module-path'].default).to.deep.equal(process.cwd()); + + expect(yargs.usage.calledOnce).to.be.true; + }); + }); + + describe('.handle', () => { + it('should handle properly and call bootstrap', () => { + sandbox.stub(Bootstrap, 'bootstrap'); + + const argv = {}; + chaincodeServerCommand.handler(argv); + + expect(Bootstrap.bootstrap.calledOnce).to.be.true; + }); + }); + + describe('.getArgs', () => { + it('should return the arguments properly', () => { + const argv = { + 'chaincode-address': '0.0.0.0:9999', + 'chaincode-id': 'test_id:1', + 'grpc.keepalive_time_ms': 1000, + 'module-path': '/tmp/example', + 'extra-options': 'something' + }; + + const ret = chaincodeServerCommand.getArgs({argv}); + + expect(ret.address).to.equal('0.0.0.0:9999'); + expect(ret.ccid).to.equal('test_id:1'); + expect(ret['grpc.keepalive_time_ms']).to.equal(1000); + expect(ret['module-path']).to.equal('/tmp/example'); + expect(ret['chaincode-address']).to.be.undefined; + expect(ret['chaincode-id']).to.be.undefined; + expect(ret['extra-options']).to.be.undefined; + }); + }); +}); diff --git a/libraries/fabric-shim/test/unit/contract-spi/bootstrap.js b/libraries/fabric-shim/test/unit/contract-spi/bootstrap.js index bd1698b7..aa7d5cdf 100644 --- a/libraries/fabric-shim/test/unit/contract-spi/bootstrap.js +++ b/libraries/fabric-shim/test/unit/contract-spi/bootstrap.js @@ -59,6 +59,7 @@ describe('bootstrap.js', () => { let sandbox; let mockShim; + let mockServer; let mockCmd; let readFileStub; let pathExistsStub; @@ -73,7 +74,8 @@ describe('bootstrap.js', () => { useCleanCache: true }); sandbox = sinon.createSandbox(); - mockShim = {start : sandbox.stub()}; + mockServer = {start: sandbox.stub()}; + mockShim = {start : sandbox.stub(), server: sandbox.stub().returns(mockServer)}; getArgsStub = sandbox.stub(); mockCmd = {getArgs : getArgsStub}; @@ -84,6 +86,7 @@ describe('bootstrap.js', () => { mockery.registerMock('yargs', {}); mockery.registerMock('../chaincode', mockShim); mockery.registerMock('../cmds/startCommand.js', mockCmd); + mockery.registerMock('../cmds/serverCommand.js', mockCmd); mockery.registerMock('./chaincodefromcontract', MockChaincodeFromContract); mockery.registerMock('fs-extra', {pathExists:pathExistsStub, readFileSync : readFileStub}); @@ -103,6 +106,16 @@ describe('bootstrap.js', () => { sinon.assert.calledOnce(mockShim.start); }); + it('should pass on the register to the shim in the server mode', async () => { + const opts = {ccid: 'abcdef', address: '0.0.0.0:9999'}; + await Bootstrap.register([sc], {}, {}, 'some title', 'some version', opts, true); + + sinon.assert.calledOnce(mockShim.server); + sinon.assert.calledOnce(mockServer.start); + + expect(mockShim.server.getCall(0).args[1]).to.deep.equal(opts); + }); + }); describe('#bootstrap', () => { @@ -129,8 +142,24 @@ describe('bootstrap.js', () => { sinon.assert.calledOnce(getMetadataStub); sinon.assert.calledOnce(getInfoFromContractStub); sinon.assert.calledOnce(registerStub); - sinon.assert.calledWith(registerStub, [sc], {}, {}, 'some title', 'some version'); + sinon.assert.calledWith(registerStub, [sc], {}, {}, 'some title', 'some version', {'module-path':'fakepath'}, false); }); + + it ('should correctly call the register method in the server mode', async () => { + getMetadataStub.resolves({}); + mockery.registerMock(path.resolve(process.cwd(), 'fakepath', 'entrypoint'), {contracts: [sc]}); + const registerStub = sandbox.stub(); + Bootstrap.register = registerStub; + getInfoFromContractStub.returns({contracts: [sc], serializers : {}, title: 'some title', version: 'some version'}); + + await Bootstrap.bootstrap(true); + + sinon.assert.calledOnce(getMetadataStub); + sinon.assert.calledOnce(getInfoFromContractStub); + sinon.assert.calledOnce(registerStub); + sinon.assert.calledWith(registerStub, [sc], {}, {}, 'some title', 'some version', {'module-path':'fakepath'}, true); + }); + }); describe('#getInfoFromContract', () => {