From 50864085d98d841878e4c1dcf1f62d4a0b0ad818 Mon Sep 17 00:00:00 2001 From: Chalith Desaman Date: Mon, 19 Sep 2022 11:09:50 +0530 Subject: [PATCH] Cross platform support by migrating to npm (#10) --- npm/.gitignore | 1 + npm/README.md | 43 ++++++++++ npm/appenv.js | 15 ++++ npm/index.js | 59 ++++++++++++++ npm/lib/child-proc.js | 9 +++ npm/lib/command-handler.js | 158 +++++++++++++++++++++++++++++++++++++ npm/lib/common.js | 122 ++++++++++++++++++++++++++++ npm/lib/logger.js | 53 +++++++++++++ npm/package-lock.json | 35 ++++++++ npm/package.json | 22 ++++++ npm/scripts/install.js | 3 + 11 files changed, 520 insertions(+) create mode 100644 npm/.gitignore create mode 100644 npm/README.md create mode 100644 npm/appenv.js create mode 100755 npm/index.js create mode 100644 npm/lib/child-proc.js create mode 100644 npm/lib/command-handler.js create mode 100644 npm/lib/common.js create mode 100644 npm/lib/logger.js create mode 100644 npm/package-lock.json create mode 100644 npm/package.json create mode 100644 npm/scripts/install.js diff --git a/npm/.gitignore b/npm/.gitignore new file mode 100644 index 0000000..b512c09 --- /dev/null +++ b/npm/.gitignore @@ -0,0 +1 @@ +node_modules \ No newline at end of file diff --git a/npm/README.md b/npm/README.md new file mode 100644 index 0000000..da0efa4 --- /dev/null +++ b/npm/README.md @@ -0,0 +1,43 @@ +# HotPocket developer kit +Evernode uses HotPocket as its smart contract engine. HotPocket smart contracts can be developed using any POSIX-compliant language/framework. To make it easy to develop and test HotPocket smart contracts on your local PC, you can use HotPocket developer kit. + +## Installation + +### Prerequisites +HotPocket developer kit requires you to install [Docker Engine](https://docs.docker.com/engine/install/) and [NodeJs](https://nodejs.org/en/) on your development machine. + +### Supports cross platform +This is a npm global package which supports both Linux and Windows +1. Install [prerequisites](#prerequisites). +2. Run the following command to install hpdevkit on your machine. + ``` + npm i -g hpdevkit + ``` + +## Updates +Update `hpdevkit` to the latest and update the supporting docker images. + +Run one of following commands to update hpdevkit. +- Method 1 - Using hpdevkit CLI + ``` + hpdevkit update + ``` + +- Method 2 - Using npm + ``` + npm update -g hpdevkit + ``` + +**NOTE: You need to re-deploy your contracts to make the new changes effective.** + +## Uninstall +Uninstall `hpdevkit` and the supporting docker images and containers. + +- Using hpdevkit CLI + ``` + hpdevkit uninstall + ``` + +**NOTE: Uninstalling from hpdevkit CLI is recommended. If you uninstall using npm you'll have to clean hpdevkit supporting docker images and containers manually.** + +_**NOTE:** In Linux platforms, for Installation, Update and Uninstallation you'll need root privileges. Add `sudo` to above commands._ diff --git a/npm/appenv.js b/npm/appenv.js new file mode 100644 index 0000000..e07c045 --- /dev/null +++ b/npm/appenv.js @@ -0,0 +1,15 @@ +const process = require('process'); + +const appenv = { + cluster: 'default', + clusterSize: process.env.HP_CLUSTER_SIZE || 3, + defaultNode: process.env.HP_DEFAULT_NODE || 1, + devkitImage: process.env.HP_DEVKIT_IMAGE || 'evernodedev/hpdevkit', + instanceImage: process.env.HP_INSTANCE_IMAGE || 'evernodedev/hotpocket:latest-ubt.20.04-njs.16', + hpUserPortBegin: process.env.HP_USER_PORT_BEGIN || 8081, + hpPeerPortBegin: process.env.HP_PEER_PORT_BEGIN || 22861, +} + +Object.freeze(appenv); + +module.exports = appenv \ No newline at end of file diff --git a/npm/index.js b/npm/index.js new file mode 100755 index 0000000..22728a3 --- /dev/null +++ b/npm/index.js @@ -0,0 +1,59 @@ +#! /usr/bin/env node + +const { program } = require('commander'); +const { version, codeGen, deploy, clean, logs, start, stop, update, uninstall } = require('./lib/command-handler'); + +program + .command('version') + .description('hpdevkit version') + .action(version); + +program + .command('gen ') + .description('hpdevkit gen ') + .action(codeGen); + +program + .command('deploy ') + .description('hpdevkit deploy ') + .action(deploy); + +program + .command('clean') + .description('hpdevkit clean') + .action(clean); + +program + .command('logs ') + .description('hpdevkit logs ') + .action(logs); + +program + .command('start ') + .description('hpdevkit start ') + .action(start); + +program + .command('stop ') + .description('hpdevkit stop ') + .action(stop); + +program + .command('update') + .description('hpdevkit update') + .action(update); + +program + .command('uninstall') + .description('uninstall') + .action(uninstall); + +try { + program.parse(); +} +catch (e) { + // Console outputs will be handled inside command functions. + // Log the exception if not a console output. + if (!('stdout' in e) && !('stderr' in e)) + console.error(e); +} \ No newline at end of file diff --git a/npm/lib/child-proc.js b/npm/lib/child-proc.js new file mode 100644 index 0000000..249ce6a --- /dev/null +++ b/npm/lib/child-proc.js @@ -0,0 +1,9 @@ +const { execSync } = require('child_process'); + +function exec(commad, streamOut = false) { + return execSync(commad, streamOut ? { stdio: 'inherit' } : {stdio : 'pipe' }); +} + +module.exports = { + exec +}; \ No newline at end of file diff --git a/npm/lib/command-handler.js b/npm/lib/command-handler.js new file mode 100644 index 0000000..8043f69 --- /dev/null +++ b/npm/lib/command-handler.js @@ -0,0 +1,158 @@ +const fs = require('fs'); +const appenv = require('../appenv'); +const { exec } = require('./child-proc'); +const { + CONSTANTS, + initializeDeploymentCluster, + runOnContainer, + executeOnContainer, + teardownDeploymentCluster, + isExists, + updateDockerImages +} = require('./common'); +const { success, error, info, warn } = require('./logger'); + +function version() { + info(`command: version`); + + try { + const res = exec(`npm -g list ${CONSTANTS.npmPackageName} --depth=0`); + const splitted = res.toString().split('\n'); + if (splitted.length > 1) { + success(`\n${splitted[1].split('@')[1]}\n`); + return; + } + } + catch (e) { } + + error(`\n${CONSTANTS.npmPackageName} is not installed.`); +} + +function codeGen(platform, apptype, projName) { + info("Code generator\n"); + + if (fs.existsSync(projName)) { + error(`Directory '${projName}' already exists.`); + return; + } + + let containerStarted = false; + try { + runOnContainer(CONSTANTS.codegenContainerName, null, null, null, null, `${platform} ${apptype} ${projName}`, 'codegen'); + containerStarted = true; + exec(`docker cp ${CONSTANTS.codegenContainerName}:${CONSTANTS.codegenOutputDir} ./${projName}`); + success(`Project '${projName}' created.`); + } + catch (e) { + error(`Project '${projName}' generation failed.`); + } + finally { + if (containerStarted) + exec(`docker rm ${CONSTANTS.codegenContainerName}`, false); + } +} + +function deploy(contractPath) { + info(`command: deploy (cluster: ${appenv.cluster})`); + + initializeDeploymentCluster(); + + // If copying a directory, delete target bundle directory. If not create empty target bundle directory to copy a file. + const prepareBundleDir = contractPath ? + `rm -rf ${CONSTANTS.bundleMount}` : + `mkdir -p ${CONSTANTS.bundleMount} && rm -rf ${CONSTANTS.bundleMount}/* ${CONSTANTS.bundleMount}/.??*`; + + executeOnContainer(CONSTANTS.deploymentContainerName, prepareBundleDir); + exec(`docker cp ${contractPath} "${CONSTANTS.deploymentContainerName}:${CONSTANTS.bundleMount}"`); + + // Sync contract bundle to all instance directories in the cluster. + executeOnContainer(CONSTANTS.deploymentContainerName, 'cluster stop ; cluster sync ; cluster start'); + + if (appenv.defaultNode > 0) { + info(`Streaming logs of node ${appenv.defaultNode}:`); + executeOnContainer(CONSTANTS.deploymentContainerName, `cluster logs ${appenv.defaultNode}`); + } +} + +function clean() { + info(`command: clean (cluster: ${appenv.cluster})`); + + teardownDeploymentCluster(); +} + +function logs(nodeNumber) { + info(`command: logs (cluster: ${appenv.cluster})`); + + runOnContainer(null, null, true, true, null, `logs ${nodeNumber}`, 'cluster'); +} + +function start(nodeNumber) { + info(`command: start (cluster: ${appenv.cluster})`); + + runOnContainer(null, null, true, true, null, `start ${nodeNumber}`, 'cluster'); +} + +function stop(nodeNumber) { + info(`command: stop (cluster: ${appenv.cluster})`); + + runOnContainer(null, null, true, true, null, `stop ${nodeNumber}`, 'cluster'); +} + +function update() { + info(`command: update`); + + // Update npm package if outdated (Docker images will be updated from there). Otherwise only update the docker images. + try { + exec(`npm -g outdated ${CONSTANTS.npmPackageName}`); + info('\nUpdating docker images...'); + updateDockerImages(); + } + catch (e) { + const splitted = e.stdout.toString().trim().split('\n').map(l => l.trim().split(/\s+/)); + if (splitted.length > 1) { + info(`\nUpdating ${CONSTANTS.npmPackageName} npm package...`); + exec(`npm -g install ${CONSTANTS.npmPackageName}@${splitted[1][3]}`, true); + } + } + + success('\nUpdate Completed !!'); + warn('NOTE: You need to re-deploy your contracts to make the new changes effective.'); +} + +function uninstall() { + info(`command: uninstall`); + + info(`\nUninstalling ${CONSTANTS.npmPackageName} npm package...`); + exec(`npm -g uninstall ${CONSTANTS.npmPackageName}`, true); + + // Remove deployment cluster if exist. + if (isExists(CONSTANTS.deploymentContainerName)) { + info('\nCleaning the deployed contracts...'); + teardownDeploymentCluster(); + } + + // Remove docker images if exist. + if (isExists(appenv.devkitImage, 'image')) { + info('\nRemoving devkit docker image...'); + exec(`docker image rm ${appenv.devkitImage}`, true); + } + + if (isExists(appenv.instanceImage, 'image')) { + info('\nRemoving instance docker image...'); + exec(`docker image rm ${appenv.instanceImage}`, true); + } + + success('\nUninstalled hpdevkit !!'); +} + +module.exports = { + version, + codeGen, + deploy, + clean, + logs, + start, + stop, + update, + uninstall +}; \ No newline at end of file diff --git a/npm/lib/common.js b/npm/lib/common.js new file mode 100644 index 0000000..11030f8 --- /dev/null +++ b/npm/lib/common.js @@ -0,0 +1,122 @@ +const appenv = require("../appenv"); +const { exec } = require("./child-proc"); +const { log, info } = require("./logger"); + +const GLOBAL_PREFIX = "hpdevkit"; +const VERSION = "0.1.0"; + +const CONSTANTS = { + npmPackageName: `hpdevkit`, + volumeMount: `/${GLOBAL_PREFIX}_vol`, + volume: `${GLOBAL_PREFIX}_${appenv.cluster}_vol`, + network: `${GLOBAL_PREFIX}_${appenv.cluster}_net`, + containerPrefix: `${GLOBAL_PREFIX}_${appenv.cluster}_node`, + bundleMount: `${GLOBAL_PREFIX}_vol/contract_bundle`, + deploymentContainerName: `${GLOBAL_PREFIX}_${appenv.cluster}_deploymgr`, + confOverrideFile: "hp.cfg.override", + codegenOutputDir: "/codegen-output", + codegenContainerName: `${GLOBAL_PREFIX}_codegen` +}; + +function runOnContainer(name, detached, autoRemove, mountStock, mountVolume, entryCmd, entryPoint, interactive = true) { + command = `docker run`; + + if (interactive) + command += " -it"; + + if (name) + command += ` --name ${name}`; + + if (detached) + command += " -d"; + + if (autoRemove) + command += " --rm"; + + if (mountStock) + command += " --mount type=bind,src=/var/run/docker.sock,dst=/var/run/docker.sock"; + + if (mountVolume) + command += ` --mount type=volume,src=${CONSTANTS.volume},dst=${CONSTANTS.volumeMount}`; + + if (entryPoint) + command += ` --entrypoint ${entryPoint}`; + else + command += " --entrypoint /bin/bash"; + + command += ` -e CLUSTER=${appenv.cluster} -e CLUSTER_SIZE=${appenv.clusterSize} -e DEFAULT_NODE=${appenv.defaultNode} -e VOLUME=${CONSTANTS.volume} -e NETWORK=${CONSTANTS.network}`; + command += ` -e CONTAINER_PREFIX=${CONSTANTS.containerPrefix} -e VOLUME_MOUNT=${CONSTANTS.volumeMount} -e BUNDLE_MOUNT=${CONSTANTS.bundleMount} -e HOTPOCKET_IMAGE=${appenv.instanceImage}`; + command += ` -e CONFIG_OVERRIDES_FILE=${CONSTANTS.confOverrideFile} -e CODEGEN_OUTPUT=${CONSTANTS.codegenOutputDir}`; + command += ` -e HP_USER_PORT_BEGIN=${appenv.hpUserPortBegin} -e HP_PEER_PORT_BEGIN=${appenv.hpPeerPortBegin}`; + + command += ` ${appenv.devkitImage}`; + + if (entryCmd) { + if (entryPoint) + command += ` ${entryCmd}`; + else + command += ` -c "${entryCmd}"`; + } + + exec(command, true); +} + +function executeOnContainer(name, cmd) { + if (name) + exec(`docker exec ${name} /bin/bash -c "${cmd}"`, true); +} + +function isExists(name, type = null) { + try { + const res = exec(`docker ${type === 'image' ? 'image ' : ''}inspect ${name}`); + if (!res) + return false; + const resJson = JSON.parse(res.toString().trim()); + return !!(resJson && resJson.length); + } + catch (e) { + return false; + } +} + +function initializeDeploymentCluster() { + if (!isExists(CONSTANTS.deploymentContainerName)) { + log("\nInitializing deployment cluster"); + + // Stop cluster if running. Create cluster if not exists. + runOnContainer(CONSTANTS.deploymentContainerName, null, true, true, null, 'cluster stop ; cluster create', null); + + // Spin up management container. + runOnContainer(CONSTANTS.deploymentContainerName, true, false, true, true, null, null); + + // Bind the instance mesh network config together. + executeOnContainer(CONSTANTS.deploymentContainerName, 'cluster bindmesh'); + } +} + +function teardownDeploymentCluster() { + exec(`docker stop ${CONSTANTS.deploymentContainerName}`); + exec(`docker rm ${CONSTANTS.deploymentContainerName}`); + runOnContainer(null, null, true, true, null, "cluster stop ; cluster destroy", null, false); +} + +function updateDockerImages() { + exec(`docker pull ${appenv.devkitImage}`); + exec(`docker pull ${appenv.instanceImage}`, true); + + // Clear if there's already deployed cluster since they are outdated now. + if (isExists(CONSTANTS.deploymentContainerName)) { + info('\nCleaning the deployed contracts...'); + teardownDeploymentCluster(); + } +} + +module.exports = { + runOnContainer, + executeOnContainer, + isExists, + initializeDeploymentCluster, + teardownDeploymentCluster, + updateDockerImages, + CONSTANTS +}; \ No newline at end of file diff --git a/npm/lib/logger.js b/npm/lib/logger.js new file mode 100644 index 0000000..c912b2e --- /dev/null +++ b/npm/lib/logger.js @@ -0,0 +1,53 @@ +const Reset = "\x1b[0m"; +const Bright = "\x1b[1m"; +const Dim = "\x1b[2m"; +const Underscore = "\x1b[4m"; +const Blink = "\x1b[5m"; +const Reverse = "\x1b[7m"; +const Hidden = "\x1b[8m"; + +const FgBlack = "\x1b[30m"; +const FgRed = "\x1b[31m"; +const FgGreen = "\x1b[32m"; +const FgYellow = "\x1b[33m"; +const FgBlue = "\x1b[34m"; +const FgMagenta = "\x1b[35m"; +const FgCyan = "\x1b[36m"; +const FgWhite = "\x1b[37m"; + +const BgBlack = "\x1b[40m"; +const BgRed = "\x1b[41m"; +const BgGreen = "\x1b[42m"; +const BgYellow = "\x1b[43m"; +const BgBlue = "\x1b[44m"; +const BgMagenta = "\x1b[45m"; +const BgCyan = "\x1b[46m"; +const BgWhite = "\x1b[47m"; + +function success(...args) { + console.log(`${FgGreen}%s${Reset}`, ...args); +} + +function info(...args) { + console.log(`${FgBlue}%s${Reset}`, ...args); +} + +function warn(...args) { + console.log(`${FgYellow}%s${Reset}`, ...args); +} + +function error(...args) { + console.log(`${FgRed}%s${Reset}`, ...args); +} + +function log(...args) { + console.log(...args); +} + +module.exports = { + success, + info, + warn, + error, + log +}; \ No newline at end of file diff --git a/npm/package-lock.json b/npm/package-lock.json new file mode 100644 index 0000000..de6ba55 --- /dev/null +++ b/npm/package-lock.json @@ -0,0 +1,35 @@ +{ + "name": "hpdevkit", + "version": "0.5.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "hpdevkit", + "version": "0.5.0", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "commander": "9.4.0" + }, + "bin": { + "hpdevkit": "index.js" + } + }, + "node_modules/commander": { + "version": "9.4.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.4.0.tgz", + "integrity": "sha512-sRPT+umqkz90UA8M1yqYfnHlZA7fF6nSphDtxeywPZ49ysjxDQybzk13CL+mXekDRG92skbcqCLVovuCusNmFw==", + "engines": { + "node": "^12.20.0 || >=14" + } + } + }, + "dependencies": { + "commander": { + "version": "9.4.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.4.0.tgz", + "integrity": "sha512-sRPT+umqkz90UA8M1yqYfnHlZA7fF6nSphDtxeywPZ49ysjxDQybzk13CL+mXekDRG92skbcqCLVovuCusNmFw==" + } + } +} diff --git a/npm/package.json b/npm/package.json new file mode 100644 index 0000000..afdea07 --- /dev/null +++ b/npm/package.json @@ -0,0 +1,22 @@ +{ + "name": "hpdevkit", + "version": "0.5.0", + "description": "Developer toolkit for HotPocket smart contract development", + "scripts": { + "install": "node scripts/install.js" + }, + "keywords": [ + "HotPocket", + "toolkit", + "hpdevkit", + "smart contract" + ], + "homepage": "https://github.com/HotPocketDev/evernode-sdk", + "license": "MIT", + "dependencies": { + "commander": "9.4.0" + }, + "bin": { + "hpdevkit": "./index.js" + } +} diff --git a/npm/scripts/install.js b/npm/scripts/install.js new file mode 100644 index 0000000..fadade2 --- /dev/null +++ b/npm/scripts/install.js @@ -0,0 +1,3 @@ +const { updateDockerImages } = require("../lib/common"); + +updateDockerImages(); \ No newline at end of file