From dcf40ba4e611b4857feb1af11753a24b7e9f156a Mon Sep 17 00:00:00 2001 From: Peter Somogyvari Date: Thu, 4 Mar 2021 17:22:55 -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 | 173 ++++++++++++- .../main/typescript/chain-code-compiler.ts | 6 +- ...y-contract-go-source-endpoint-constants.ts | 16 -- .../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 | 201 ++++++++++----- .../run-transaction-endpoint-v1.test.ts | 1 + .../run-transaction-endpoint-v1.test.ts | 1 + .../unit/chain-code-compiler.test.ts | 4 +- .../fabric/fabric-test-ledger-v1.ts | 2 +- tools/docker/fabric-all-in-one/README.md | 11 +- .../docker/fabric-all-in-one/supervisord.conf | 2 +- 15 files changed, 746 insertions(+), 259 deletions(-) delete mode 100644 packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/deploy-contract-go-source/deploy-contract-go-source-endpoint-constants.ts diff --git a/packages/cactus-plugin-ledger-connector-fabric/package-lock.json b/packages/cactus-plugin-ledger-connector-fabric/package-lock.json index ceb25bf0241..5dc3d1d1b60 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 060a055c907..aab0ad64dcb 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 667e44038ac..9df9fd6610f 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" } } }, @@ -331,6 +490,12 @@ "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" + } + }, "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 058373f0dc1..9ff27e690f7 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-constants.ts b/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/deploy-contract-go-source/deploy-contract-go-source-endpoint-constants.ts deleted file mode 100644 index 326e49651f1..00000000000 --- a/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/deploy-contract-go-source/deploy-contract-go-source-endpoint-constants.ts +++ /dev/null @@ -1,16 +0,0 @@ -/** - * FIXME: Once we have the build based on Webpack 5 with the dynamic dependencies - * feature enabled these static variables can be moved back to the - * corresponding Typescript file and this current one can be - * deleted altogether. Until then, this workaround is necessary which makes the - * code more verbose than it should be. - Peter - */ -export class DeployContractGoSourceEndpointV1 { - public static readonly HTTP_PATH: string = - "/api/v1/plugins/@hyperledger/cactus-plugin-ledger-connector-fabric/deploy-contract-go-source"; - - public static readonly HTTP_VERB_LOWER_CASE: string = "post"; - - public static readonly OPENAPI_OPERATION_ID: string = - "deployContractGoSourceV1"; -} 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 2850d6bae05..c019ded77de 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 @@ -15,8 +15,9 @@ import { import { registerWebServiceEndpoint } from "@hyperledger/cactus-core"; -import { DeployContractGoSourceEndpointV1 as Constants } from "./deploy-contract-go-source-endpoint-constants"; import { PluginLedgerConnectorFabric } from "../plugin-ledger-connector-fabric"; +import { DeployContractGoSourceV1Request } from "../generated/openapi/typescript-axios/index"; +import OAS from "../../json/openapi.json"; export interface IDeployContractGoSourceEndpointV1Options { logLevel?: LogLevelDesc; @@ -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,12 +47,18 @@ export class DeployContractGoSourceEndpointV1 implements IWebServiceEndpoint { return this.handleRequest.bind(this); } + public get oasPath() { + return OAS.paths[ + "/api/v1/plugins/@hyperledger/cactus-plugin-ledger-connector-fabric/deploy-contract-go-source" + ]; + } + public getPath(): string { - return Constants.HTTP_PATH; + return this.oasPath.post["x-hyperledger-cactus"].http.path; } public getVerbLowerCase(): string { - return Constants.HTTP_VERB_LOWER_CASE; + return this.oasPath.post["x-hyperledger-cactus"].http.verbLowerCase; } public registerExpress(app: Express): IWebServiceEndpoint { @@ -60,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 957925f83fe..53b72fed225 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 27320b66aa5..940e9754fe4 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 a4341ce8ed3..b378361ce3e 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,28 @@ 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({ + publishAllPorts: true, + imageName: "hyperledger/cactus-fabric-all-in-one", + imageVersion: "2021-03-02-ssh-hotfix", + }); await ledger.start(); const tearDown = async () => { @@ -43,19 +49,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 +129,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 e119ea86ee5..de4765f46ba 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 @@ -92,6 +92,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 7154f0006dc..a065afd24ba 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 @@ -92,6 +92,7 @@ test("runs tx on a Fabric v2.2.0 ledger", async (t: Test) => { instanceId: uuidv4(), pluginRegistry, sshConfig, + cliContainerEnv: {}, logLevel, connectionProfile, discoveryOptions, 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 7e2ea9764cf..7d5b61a0ab9 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 = { diff --git a/packages/cactus-test-tooling/src/main/typescript/fabric/fabric-test-ledger-v1.ts b/packages/cactus-test-tooling/src/main/typescript/fabric/fabric-test-ledger-v1.ts index af153664455..3d60877fc80 100644 --- a/packages/cactus-test-tooling/src/main/typescript/fabric/fabric-test-ledger-v1.ts +++ b/packages/cactus-test-tooling/src/main/typescript/fabric/fabric-test-ledger-v1.ts @@ -77,7 +77,7 @@ export class FabricTestLedgerV1 implements ITestLedger { private container: Container | undefined; private containerId: string | undefined; - public get className() { + public get className(): string { return FabricTestLedgerV1.CLASS_NAME; } diff --git a/tools/docker/fabric-all-in-one/README.md b/tools/docker/fabric-all-in-one/README.md index 69597d73e17..617922198f8 100644 --- a/tools/docker/fabric-all-in-one/README.md +++ b/tools/docker/fabric-all-in-one/README.md @@ -3,7 +3,16 @@ > This docker image is for `testing` and `development` only. > Do NOT use in production! -An all in one fabric docker image with 1 peer, 1 orderer and 1 channel. +An all in one fabric docker image with the `fabric-samples` repo fully embedded. + +## Usage + +### Building Local Image + +```sh +DOCKER_BUILDKIT=1 docker build ./tools/docker/fabric-all-in-one/ -f ./tools/docker/fabric-all-in-one/Dockerfile_v1.4.x -t faio14x +``` +### VSCode ## Usage diff --git a/tools/docker/fabric-all-in-one/supervisord.conf b/tools/docker/fabric-all-in-one/supervisord.conf index 811a873c72e..5faca58d644 100644 --- a/tools/docker/fabric-all-in-one/supervisord.conf +++ b/tools/docker/fabric-all-in-one/supervisord.conf @@ -5,7 +5,7 @@ logfile_backups=10 loglevel = info [program:sshd] -command=/usr/sbin/sshd -D +command=/usr/sbin/sshd -D -dd autostart=true autorestart=true stderr_logfile=/var/log/sshd.err.log