From 30807767167773f3528cd418949ff5e62588e33d Mon Sep 17 00:00:00 2001 From: Gabriel Montes Date: Tue, 11 Jul 2023 15:29:38 -0400 Subject: [PATCH 1/3] Allow admins to get all user's nodes --- src/commands/nodes.js | 24 +++++++++++++++--------- src/nodes/list.js | 32 ++++++++++++++++++++------------ 2 files changed, 35 insertions(+), 21 deletions(-) diff --git a/src/commands/nodes.js b/src/commands/nodes.js index 96ac4f6..b6fcd7d 100644 --- a/src/commands/nodes.js +++ b/src/commands/nodes.js @@ -37,27 +37,33 @@ class NodesCommand extends Command { NodesCommand.description = 'Manage your Bloq nodes' NodesCommand.flags = { - serviceId: flags.string({ char: 's', description: 'service id' }), - authType: flags.enum({ - char: 't', - description: 'auth type (jwt or basic)', - default: 'basic', - options: ['jwt', 'basic'] - }), all: flags.boolean({ char: 'a', description: 'list all nodes', default: false, required: false }), + allUsers: flags.boolean({ + char: 'A', + description: 'list nodes from all user (admins only)', + default: false, + required: false + }), + authType: flags.enum({ + char: 't', + description: 'auth type (jwt or basic)', + default: 'basic', + options: ['jwt', 'basic'] + }), json: flags.boolean({ char: 'j', description: 'JSON output' }), + lines: flags.integer({ char: 'l', description: 'max lines to retrieve' }), nodeId: flags.string({ char: 'i', description: 'node id' }), + serviceId: flags.string({ char: 's', description: 'service id' }), yes: flags.boolean({ char: 'y', description: 'answer "yes" to prompts', default: false - }), - lines: flags.integer({ char: 'l', description: 'max lines to retrieve' }) + }) } NodesCommand.args = [ diff --git a/src/nodes/list.js b/src/nodes/list.js index 2d5880e..f1c8017 100644 --- a/src/nodes/list.js +++ b/src/nodes/list.js @@ -16,16 +16,20 @@ const config = require('../config') * * @param {Object} params object * @param {Object} params.accessToken Account access token - * @param {Object} params.all Boolean defining if it should show killed nodes + * @param {Object} [params.all] Boolean defining if it should show killed nodes + * @param {string} [params.allUsers] List nodes from all users + * @param {string} [params.json] Format output as JSON * @returns {Promise} The information nodes promise */ -async function listNodes({ accessToken, all, json }) { +async function listNodes({ accessToken, all, allUsers, json }) { const isJson = typeof json !== 'undefined' !isJson && consola.info('Retrieving all nodes\n') const env = config.get('env') || 'prod' - const url = `${config.get(`services.${env}.nodes.url`)}/users/me/nodes` + const url = `${config.get(`services.${env}.nodes.url`)}${ + allUsers ? '/nodes' : '/users/me/nodes' + }` return fetcher(url, 'GET', accessToken).then(res => { if (!res.ok) { @@ -43,29 +47,33 @@ async function listNodes({ accessToken, all, json }) { return } body = body.map(function ({ - id, chain, - state, - network, - ip, createdAt, + id, + ip, + network, serviceData, - stoppedAt + state, + stoppedAt, + user }) { const node = { id, + ip, chain, network, - ip, - state, - createdAt, version: serviceData.software, - performance: serviceData.performance + performance: serviceData.performance, + state, + createdAt } if (all) { node.stoppedAt = stoppedAt || 'N/A' } + if (allUsers) { + node.user = user + } return node }) From fde8c8f3426ea692740ec71554943d94bd19bbb3 Mon Sep 17 00:00:00 2001 From: Gabriel Montes Date: Tue, 11 Jul 2023 15:32:47 -0400 Subject: [PATCH 2/3] Fix typo --- src/client-keys/create.js | 4 ++-- src/clusters/create.js | 4 ++-- src/commands/client-token.js | 4 ++-- src/nodes/create.js | 4 ++-- src/utils.js | 4 ++-- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/client-keys/create.js b/src/client-keys/create.js index c1d5753..222be7a 100644 --- a/src/client-keys/create.js +++ b/src/client-keys/create.js @@ -4,7 +4,7 @@ const consola = require('consola') const inquirer = require('inquirer') const config = require('../config') const { - coppyToClipboard, + copyToClipboard, fetcher, formatOutput, formatErrorResponse @@ -56,7 +56,7 @@ async function createClientKey({ user, accessToken, json }) { consola.warn( 'You will NOT be able to see your client secret again. Remember to copy it and keep it safe.' ) - coppyToClipboard(res.data.clientSecret, 'Client secret') + copyToClipboard(res.data.clientSecret, 'Client secret') consola.success(`Generated new client keys:`) } diff --git a/src/clusters/create.js b/src/clusters/create.js index 25a94ae..3e5677c 100644 --- a/src/clusters/create.js +++ b/src/clusters/create.js @@ -11,7 +11,7 @@ const { const jwtDecode = require('jwt-decode') const config = require('../config') -const { coppyToClipboard } = require('../utils') +const { copyToClipboard } = require('../utils') const CLUSTER_MIN_CAPACITY = 2 const CLUSTER_MAX_CAPACITY = 10 @@ -89,7 +89,7 @@ async function createCluster(params) { if (!isJson) { consola.success(`Initialized new cluster from service ${serviceId}\n`) - coppyToClipboard(data.id, 'Cluster id') + copyToClipboard(data.id, 'Cluster id') } }) } diff --git a/src/commands/client-token.js b/src/commands/client-token.js index 246c8f8..a073b1c 100644 --- a/src/commands/client-token.js +++ b/src/commands/client-token.js @@ -12,7 +12,7 @@ const inquirer = require('inquirer') const { Command, flags } = require('@oclif/command') const config = require('../config') -const { coppyToClipboard } = require('../utils') +const { copyToClipboard } = require('../utils') class ClientTokenCommand extends Command { async run() { @@ -88,7 +88,7 @@ class ClientTokenCommand extends Command { config.set('refreshToken', data.refreshToken) } - coppyToClipboard(data.accessToken, 'Client access token') + copyToClipboard(data.accessToken, 'Client access token') }) } } diff --git a/src/nodes/create.js b/src/nodes/create.js index 59bd7bd..edac9a0 100644 --- a/src/nodes/create.js +++ b/src/nodes/create.js @@ -11,7 +11,7 @@ const { const jwtDecode = require('jwt-decode') const config = require('../config') -const { coppyToClipboard } = require('../utils') +const { copyToClipboard } = require('../utils') /** * Creates a node from a service ID (Valid for admin users) @@ -86,7 +86,7 @@ async function createNode({ accessToken, serviceId, authType, json }) { if (!isJson) { consola.success(`Initialized new node from service ${serviceId}\n`) - coppyToClipboard(res.data.id, 'Node id') + copyToClipboard(res.data.id, 'Node id') } }) } diff --git a/src/utils.js b/src/utils.js index 476176c..c42997a 100644 --- a/src/utils.js +++ b/src/utils.js @@ -11,7 +11,7 @@ const ora = require('ora') * @param {string} value What to copy to the clipboard. * @param {string} name The name of what is being copied. */ -const coppyToClipboard = (value, name) => +const copyToClipboard = (value, name) => clipboardy .write(value) .then(() => consola.info(`${name} was copied to the clipboard.`)) @@ -115,7 +115,7 @@ const formatOutput = (isJson, dataObj) => { } module.exports = { - coppyToClipboard, + copyToClipboard, fetcher, formatCredentials, formatOutput, From d1ae60a0114b78021986c86e69f4cdfcefa3f7f8 Mon Sep 17 00:00:00 2001 From: Gabriel Montes Date: Tue, 11 Jul 2023 15:33:47 -0400 Subject: [PATCH 3/3] List clusters exact sw versions --- src/clusters/list.js | 154 ++++++++++++++++++++++++------------------- src/utils.js | 74 ++++++++++++++++++++- 2 files changed, 161 insertions(+), 67 deletions(-) diff --git a/src/clusters/list.js b/src/clusters/list.js index 4a065b2..26fa800 100644 --- a/src/clusters/list.js +++ b/src/clusters/list.js @@ -1,10 +1,17 @@ 'use strict' +require('console.table') + const consola = require('consola') const lodash = require('lodash') -const { fetcher, formatErrorResponse, formatOutput } = require('../utils') -require('console.table') +const { + fetcher, + formatErrorResponse, + formatOutput, + getArchiveStatus, + getClientVersion +} = require('../utils') const config = require('../config') /** @@ -12,13 +19,14 @@ const config = require('../config') * * @param {Object} params object * @param {string} params.accessToken Account access token - * @param {boolean} params.all Flag defining if it should show killed clusters - * @param {string} params.allClusters List clusters from all users - * @param {string} params.sort Key used to sort the output + * @param {boolean} [params.all] Flag defining if it should show killed clusters + * @param {string} [params.allClusters] List clusters from all users + * @param {string} [params.json] Format output as JSON + * @param {string} [params.sort] Key used to sort the output * @returns {Promise} The information cluster promise */ -async function listClusters({ accessToken, all, allClusters, sort, json }) { +function listClusters({ accessToken, all, allClusters, sort, json }) { const isJson = typeof json !== 'undefined' !isJson && consola.info('Retrieving clusters...') @@ -27,73 +35,87 @@ async function listClusters({ accessToken, all, allClusters, sort, json }) { allClusters ? '/clusters' : '/users/me/clusters' }` - return fetcher(url, 'GET', accessToken).then(res => { - if (!res.ok) { - formatErrorResponse( - isJson, - `Error retrieving all clusters: ${res.status}` - ) - return - } - - let body = res.data - if (!body) { - return - } - - if (!body.length) { - const user = `${config.get('user')}` - formatErrorResponse(isJson, `No clusters were found for user ${user}`) - return - } - - body = body.map(function ({ - alias, - capacity, - chain, - createdAt, - healthCount = 0, - id, - name, - network, - serviceData = {}, - state, - stoppedAt, - updatingService, - user - }) { - const cluster = { - id, - chain, - network, - 'name/alias': alias || name, - 'state': state === 'started' && updatingService ? 'updating' : state, - 'health': `${Math.round((healthCount / capacity) * 100)}%`, - createdAt, - 'version': serviceData.software, - 'performance': serviceData.performance + return fetcher(url, 'GET', accessToken) + .then(res => { + if (!res.ok) { + formatErrorResponse( + isJson, + `Error retrieving all clusters: ${res.status}` + ) + return } - if (all) { - cluster.stoppedAt = stoppedAt + const { data } = res + if (!data) { + return } - if (allClusters) { - cluster.user = user.email + if (!data.length) { + const user = `${config.get('user')}` + formatErrorResponse(isJson, `No clusters were found for user ${user}`) + return } - return cluster - }) + // eslint-disable-next-line consistent-return + return Promise.all( + data.map(clusterRow => + Promise.all([ + getClientVersion(clusterRow).catch(() => 'error'), + getArchiveStatus(clusterRow) + ]).then(function ([clientVersion, archive]) { + const { + alias, + capacity, + chain, + createdAt, + healthCount = 0, + id, + name, + network, + serviceData = {}, + state, + stoppedAt, + updatingService, + user + } = clusterRow + + const cluster = { + id, + 'alias/name': alias ? `${alias} (${name})` : name, + chain, + network, + 'version': `${ + clientVersion && clientVersion !== 'error' + ? clientVersion + : serviceData.software + }${archive !== null ? ` (${archive ? 'archive' : 'full'})` : ''}`, + 'performance': serviceData.performance, + 'cap': capacity, + 'state': + state === 'started' && updatingService ? 'updating' : state, + 'health': `${Math.round((healthCount / capacity) * 100)}%`, + createdAt + } - if (!all) { - body = body.filter(n => n.state !== 'stopped') - } + if (all) { + cluster.stoppedAt = stoppedAt + } + if (allClusters) { + cluster.user = user.email + } + return cluster + }) + ) + ) + }) + .then(function (clusters) { + const body = all ? clusters : clusters.filter(n => n.state !== 'stopped') - !isJson && consola.success(`Got ${body.length} clusters:\n`) - formatOutput( - isJson, - lodash.sortBy(body, sort ? sort.split(',') : 'createdAt') - ) - }) + !isJson && consola.success(`Got ${body.length} clusters:\n`) + formatOutput( + isJson, + lodash.sortBy(body, sort ? sort.split(',') : 'createdAt') + ) + }) } module.exports = listClusters diff --git a/src/utils.js b/src/utils.js index c42997a..d4dc750 100644 --- a/src/utils.js +++ b/src/utils.js @@ -114,11 +114,83 @@ const formatOutput = (isJson, dataObj) => { } } +const getAuthHeader = ({ user, pass }) => + `Basic ${Buffer.from(`${user}:${pass}`).toString('base64')}` + +const jsonRpcCall = function (url, method, params, auth) { + const options = { + method: 'POST', + headers: { 'content-type': 'application/json' } + } + if (auth.type === 'basic') { + options.headers.authorization = getAuthHeader(auth) + } + options.body = JSON.stringify({ jsonrpc: '2.0', method, params, id: 1 }) + return fetch(url, options).then(res => res.json()) +} + +const getClientVersion = function ({ auth, chain, domain }) { + switch (chain) { + case 'avalanche': + return jsonRpcCall( + `https://${domain}:9650/ext/info`, + 'info.getNodeVersion', + [], + auth + ).then(({ result }) => result.version) + case 'etc': + case 'eth': + case 'polygon': + return jsonRpcCall( + `https://${domain}:8545`, + 'web3_clientVersion', + [], + auth + ).then( + ({ result }) => + result && result.match(/^[A-Za-z]+\/?v[0-9.]+/)[0].replace(/\//g, ' ') + ) + case 'algorand': + return fetch(`https://${domain}:8080/versions`, { + method: 'GET', + headers: { authorization: getAuthHeader(auth) } + }) + .then(res => res.json()) + .then(res => res.build) + .then( + ({ major, minor, build_number }) => + `Algod v${major}.${minor}.${build_number}` + ) + + default: + return Promise.resolve('?') + } +} + +const getArchiveStatus = function ({ auth, chain, domain }) { + switch (chain) { + case 'etc': + case 'eth': + return jsonRpcCall( + `https://${domain}:8545`, + 'eth_getBalance', + ['0x1111111111111111111111111111111111111111', '0x1'], + auth + ) + .then(({ error }) => !error) + .catch(() => null) + default: + return Promise.resolve(null) + } +} + module.exports = { copyToClipboard, fetcher, formatCredentials, + formatErrorResponse, formatOutput, formatResponse, - formatErrorResponse + getArchiveStatus, + getClientVersion }