From 8ae2dcc288c3aeba697b74baeaa440b5b0dc3ffb Mon Sep 17 00:00:00 2001 From: Peter Somogyvari Date: Tue, 9 Mar 2021 15:20:31 -0800 Subject: [PATCH] feat(fabric-connector): contract deployment endpoint #616 Primary change ============= Finally adds real support for chain code deployment with custom made contracts not just the ones that the fabric samples container comes pre-built with. Additional notes, changes ======================== 1. Makes assumptions about the ledger being containerized the same way the fabric-samples repo does it, e.g. there must be a container called "cli" which is set up to execute the peer binary and to be able to perform go builds with a version of go that has go modules support (1.11+ IIRC) 2. Does not yet support Fabric 2.x (at least it was not tested) 3. Magic strings in the connector that still need to be eliminted. 4. Go mod file upload is not supported, for now one must use the pinned dependencies array to lock dependencies to specific versions. 5. The deployment endpoint supports deploying to multiple orgs with a single request but this only makes sense for example code since in a production environment it is not expected that different organizations will share the same server/container to run their infrastructure. 6. Output structure is not yet finalized. Priority was for now to at least allow failure detection. Diagnostics is a bit harder to achieve but should come in a follow-up improvement nevertheless. 7. Forced to use ugly timeout to make sure test is passing. 8. Pending refactor of the chaincode compiler which will no longer build on the file-system locally (after the refactor is done). For now the test is being skipped. End to end test demonstrating how it all works can be seen at: packages/cactus-plugin-ledger-connector-fabric/src/test/typescript/integration/deploy-contract-go-bin-endpoint-v1/deploy-contract/deploy-cc-from-golang-source.test.ts Fixes #616 Signed-off-by: Peter Somogyvari --- .../package-lock.json | 162 ++---------- .../package.json | 13 +- .../src/main/json/openapi.json | 171 ++++++++++++- .../main/typescript/chain-code-compiler.ts | 6 +- .../deploy-contract-go-source-endpoint-v1.ts | 37 ++- .../generated/openapi/typescript-axios/api.ts | 139 +++++++++- .../plugin-ledger-connector-fabric.ts | 237 +++++++++++++++++- .../deploy-cc-from-golang-source.test.ts | 202 ++++++++++----- .../run-transaction-endpoint-v1.test.ts | 2 + .../run-transaction-endpoint-v1.test.ts | 44 ++-- .../unit/chain-code-compiler.test.ts | 4 +- 11 files changed, 749 insertions(+), 268 deletions(-) diff --git a/packages/cactus-plugin-ledger-connector-fabric/package-lock.json b/packages/cactus-plugin-ledger-connector-fabric/package-lock.json index ceb25bf024..5dc3d1d1b6 100644 --- a/packages/cactus-plugin-ledger-connector-fabric/package-lock.json +++ b/packages/cactus-plugin-ledger-connector-fabric/package-lock.json @@ -330,7 +330,6 @@ "version": "0.21.1", "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz", "integrity": "sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==", - "dev": true, "requires": { "follow-redirects": "^1.10.0" } @@ -910,16 +909,6 @@ "sha.js": "^2.4.8" } }, - "cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "requires": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - } - }, "crypto-browserify": { "version": "3.12.0", "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz", @@ -1458,37 +1447,6 @@ "safe-buffer": "^5.1.1" } }, - "execa": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/execa/-/execa-4.0.3.tgz", - "integrity": "sha512-WFDXGHckXPWZX19t1kCsXzOpqX9LWYNqn4C+HqZlk/V0imTkzJZqf87ZBhvpHaftERYknpk0fjSylnXVlVgI0A==", - "requires": { - "cross-spawn": "^7.0.0", - "get-stream": "^5.0.0", - "human-signals": "^1.1.1", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.0", - "onetime": "^5.1.0", - "signal-exit": "^3.0.2", - "strip-final-newline": "^2.0.0" - }, - "dependencies": { - "get-stream": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", - "requires": { - "pump": "^3.0.0" - } - }, - "is-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", - "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==" - } - } - }, "express": { "version": "4.17.1", "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", @@ -1834,10 +1792,9 @@ } }, "follow-redirects": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.1.tgz", - "integrity": "sha512-SSG5xmZh1mkPGyKzjZP8zLjltIfpW32Y5QpdNJyjcfGxK3qo3NDDkZOZSFiGn1A6SclQxY9GzEwAHQ3dmYRWpg==", - "dev": true + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.3.tgz", + "integrity": "sha512-DUgl6+HDzB0iEptNQEXLx/KhTmDb8tZUHSeLqpnjpknR70H0nC2t9N73BK6fN4hOvJ84pKlIQVQ4k5FFlBedKA==" }, "forever-agent": { "version": "0.6.1", @@ -1930,20 +1887,6 @@ "process": "~0.5.1" } }, - "go-bin": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/go-bin/-/go-bin-1.4.0.tgz", - "integrity": "sha512-T+jeJFVboLESIVqi3v8vJMAoHvsauR8XRKBkTwLwE1PUdDWOSAyrIQ9ymWFJ6suaDNEcNNglBCOc6AFbtVkqow==", - "requires": { - "decompress": "^4.2.1", - "mkdirp": "^1.0.4" - } - }, - "go-versions": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/go-versions/-/go-versions-1.3.2.tgz", - "integrity": "sha512-nKjEKqRT1BUPVGO8WO5EKUWgJ6l1sThfSdYuRi6WwNyiwR4SOfC/FoB7aRRUtfmMHBU3ZJNMG2x8GiE51/tbhg==" - }, "got": { "version": "9.6.0", "resolved": "https://registry.npmjs.org/got/-/got-9.6.0.tgz", @@ -2497,11 +2440,6 @@ "resolved": "https://registry.npmjs.org/http-status-codes/-/http-status-codes-2.1.4.tgz", "integrity": "sha512-MZVIsLKGVOVE1KEnldppe6Ij+vmemMuApDfjhVSLzyYP+td0bREEYyAoIw9yFePoBXManCuBqmiNP5FqJS5Xkg==" }, - "human-signals": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz", - "integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==" - }, "iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -2628,11 +2566,6 @@ "punycode": "2.x.x" } }, - "isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" - }, "isstream": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", @@ -2841,11 +2774,6 @@ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" }, - "merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==" - }, "methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", @@ -2878,11 +2806,6 @@ "mime-db": "1.44.0" } }, - "mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==" - }, "mimic-response": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", @@ -3034,16 +2957,6 @@ "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=" }, - "ngo": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/ngo/-/ngo-2.6.2.tgz", - "integrity": "sha512-fOAX8YlMFUHvJUBp0uNOqYMS/OqK05iV9lPzLVhn9sFJR0n4wFIfz9Y59Kg0v9mtrxZizN8L0mkimn7ikyIbbA==", - "requires": { - "execa": "^4.0.0", - "go-bin": "^1.4.0", - "go-versions": "^1.3.2" - } - }, "node-addon-api": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-2.0.2.tgz", @@ -3081,14 +2994,6 @@ "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.0.tgz", "integrity": "sha512-2s47yzUxdexf1OhyRi4Em83iQk0aPvwTddtFz4hnSSw9dCEsLEGf6SwIO8ss/19S9iBb5sJaOuTvTGDeZI00BQ==" }, - "npm-run-path": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", - "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", - "requires": { - "path-key": "^3.0.0" - } - }, "number-is-nan": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", @@ -3144,14 +3049,6 @@ "wrappy": "1" } }, - "onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "requires": { - "mimic-fn": "^2.1.0" - } - }, "ono": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/ono/-/ono-7.1.3.tgz", @@ -3223,11 +3120,6 @@ "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" }, - "path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==" - }, "path-to-regexp": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", @@ -3637,29 +3529,11 @@ "safe-buffer": "^5.0.1" } }, - "shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "requires": { - "shebang-regex": "^3.0.0" - } - }, - "shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==" - }, "shell-escape": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/shell-escape/-/shell-escape-0.2.0.tgz", "integrity": "sha1-aP0CXrBJC09WegJ/C/IkgLX4QTM=" }, - "signal-exit": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", - "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==" - }, "simple-concat": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", @@ -3780,11 +3654,6 @@ "is-natural-number": "^4.0.1" } }, - "strip-final-newline": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", - "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==" - }, "strip-hex-prefix": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/strip-hex-prefix/-/strip-hex-prefix-1.0.0.tgz", @@ -3928,11 +3797,22 @@ } }, "temp": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/temp/-/temp-0.9.1.tgz", - "integrity": "sha512-WMuOgiua1xb5R56lE0eH6ivpVmg/lq2OHm4+LtT/xtEtPQ+sz6N3bBM6WZ5FvO1lO4IKIOb43qnhoc4qxP5OeA==", + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/temp/-/temp-0.9.4.tgz", + "integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==", "requires": { + "mkdirp": "^0.5.1", "rimraf": "~2.6.2" + }, + "dependencies": { + "mkdirp": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "requires": { + "minimist": "^1.2.5" + } + } } }, "through": { @@ -4963,14 +4843,6 @@ } } }, - "which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "requires": { - "isexe": "^2.0.0" - } - }, "window-size": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.4.tgz", diff --git a/packages/cactus-plugin-ledger-connector-fabric/package.json b/packages/cactus-plugin-ledger-connector-fabric/package.json index 060a055c90..aab0ad64dc 100644 --- a/packages/cactus-plugin-ledger-connector-fabric/package.json +++ b/packages/cactus-plugin-ledger-connector-fabric/package.json @@ -33,7 +33,10 @@ "ignore": [ "src/**/generated/*" ], - "extensions": ["ts", "json"], + "extensions": [ + "ts", + "json" + ], "quiet": true, "verbose": false, "runOnChangeOnly": true @@ -83,6 +86,7 @@ "@hyperledger/cactus-common": "0.3.0", "@hyperledger/cactus-core": "0.3.0", "@hyperledger/cactus-core-api": "0.3.0", + "axios": "0.21.1", "bl": "1.2.3", "express": "4.17.1", "express-openapi-validator": "3.16.11", @@ -92,11 +96,10 @@ "http-status-codes": "2.1.4", "joi": "14.3.1", "multer": "1.4.2", - "ngo": "2.6.2", "node-ssh": "11.0.0", "openapi-types": "7.0.1", "prom-client": "13.0.0", - "temp": "0.9.1", + "temp": "0.9.4", "typescript-optional": "2.0.1", "uuid": "8.3.0", "web3": "1.2.7", @@ -110,8 +113,6 @@ "@types/multer": "1.4.4", "@types/ssh2": "0.5.44", "@types/temp": "0.8.34", - "@types/uuid": "8.3.0", - "axios": "0.21.1", - "form-data": "3.0.0" + "@types/uuid": "8.3.0" } } diff --git a/packages/cactus-plugin-ledger-connector-fabric/src/main/json/openapi.json b/packages/cactus-plugin-ledger-connector-fabric/src/main/json/openapi.json index b083d80af0..79c21e342c 100644 --- a/packages/cactus-plugin-ledger-connector-fabric/src/main/json/openapi.json +++ b/packages/cactus-plugin-ledger-connector-fabric/src/main/json/openapi.json @@ -165,6 +165,33 @@ "FabricContractInvocationType.CALL" ] }, + "SSHExecCommandResponse": { + "type": "object", + "required": [ + "stdout", + "stderr", + "code", + "signal" + ], + "properties": { + "stdout": { + "type": "string", + "nullable": false + }, + "stderr": { + "type": "string", + "nullable": false + }, + "code": { + "type": "integer", + "nullable": true + }, + "signal": { + "type": "string", + "nullable": true + } + } + }, "RunTransactionRequest": { "type": "object", "required": [ @@ -235,12 +262,136 @@ } } }, + "DeploymentTargetOrganization": { + "type": "object", + "required": [ + "CORE_PEER_LOCALMSPID", + "CORE_PEER_ADDRESS", + "CORE_PEER_MSPCONFIGPATH", + "CORE_PEER_TLS_ROOTCERT_FILE", + "ORDERER_TLS_ROOTCERT_FILE" + ], + "properties": { + "CORE_PEER_LOCALMSPID": { + "type": "string", + "example": "Org1MSP", + "nullable": false, + "minLength": 1, + "maxLength": 1024, + "description": "Mapped to environment variables of the Fabric CLI container." + }, + "CORE_PEER_ADDRESS": { + "type": "string", + "example": "peer0.org1.example.com:7051", + "nullable": false, + "minLength": 1, + "maxLength": 1024, + "description": "Mapped to environment variables of the Fabric CLI container." + }, + "CORE_PEER_MSPCONFIGPATH": { + "type": "string", + "example": "/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org1.example.com/users/Admin@org1.example.com/msp", + "nullable": false, + "minLength": 1, + "maxLength": 1024, + "description": "Mapped to environment variables of the Fabric CLI container." + }, + "CORE_PEER_TLS_ROOTCERT_FILE": { + "type": "string", + "example": "/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt", + "nullable": false, + "minLength": 1, + "maxLength": 1024, + "description": "Mapped to environment variables of the Fabric CLI container." + }, + "ORDERER_TLS_ROOTCERT_FILE": { + "type": "string", + "example": "/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem", + "nullable": false, + "minLength": 1, + "maxLength": 1024, + "description": "Mapped to environment variables of the Fabric CLI container." + } + } + }, "DeployContractGoSourceV1Request": { "type": "object", "required": [ - "goSource" + "goSource", + "targetOrganizations", + "chainCodeVersion", + "channelId", + "policyDslSource", + "targetPeerAddresses", + "tlsRootCertFiles" ], "properties": { + "policyDslSource": { + "type": "string", + "minLength": 1, + "maxLength": 65535, + "nullable": false, + "example": "AND('Org1MSP.member','Org2MSP.member')" + }, + "tlsRootCertFiles": { + "type": "string", + "description": "The TLS root cert files that will be passed to the chaincode instantiation command.", + "minLength": 1, + "maxLength": 65535, + "nullable": false, + "example": "/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt" + }, + "channelId": { + "type": "string", + "description": "The name of the Fabric channel where the contract will get instantiated.", + "example": "mychannel", + "minLength": 1, + "maxLength": 2048, + "nullable": false + }, + "targetOrganizations": { + "type": "array", + "minItems": 1, + "nullable": false, + "maxItems": 1024, + "items": { + "$ref": "#/components/schemas/DeploymentTargetOrganization" + } + }, + "targetPeerAddresses": { + "type": "array", + "description": "An array of peer addresses where the contract will be instantiated.", + "example": [ + "peer0.org1.example.com:7051" + ], + "minItems": 1, + "maxItems": 2048, + "items": { + "type": "string", + "minLength": 1, + "maxLength": 4096 + } + }, + "constructorArgs": { + "type": "object", + "example": "{} - An empty object literal can be sufficient if your contract does not have parameters.", + "nullable": false, + "properties": { + "Args": { + "type": "array", + "minLength": 0, + "maxLength": 2048, + "items": {} + } + } + }, + "chainCodeVersion": { + "type": "string", + "minLength": 1, + "maxLength": 128, + "example": "1.0.0", + "nullable": false + }, "goSource": { "description": "The your-smart-contract.go file where the functionality of your contract is implemented.", "$ref": "#/components/schemas/FileBase64", @@ -279,11 +430,19 @@ "DeployContractGoSourceV1Response": { "type": "object", "required": [ - "result" + "success", + "installationCommandResponse", + "instantiationCommandResponse" ], "properties": { - "result": { - "type": "string" + "success": { + "type": "boolean" + }, + "installationCommandResponse": { + "$ref": "#/components/schemas/SSHExecCommandResponse" + }, + "instantiationCommandResponse": { + "$ref": "#/components/schemas/SSHExecCommandResponse" } } }, @@ -335,14 +494,14 @@ }, "/api/v1/plugins/@hyperledger/cactus-plugin-ledger-connector-fabric/deploy-contract-go-source": { "post": { + "operationId": "deployContractGoSourceV1", + "summary": "Deploys a chaincode contract in the form of a go sources.", "x-hyperledger-cactus": { "http": { "verbLowerCase": "post", "path": "/api/v1/plugins/@hyperledger/cactus-plugin-ledger-connector-fabric/deploy-contract-go-source" } }, - "operationId": "deployContractGoSourceV1", - "summary": "Deploys a chaincode contract in the form of a go sources.", "parameters": [], "requestBody": { "content": { diff --git a/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/chain-code-compiler.ts b/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/chain-code-compiler.ts index 058373f0dc..9ff27e690f 100644 --- a/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/chain-code-compiler.ts +++ b/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/chain-code-compiler.ts @@ -59,7 +59,11 @@ export interface ICompilationResult { sourceFilePath: string; goModFilePath: string; } - +/** + * TODO: Refactor this to not use the ngo module at all and instead rely on + * SSH/docker exec-ing into environments where go is provided such as the + * Fabric CLI container. + */ export class ChainCodeCompiler { private readonly log: Logger; diff --git a/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/deploy-contract-go-source/deploy-contract-go-source-endpoint-v1.ts b/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/deploy-contract-go-source/deploy-contract-go-source-endpoint-v1.ts index 8d10ffdcd4..c019ded77d 100644 --- a/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/deploy-contract-go-source/deploy-contract-go-source-endpoint-v1.ts +++ b/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/deploy-contract-go-source/deploy-contract-go-source-endpoint-v1.ts @@ -16,6 +16,7 @@ import { import { registerWebServiceEndpoint } from "@hyperledger/cactus-core"; import { PluginLedgerConnectorFabric } from "../plugin-ledger-connector-fabric"; +import { DeployContractGoSourceV1Request } from "../generated/openapi/typescript-axios/index"; import OAS from "../../json/openapi.json"; export interface IDeployContractGoSourceEndpointV1Options { @@ -28,7 +29,7 @@ export class DeployContractGoSourceEndpointV1 implements IWebServiceEndpoint { private readonly log: Logger; - public get className() { + public get className(): string { return DeployContractGoSourceEndpointV1.CLASS_NAME; } @@ -46,24 +47,18 @@ export class DeployContractGoSourceEndpointV1 implements IWebServiceEndpoint { return this.handleRequest.bind(this); } - public getOasPath() { + public get oasPath() { return OAS.paths[ "/api/v1/plugins/@hyperledger/cactus-plugin-ledger-connector-fabric/deploy-contract-go-source" ]; } public getPath(): string { - const apiPath = this.getOasPath(); - return apiPath.post["x-hyperledger-cactus"].http.path; + return this.oasPath.post["x-hyperledger-cactus"].http.path; } public getVerbLowerCase(): string { - const apiPath = this.getOasPath(); - return apiPath.post["x-hyperledger-cactus"].http.verbLowerCase; - } - - public getOperationId(): string { - return this.getOasPath().post.operationId; + return this.oasPath.post["x-hyperledger-cactus"].http.verbLowerCase; } public registerExpress(app: Express): IWebServiceEndpoint { @@ -72,23 +67,19 @@ export class DeployContractGoSourceEndpointV1 implements IWebServiceEndpoint { } async handleRequest(req: Request, res: Response): Promise { - const fnTag = "DeployContractGoSourceEndpointV1#handleRequest()"; - this.log.debug(`POST ${this.getPath()}`); + const fnTag = `${this.className}#handleRequest()`; + const verbUpper = this.getVerbLowerCase().toUpperCase(); + this.log.debug(`${verbUpper} ${this.getPath()}`); try { - const message = - `${this.opts.connector.className} does not support ` + - ` contract deployment yet. This is a feature that is under ` + - ` development for now. Stay tuned!`; - const resBody = { message }; - // const { connector } = this.opts; - // const reqBody = req.body as DeployContractGoSourceV1Request; - // const resBody = await connector.deployContract(reqBody); - res.status(HttpStatus.NOT_IMPLEMENTED); + const { connector } = this.opts; + const reqBody = req.body as DeployContractGoSourceV1Request; + const resBody = await connector.deployContract(reqBody); + res.status(HttpStatus.OK); res.json(resBody); } catch (ex) { - this.log.error(`${fnTag} failed to serve request`, ex); - res.status(500); + this.log.error(`${fnTag} failed to serve contract deploy request`, ex); + res.status(HttpStatus.INTERNAL_SERVER_ERROR); res.statusMessage = ex.message; res.json({ error: ex.stack }); } diff --git a/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/generated/openapi/typescript-axios/api.ts b/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/generated/openapi/typescript-axios/api.ts index 957925f83f..53b72fed22 100644 --- a/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/generated/openapi/typescript-axios/api.ts +++ b/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/generated/openapi/typescript-axios/api.ts @@ -119,6 +119,48 @@ export enum DefaultEventHandlerStrategy { * @interface DeployContractGoSourceV1Request */ export interface DeployContractGoSourceV1Request { + /** + * + * @type {string} + * @memberof DeployContractGoSourceV1Request + */ + policyDslSource: string; + /** + * The TLS root cert files that will be passed to the chaincode instantiation command. + * @type {string} + * @memberof DeployContractGoSourceV1Request + */ + tlsRootCertFiles: string; + /** + * The name of the Fabric channel where the contract will get instantiated. + * @type {string} + * @memberof DeployContractGoSourceV1Request + */ + channelId: string; + /** + * + * @type {Array} + * @memberof DeployContractGoSourceV1Request + */ + targetOrganizations: Array; + /** + * An array of peer addresses where the contract will be instantiated. + * @type {Array} + * @memberof DeployContractGoSourceV1Request + */ + targetPeerAddresses: Array; + /** + * + * @type {DeployContractGoSourceV1RequestConstructorArgs} + * @memberof DeployContractGoSourceV1Request + */ + constructorArgs?: DeployContractGoSourceV1RequestConstructorArgs; + /** + * + * @type {string} + * @memberof DeployContractGoSourceV1Request + */ + chainCodeVersion: string; /** * * @type {FileBase64} @@ -150,6 +192,19 @@ export interface DeployContractGoSourceV1Request { */ modTidyOnly?: boolean | null; } +/** + * + * @export + * @interface DeployContractGoSourceV1RequestConstructorArgs + */ +export interface DeployContractGoSourceV1RequestConstructorArgs { + /** + * + * @type {Array} + * @memberof DeployContractGoSourceV1RequestConstructorArgs + */ + Args?: Array; +} /** * * @export @@ -158,10 +213,59 @@ export interface DeployContractGoSourceV1Request { export interface DeployContractGoSourceV1Response { /** * - * @type {string} + * @type {boolean} * @memberof DeployContractGoSourceV1Response */ - result: string; + success: boolean; + /** + * + * @type {SSHExecCommandResponse} + * @memberof DeployContractGoSourceV1Response + */ + installationCommandResponse: SSHExecCommandResponse; + /** + * + * @type {SSHExecCommandResponse} + * @memberof DeployContractGoSourceV1Response + */ + instantiationCommandResponse: SSHExecCommandResponse; +} +/** + * + * @export + * @interface DeploymentTargetOrganization + */ +export interface DeploymentTargetOrganization { + /** + * Mapped to environment variables of the Fabric CLI container. + * @type {string} + * @memberof DeploymentTargetOrganization + */ + CORE_PEER_LOCALMSPID: string; + /** + * Mapped to environment variables of the Fabric CLI container. + * @type {string} + * @memberof DeploymentTargetOrganization + */ + CORE_PEER_ADDRESS: string; + /** + * Mapped to environment variables of the Fabric CLI container. + * @type {string} + * @memberof DeploymentTargetOrganization + */ + CORE_PEER_MSPCONFIGPATH: string; + /** + * Mapped to environment variables of the Fabric CLI container. + * @type {string} + * @memberof DeploymentTargetOrganization + */ + CORE_PEER_TLS_ROOTCERT_FILE: string; + /** + * Mapped to environment variables of the Fabric CLI container. + * @type {string} + * @memberof DeploymentTargetOrganization + */ + ORDERER_TLS_ROOTCERT_FILE: string; } /** * @@ -305,6 +409,37 @@ export interface RunTransactionResponse { */ functionOutput: string; } +/** + * + * @export + * @interface SSHExecCommandResponse + */ +export interface SSHExecCommandResponse { + /** + * + * @type {string} + * @memberof SSHExecCommandResponse + */ + stdout: string; + /** + * + * @type {string} + * @memberof SSHExecCommandResponse + */ + stderr: string; + /** + * + * @type {number} + * @memberof SSHExecCommandResponse + */ + code: number | null; + /** + * + * @type {string} + * @memberof SSHExecCommandResponse + */ + signal: string | null; +} /** * DefaultApi - axios parameter creator diff --git a/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/plugin-ledger-connector-fabric.ts b/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/plugin-ledger-connector-fabric.ts index 27320b66aa..940e9754fe 100644 --- a/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/plugin-ledger-connector-fabric.ts +++ b/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/plugin-ledger-connector-fabric.ts @@ -1,9 +1,17 @@ +import fs from "fs"; +import path from "path"; import { Server } from "http"; import { Server as SecureServer } from "https"; -import { Express } from "express"; +import { Express } from "express"; import "multer"; -import { Config as SshConfig } from "node-ssh"; +import temp from "temp"; +import { + NodeSSH, + Config as SshConfig, + SSHExecCommandOptions, + SSHExecCommandResponse, +} from "node-ssh"; import { DefaultEventHandlerOptions, DefaultEventHandlerStrategies, @@ -62,9 +70,25 @@ import { import { PrometheusExporter } from "./prometheus-exporter/prometheus-exporter"; +/** + * Constant value holding the default $GOPATH in the Fabric CLI container as + * observed on fabric deployments that are produced by the official examples + * found in the https://github.com/hyperledger/fabric-samples repository. + */ +export const K_DEFAULT_CLI_CONTAINER_GO_PATH = "/opt/gopath/"; + +/** + * The command that will be used to issue docker commands while controlling + * the Fabric CLI container and the peers. + */ +export const K_DEFAULT_DOCKER_BINARY = "docker"; + export interface IPluginLedgerConnectorFabricOptions extends ICactusPluginOptions { logLevel?: LogLevelDesc; + dockerBinary?: string; + cliContainerGoPath?: string; + cliContainerEnv: NodeJS.ProcessEnv; pluginRegistry: PluginRegistry; sshConfig: SshConfig; connectionProfile: ConnectionProfile; @@ -86,6 +110,8 @@ export class PluginLedgerConnectorFabric public static readonly CLASS_NAME = "PluginLedgerConnectorFabric"; private readonly instanceId: string; private readonly log: Logger; + private readonly dockerBinary: string; + private readonly cliContainerGoPath: string; public prometheusExporter: PrometheusExporter; public get className(): string { @@ -105,6 +131,15 @@ export class PluginLedgerConnectorFabric this.prometheusExporter, `${fnTag} options.prometheusExporter`, ); + this.dockerBinary = opts.dockerBinary || K_DEFAULT_DOCKER_BINARY; + Checks.truthy(this.dockerBinary != null, `${fnTag}:dockerBinary`); + + this.cliContainerGoPath = + opts.cliContainerGoPath || K_DEFAULT_CLI_CONTAINER_GO_PATH; + Checks.nonBlankString( + this.cliContainerGoPath, + `${fnTag}:cliContainerGoPath`, + ); const level = this.opts.logLevel || "INFO"; const label = this.className; @@ -149,17 +184,207 @@ export class PluginLedgerConnectorFabric return ConsensusAlgorithmFamily.AUTHORITY; } + private async sshExec( + cmd: string, + label: string, + ssh: NodeSSH, + sshCmdOptions: SSHExecCommandOptions, + ): Promise { + this.log.debug(`${label} CMD: ${cmd}`); + const cmdRes = await ssh.execCommand(cmd, sshCmdOptions); + this.log.debug(`${label} CMD Response: %o`, cmdRes); + Checks.truthy(cmdRes.code === null, `${label} cmdRes.code === null`); + return cmdRes; + } + /** - * FIXME: Implement this feature of the connector. - * * @param req The object containing all the necessary metadata and parameters * in order to have the contract deployed. */ public async deployContract( req: DeployContractGoSourceV1Request, ): Promise { - const fnTag = "PluginLedgerConnectorFabric#deployContract()"; - throw new Error(`${fnTag} Not yet implemented! ${req}`); + const fnTag = `${this.className}#deployContract()`; + + const ssh = new NodeSSH(); + await ssh.connect(this.opts.sshConfig); + this.log.debug(`SSH connection OK`); + + try { + this.log.debug(`${fnTag} Deploying .go source: ${req.goSource.filename}`); + + Checks.truthy(req.goSource, `${fnTag}:req.goSource`); + + temp.track(); + const tmpDirPrefix = `hyperledger-cactus-${this.className}`; + const tmpDirPath = temp.mkdirSync(tmpDirPrefix); + + // The module name of the chain-code, for example this will extract + // ccName to be "hello-world" from a filename of "hello-world.go" + const inferredModuleName = path.basename(req.goSource.filename, ".go"); + this.log.debug(`Inferred module name: ${inferredModuleName}`); + const ccName = req.moduleName || inferredModuleName; + this.log.debug(`Determined ChainCode name: ${ccName}`); + + const remoteDirPath = path.join(this.cliContainerGoPath, "src/", ccName); + this.log.debug(`Remote dir path on CLI container: ${remoteDirPath}`); + + const localFilePath = path.join(tmpDirPath, req.goSource.filename); + fs.writeFileSync(localFilePath, req.goSource.body, "base64"); + + const remoteFilePath = path.join(remoteDirPath, req.goSource.filename); + + this.log.debug(`SCP from/to %o => %o`, localFilePath, remoteFilePath); + await ssh.putFile(localFilePath, remoteFilePath); + this.log.debug(`SCP OK %o`, remoteFilePath); + + const sshCmdOptions: SSHExecCommandOptions = { + execOptions: { + pty: true, + env: { + // just in case go modules would be otherwise disabled + GO111MODULE: "on", + FABRIC_LOGGING_SPEC: "DEBUG", + }, + }, + cwd: remoteDirPath, + }; + + const dockerExecEnv = Object.entries(this.opts.cliContainerEnv) + .map(([key, value]) => `--env ${key}=${value}`) + .join(" "); + + const { dockerBinary } = this; + const dockerBuildCmd = + `${dockerBinary} exec ` + + dockerExecEnv + + ` --env GO111MODULE=on` + + ` --workdir=${remoteDirPath}` + + ` cli `; + + await this.sshExec( + `${dockerBinary} exec cli mkdir -p ${remoteDirPath}/`, + "Create ChainCode project (go module) directory", + ssh, + sshCmdOptions, + ); + + await this.sshExec( + `${dockerBinary} exec cli go version`, + "Print go version", + ssh, + sshCmdOptions, + ); + + const copyToCliCmd = `${dockerBinary} cp ${remoteFilePath} cli:${remoteFilePath}`; + this.log.debug(`Copy to CLI Container CMD: ${copyToCliCmd}`); + const copyToCliRes = await ssh.execCommand(copyToCliCmd, sshCmdOptions); + this.log.debug(`Copy to CLI Container CMD Response: %o`, copyToCliRes); + Checks.truthy(copyToCliRes.code === null, `copyToCliRes.code === null`); + + { + const goModInitCmd = `${dockerBuildCmd} go mod init ${ccName}`; + this.log.debug(`go mod init CMD: ${goModInitCmd}`); + const goModInitRes = await ssh.execCommand(goModInitCmd, sshCmdOptions); + this.log.debug(`go mod init CMD Response: %o`, goModInitRes); + Checks.truthy(goModInitRes.code === null, `goModInitRes.code === null`); + } + + const pinnedDeps = req.pinnedDeps || []; + for (const dep of pinnedDeps) { + const goGetCmd = `${dockerBuildCmd} go get ${dep}`; + this.log.debug(`go get CMD: ${goGetCmd}`); + const goGetRes = await ssh.execCommand(goGetCmd, sshCmdOptions); + this.log.debug(`go get CMD Response: %o`, goGetRes); + Checks.truthy(goGetRes.code === null, `goGetRes.code === null`); + } + + { + const goModTidyCmd = `${dockerBuildCmd} go mod tidy`; + this.log.debug(`go mod tidy CMD: ${goModTidyCmd}`); + const goModTidyRes = await ssh.execCommand(goModTidyCmd, sshCmdOptions); + this.log.debug(`go mod tidy CMD Response: %o`, goModTidyRes); + Checks.truthy(goModTidyRes.code === null, `goModTidyRes.code === null`); + } + + { + const goVendorCmd = `${dockerBuildCmd} go mod vendor`; + this.log.debug(`go mod vendor CMD: ${goVendorCmd}`); + const goVendorRes = await ssh.execCommand(goVendorCmd, sshCmdOptions); + this.log.debug(`go mod vendor CMD Response: %o`, goVendorRes); + Checks.truthy(goVendorRes.code === null, `goVendorRes.code === null`); + } + + { + const goBuildCmd = `${dockerBuildCmd} go build`; + this.log.debug(`go build CMD: ${goBuildCmd}`); + const goBuildRes = await ssh.execCommand(goBuildCmd, sshCmdOptions); + this.log.debug(`go build CMD Response: %o`, goBuildRes); + Checks.truthy(goBuildRes.code === null, `goBuildRes.code === null`); + } + + // https://github.com/hyperledger/fabric-samples/blob/release-1.4/fabcar/startFabric.sh + for (const org of req.targetOrganizations) { + const env = + ` --env CORE_PEER_LOCALMSPID=${org.CORE_PEER_LOCALMSPID}` + + ` --env CORE_PEER_ADDRESS=${org.CORE_PEER_ADDRESS}` + + ` --env CORE_PEER_MSPCONFIGPATH=${org.CORE_PEER_MSPCONFIGPATH}` + + ` --env CORE_PEER_TLS_ROOTCERT_FILE=${org.CORE_PEER_TLS_ROOTCERT_FILE}`; + + await this.sshExec( + dockerBinary + + ` exec ${env} cli peer chaincode install` + + ` --name ${ccName} ` + + ` --path ${ccName} ` + + ` --version ${req.chainCodeVersion} ` + + ` --lang golang`, + `Install ChainCode in ${org.CORE_PEER_LOCALMSPID}`, + ssh, + sshCmdOptions, + ); + } + + let success = true; + + const ctorArgsJson = JSON.stringify(req.constructorArgs || {}); + const ordererCaFile = + "/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/ordererOrganizations/example.com/orderers/orderer.example.com/tls/ca.crt"; + + const instantiateCmd = + `${dockerBuildCmd} peer chaincode instantiate ` + + ` --name ${ccName} ` + + ` --version ${req.chainCodeVersion} ` + + ` --ctor '${ctorArgsJson}' ` + + ` --channelID ${req.channelId} ` + + ` --peerAddresses ${req.targetPeerAddresses[0]}` + + ` --lang golang ` + + ` --tlsRootCertFiles ${req.tlsRootCertFiles}` + + ` --policy "${req.policyDslSource}"` + + ` --tls --cafile ${ordererCaFile}`; + + this.log.debug(`Instantiate CMD: %o`, instantiateCmd); + const instantiateCmdRes = await ssh.execCommand( + instantiateCmd, + sshCmdOptions, + ); + + this.log.debug(`Instantiate CMD Response: %o`, instantiateCmdRes); + success = success && instantiateCmdRes.code === null; + + this.log.debug(`EXIT doDeploy()`); + const res: DeployContractGoSourceV1Response = { + success, + installationCommandResponse: {} as any, + instantiationCommandResponse: instantiateCmdRes, + }; + return res; + } finally { + try { + ssh.dispose(); + } finally { + temp.cleanup(); + } + } } public async installWebServices( diff --git a/packages/cactus-plugin-ledger-connector-fabric/src/test/typescript/integration/deploy-contract-go-bin-endpoint-v1/deploy-contract/deploy-cc-from-golang-source.test.ts b/packages/cactus-plugin-ledger-connector-fabric/src/test/typescript/integration/deploy-contract-go-bin-endpoint-v1/deploy-contract/deploy-cc-from-golang-source.test.ts index a4341ce8ed..d30e1774e2 100644 --- a/packages/cactus-plugin-ledger-connector-fabric/src/test/typescript/integration/deploy-contract-go-bin-endpoint-v1/deploy-contract/deploy-cc-from-golang-source.test.ts +++ b/packages/cactus-plugin-ledger-connector-fabric/src/test/typescript/integration/deploy-contract-go-bin-endpoint-v1/deploy-contract/deploy-cc-from-golang-source.test.ts @@ -1,15 +1,11 @@ -import fs from "fs"; import { AddressInfo } from "net"; import http from "http"; -import path from "path"; import test, { Test } from "tape"; import { v4 as uuidv4 } from "uuid"; import express from "express"; import bodyParser from "body-parser"; -import axios, { AxiosRequestConfig } from "axios"; -import FormData from "form-data"; import { FabricTestLedgerV1 } from "@hyperledger/cactus-test-tooling"; @@ -21,18 +17,29 @@ import { import { PluginRegistry } from "@hyperledger/cactus-core"; import { + DefaultEventHandlerStrategy, + FabricContractInvocationType, PluginLedgerConnectorFabric, - ChainCodeCompiler, - ICompilationOptions, } from "../../../../../main/typescript/public-api"; import { HELLO_WORLD_CONTRACT_GO_SOURCE } from "../../../fixtures/go/hello-world-contract-fabric-v14/hello-world-contract-go-source"; +import { DefaultApi as FabricApi } from "../../../../../main/typescript/public-api"; + import { IPluginLedgerConnectorFabricOptions } from "../../../../../main/typescript/plugin-ledger-connector-fabric"; -test.skip("deploys contract from go source", async (t: Test) => { - const logLevel: LogLevelDesc = "TRACE"; - const ledger = new FabricTestLedgerV1({ publishAllPorts: true }); +import { DiscoveryOptions } from "fabric-network"; +import { PluginKeychainMemory } from "@hyperledger/cactus-plugin-keychain-memory"; + +const logLevel: LogLevelDesc = "TRACE"; + +test("deploys contract from go source", async (t: Test) => { + const ledger = new FabricTestLedgerV1({ + emitContainerLogs: true, + publishAllPorts: true, + imageName: "hyperledger/cactus-fabric-all-in-one", + imageVersion: "2021-03-02-ssh-hotfix", + }); await ledger.start(); const tearDown = async () => { @@ -43,19 +50,73 @@ test.skip("deploys contract from go source", async (t: Test) => { test.onFinish(tearDown); const connectionProfile = await ledger.getConnectionProfileOrg1(); - t.ok(connectionProfile); + t.ok(connectionProfile, "getConnectionProfileOrg1() out truthy OK"); + const enrollAdminOut = await ledger.enrollAdmin(); + const adminWallet = enrollAdminOut[1]; + const [userIdentity] = await ledger.enrollUser(adminWallet); const sshConfig = await ledger.getSshConfig(); - const pluginRegistry = new PluginRegistry(); - const pluginOpts: IPluginLedgerConnectorFabricOptions = { + const keychainInstanceId = uuidv4(); + const keychainId = uuidv4(); + const keychainEntryKey = "user2"; + const keychainEntryValue = JSON.stringify(userIdentity); + + const keychainPlugin = new PluginKeychainMemory({ + instanceId: keychainInstanceId, + keychainId, + logLevel, + backend: new Map([ + [keychainEntryKey, keychainEntryValue], + ["some-other-entry-key", "some-other-entry-value"], + ]), + }); + + const pluginRegistry = new PluginRegistry({ plugins: [keychainPlugin] }); + + const discoveryOptions: DiscoveryOptions = { + enabled: true, + asLocalhost: true, + }; + + // these below mirror how the fabric-samples sets up the configuration + const org1Env = { + CORE_PEER_LOCALMSPID: "Org1MSP", + CORE_PEER_ADDRESS: "peer0.org1.example.com:7051", + CORE_PEER_MSPCONFIGPATH: + "/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org1.example.com/users/Admin@org1.example.com/msp", + CORE_PEER_TLS_ROOTCERT_FILE: + "/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt", + ORDERER_TLS_ROOTCERT_FILE: + "/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem", + }; + + // these below mirror how the fabric-samples sets up the configuration + const org2Env = { + CORE_PEER_LOCALMSPID: "Org2MSP", + CORE_PEER_ADDRESS: "peer0.org2.example.com:9051", + CORE_PEER_MSPCONFIGPATH: + "/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org2.example.com/users/Admin@org2.example.com/msp", + CORE_PEER_TLS_ROOTCERT_FILE: + "/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt", + ORDERER_TLS_ROOTCERT_FILE: + "/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem", + }; + + const pluginOptions: IPluginLedgerConnectorFabricOptions = { instanceId: uuidv4(), + dockerBinary: "/usr/local/bin/docker", pluginRegistry, + cliContainerEnv: org1Env, sshConfig, logLevel, connectionProfile, + discoveryOptions, + eventHandlerOptions: { + strategy: DefaultEventHandlerStrategy.NETWORKSCOPEALLFORTX, + }, }; - const plugin = new PluginLedgerConnectorFabric(pluginOpts); + const plugin = new PluginLedgerConnectorFabric(pluginOptions); const expressApp = express(); expressApp.use(bodyParser.json({ limit: "250mb" })); @@ -69,51 +130,76 @@ test.skip("deploys contract from go source", async (t: Test) => { const { port } = addressInfo; test.onFinish(async () => await Servers.shutdown(server)); - const [endpoint] = await plugin.installWebServices(expressApp); - - const url = `http://localhost:${port}${endpoint.getPath()}`; - - const form = new FormData(); - const headers = form.getHeaders(); - - const compiler = new ChainCodeCompiler({ logLevel }); - - const opts: ICompilationOptions = { - fileName: "hello-world-contract.go", - moduleName: "hello-world-contract", + await plugin.installWebServices(expressApp); + const apiUrl = `http://localhost:${port}`; + + const apiClient = new FabricApi({ basePath: apiUrl }); + const res = await apiClient.deployContractGoSourceV1({ + targetPeerAddresses: ["peer0.org1.example.com:7051"], + tlsRootCertFiles: + "/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt", + policyDslSource: "AND('Org1MSP.member','Org2MSP.member')", + channelId: "mychannel", + chainCodeVersion: "1.0.0", + constructorArgs: { Args: ["john", "99"] }, + goSource: { + body: Buffer.from(HELLO_WORLD_CONTRACT_GO_SOURCE).toString("base64"), + filename: "hello-world.go", + }, + moduleName: "hello-world", + targetOrganizations: [org1Env, org2Env], pinnedDeps: ["github.com/hyperledger/fabric@v1.4.8"], - modTidyOnly: true, // we just need the go.mod file so tidy only is enough - sourceCode: HELLO_WORLD_CONTRACT_GO_SOURCE, - }; - - const result = await compiler.compile(opts); - t.ok(result, "result OK"); - t.ok(result.goVersionInfo, "result.goVersionInfo OK"); - t.ok(result.goModFilePath, "result.goModFilePath OK"); - t.ok(result.sourceFilePath, "result.sourceFilePath OK"); - t.comment(result.goVersionInfo); - - const goModStream = fs.createReadStream(result.goModFilePath); - const sourceFileStream = fs.createReadStream(result.sourceFilePath); - - // Second argument can take Buffer or Stream (lazily read during the request) too. - // Third argument is filename if you want to simulate a file upload. Otherwise omit. - form.append("files", sourceFileStream, path.basename(result.sourceFilePath)); - form.append("files", goModStream, path.basename(result.goModFilePath)); - - const reqConfig: AxiosRequestConfig = { - headers, - maxContentLength: 128 * 1024 * 1024, // 128 MB - maxBodyLength: 128 * 1024 * 1024, // 128 MB, - }; - t.comment(`Req.URL=${url}`); - const res = await axios.post(url, form, reqConfig); - const { status, data } = res; - - t.comment(`res.status: ${res.status}`); - t.equal(status, 200, "res.status === 200 OK"); - - t.true(data.success, "res.data.success === true"); - + }); + + const { + installationCommandResponse, + instantiationCommandResponse, + success, + } = res.data; + + t.comment(`CC installation out: ${installationCommandResponse.stdout}`); + t.comment(`CC installation err: ${installationCommandResponse.stderr}`); + t.comment(`CC instantiation out: ${instantiationCommandResponse.stdout}`); + t.comment(`CC instantiation err: ${instantiationCommandResponse.stderr}`); + + t.equal(res.status, 200, "res.status === 200 OK"); + t.true(success, "res.data.success === true"); + + // FIXME - without this wait it randomly fails with an error claiming that + // the endorsment was impossible to be obtained. The fabric-samples script + // does the same thing, it just waits 10 seconds for good measure so there + // might not be a way for us to avoid doing this, but if there is a way we + // absolutely should not have timeouts like this, anywhere... + await new Promise((resolve) => setTimeout(resolve, 20000)); + + const testKey = uuidv4(); + const testValue = uuidv4(); + + const setRes = await apiClient.runTransactionV1({ + chainCodeId: "hello-world", + channelName: "mychannel", + functionArgs: [testKey, testValue], + functionName: "set", + invocationType: FabricContractInvocationType.SEND, + keychainId, + keychainRef: keychainEntryKey, + }); + t.ok(setRes, "setRes truthy OK"); + t.true(setRes.status > 199 && setRes.status < 300, "setRes status 2xx OK"); + t.comment(`HelloWorld.set() ResponseBody: ${JSON.stringify(setRes.data)}`); + + const getRes = await apiClient.runTransactionV1({ + chainCodeId: "hello-world", + channelName: "mychannel", + functionArgs: [testKey], + functionName: "get", + invocationType: FabricContractInvocationType.CALL, + keychainId, + keychainRef: keychainEntryKey, + }); + t.ok(getRes, "getRes truthy OK"); + t.true(getRes.status > 199 && setRes.status < 300, "getRes status 2xx OK"); + t.comment(`HelloWorld.get() ResponseBody: ${JSON.stringify(getRes.data)}`); + t.equal(getRes.data.functionOutput, testValue, "get returns UUID OK"); t.end(); }); diff --git a/packages/cactus-plugin-ledger-connector-fabric/src/test/typescript/integration/fabric-v1-4-x/run-transaction-endpoint-v1.test.ts b/packages/cactus-plugin-ledger-connector-fabric/src/test/typescript/integration/fabric-v1-4-x/run-transaction-endpoint-v1.test.ts index e119ea86ee..53424c015c 100644 --- a/packages/cactus-plugin-ledger-connector-fabric/src/test/typescript/integration/fabric-v1-4-x/run-transaction-endpoint-v1.test.ts +++ b/packages/cactus-plugin-ledger-connector-fabric/src/test/typescript/integration/fabric-v1-4-x/run-transaction-endpoint-v1.test.ts @@ -41,6 +41,7 @@ test("runs tx on a Fabric v1.4.8 ledger", async (t: Test) => { const ledger = new FabricTestLedgerV1({ publishAllPorts: true, + emitContainerLogs: false, logLevel, imageName: "hyperledger/cactus-fabric-all-in-one", imageVersion: "2020-12-16-3ddfd8f-v1.4.8", @@ -92,6 +93,7 @@ test("runs tx on a Fabric v1.4.8 ledger", async (t: Test) => { instanceId: uuidv4(), pluginRegistry, sshConfig, + cliContainerEnv: {}, logLevel, connectionProfile, discoveryOptions, diff --git a/packages/cactus-plugin-ledger-connector-fabric/src/test/typescript/integration/fabric-v2-2-x/run-transaction-endpoint-v1.test.ts b/packages/cactus-plugin-ledger-connector-fabric/src/test/typescript/integration/fabric-v2-2-x/run-transaction-endpoint-v1.test.ts index cec33625e4..1bb42a6ca0 100644 --- a/packages/cactus-plugin-ledger-connector-fabric/src/test/typescript/integration/fabric-v2-2-x/run-transaction-endpoint-v1.test.ts +++ b/packages/cactus-plugin-ledger-connector-fabric/src/test/typescript/integration/fabric-v2-2-x/run-transaction-endpoint-v1.test.ts @@ -53,10 +53,11 @@ test("runs tx on a Fabric v2.2.0 ledger", async (t: Test) => { const logLevel: LogLevelDesc = "TRACE"; const ledger = new FabricTestLedgerV1({ + emitContainerLogs: false, publishAllPorts: true, logLevel, - imageName: "hyperledger/cactus-fabric-all-in-one", - imageVersion: "2020-12-16-3ddfd8f-v2.2.0", + imageName: "hyperledger/cactus-fabric2-all-in-one", + imageVersion: "2021-03-08-hotfix-test-network", envVars: new Map([ ["FABRIC_VERSION", "2.2.0"], ["CA_VERSION", "1.4.9"], @@ -69,6 +70,7 @@ test("runs tx on a Fabric v2.2.0 ledger", async (t: Test) => { await ledger.stop(); await ledger.destroy(); }; + test.onFinish(tearDownLedger); const enrollAdminOut = await ledger.enrollAdmin(); @@ -105,6 +107,7 @@ test("runs tx on a Fabric v2.2.0 ledger", async (t: Test) => { instanceId: uuidv4(), pluginRegistry, sshConfig, + cliContainerEnv: {}, logLevel, connectionProfile, discoveryOptions, @@ -133,17 +136,19 @@ test("runs tx on a Fabric v2.2.0 ledger", async (t: Test) => { await plugin.installWebServices(expressApp); - const carId = "CAR277"; - const carOwner = uuidv4(); + const assetId = "asset277"; + const assetOwner = uuidv4(); + const channelName = "mychannel"; + const chainCodeId = "basic"; { const res = await apiClient.runTransactionV1({ keychainId, keychainRef: keychainEntryKey, - channelName: "mychannel", - chainCodeId: "fabcar", + channelName, + chainCodeId, invocationType: FabricContractInvocationType.CALL, - functionName: "queryAllCars", + functionName: "GetAllAssets", functionArgs: [], } as RunTransactionRequest); t.ok(res); @@ -155,11 +160,11 @@ test("runs tx on a Fabric v2.2.0 ledger", async (t: Test) => { const req: RunTransactionRequest = { keychainId, keychainRef: keychainEntryKey, - channelName: "mychannel", + channelName, invocationType: FabricContractInvocationType.SEND, - chainCodeId: "fabcar", - functionName: "createCar", - functionArgs: [carId, "Trabant", "601", "Blue", carOwner], + chainCodeId, + functionName: "CreateAsset", + functionArgs: [assetId, "yellow", "11", assetOwner, "199"], }; const res = await apiClient.runTransactionV1(req); @@ -172,21 +177,20 @@ test("runs tx on a Fabric v2.2.0 ledger", async (t: Test) => { const res = await apiClient.runTransactionV1({ keychainId, keychainRef: keychainEntryKey, - channelName: "mychannel", - chainCodeId: "fabcar", + channelName, + chainCodeId, invocationType: FabricContractInvocationType.CALL, - functionName: "queryAllCars", + functionName: "GetAllAssets", functionArgs: [], } as RunTransactionRequest); t.ok(res); t.ok(res.data); t.equal(res.status, 200); - const cars = JSON.parse(res.data.functionOutput); - const car277 = cars.find((c: any) => c.Key === carId); - t.ok(car277, "Located Car record by its ID OK"); - t.ok(car277.Record, `Car object has "Record" property OK`); - t.ok(car277.Record.owner, `Car object has "Record"."owner" property OK`); - t.equal(car277.Record.owner, carOwner, `Car has expected owner OK`); + const assets = JSON.parse(res.data.functionOutput); + const asset277 = assets.find((c: { ID: string }) => c.ID === assetId); + t.ok(asset277, "Located Asset record by its ID OK"); + t.ok(asset277.owner, `Asset object has "owner" property OK`); + t.equal(asset277.owner, assetOwner, `Asset has expected owner OK`); } { diff --git a/packages/cactus-plugin-ledger-connector-fabric/src/test/typescript/unit/chain-code-compiler.test.ts b/packages/cactus-plugin-ledger-connector-fabric/src/test/typescript/unit/chain-code-compiler.test.ts index 7e2ea9764c..7d5b61a0ab 100644 --- a/packages/cactus-plugin-ledger-connector-fabric/src/test/typescript/unit/chain-code-compiler.test.ts +++ b/packages/cactus-plugin-ledger-connector-fabric/src/test/typescript/unit/chain-code-compiler.test.ts @@ -9,7 +9,9 @@ import { import { HELLO_WORLD_CONTRACT_GO_SOURCE } from "../fixtures/go/hello-world-contract-fabric-v14/hello-world-contract-go-source"; -test("compiles chaincode straight from go source code", async (t: Test) => { +// FIXME - the chain code compiler will undergo a refactor to make it work via +// SSH/docker exec. Until then, leave this test out. +test.skip("compiles chaincode straight from go source code", async (t: Test) => { const compiler = new ChainCodeCompiler({ logLevel: "TRACE" }); const opts: ICompilationOptions = {