From 92c91e2efa0ca84f9799260aa16b26c085772dba Mon Sep 17 00:00:00 2001 From: David Huffman Date: Tue, 14 Nov 2023 12:42:03 -0500 Subject: [PATCH 1/2] add gather version code Signed-off-by: David Huffman --- .../apollo/src/assets/i18n/en/messages.json | 4 +- .../src/components/Settings/Settings.js | 58 ++++++++ .../src/components/Settings/_settings.scss | 4 +- packages/apollo/src/rest/NodeRestApi.js | 5 + packages/apollo/src/utils/helper.js | 1 + packages/athena/libs/component_lib.js | 89 ++++++++++++ packages/athena/libs/misc.js | 20 +++ packages/athena/libs/other_apis_lib.js | 130 ++++++++++++++++++ packages/athena/routes/other_apis.js | 14 ++ 9 files changed, 322 insertions(+), 3 deletions(-) diff --git a/packages/apollo/src/assets/i18n/en/messages.json b/packages/apollo/src/assets/i18n/en/messages.json index 7fa351cb..a35c5729 100644 --- a/packages/apollo/src/assets/i18n/en/messages.json +++ b/packages/apollo/src/assets/i18n/en/messages.json @@ -2846,7 +2846,9 @@ "log_detail_desc": "The full details of this activity log are below.", "log_columns_desc": "Display", "audit_no_access_msg": "Only users with the \"Manager\" role can view this page.", - "threat_message":"This service instance must be deleted by Oct 31, 2023 to avoid a blockchain network outage", + "threat_message": "This service instance must be deleted by Oct 31, 2023 to avoid a blockchain network outage", + "version_debug_msg": "Version summary", + "version_debug_tooltip": "Use this tool to export a version summary which shows the versions of each of your component. This summary is helpful for debugging/support purposes.", "hide_archived_channels": "Hide Archived Channels", "show_archived_channels": "Show Archived Channels" } diff --git a/packages/apollo/src/components/Settings/Settings.js b/packages/apollo/src/components/Settings/Settings.js index d056b022..868fee54 100644 --- a/packages/apollo/src/components/Settings/Settings.js +++ b/packages/apollo/src/components/Settings/Settings.js @@ -35,6 +35,7 @@ import ToggleSmallSkeleton from 'carbon-components-react/lib/components/ToggleSm import { NodeRestApi } from '../../rest/NodeRestApi'; import IdentityApi from '../../rest/IdentityApi'; import SidePanel from '../SidePanel/SidePanel'; +import { EventsRestApi } from '../../rest/EventsRestApi'; const SCOPE = 'comp_settings'; const Log = new Logger(SCOPE); @@ -671,6 +672,62 @@ export class Settings extends Component { ); } + // show section on version gathering (debug) + renderVersionDebug(translate) { + return ( +
+
+

+ + {translate('version_debug_tooltip')} + +

+
+ +
+
+
+ ); + } + + // download the version summary as a json file + downloadVersion(json) { + let filename = 'versions.' + Date.now() + '.json'; + const createTarget = document.body; + let link = document.createElement('a'); + if (link.download !== undefined) { + let blob = new Blob([JSON.stringify(json, null, '\t')], { type: 'application/json' }); + let url = URL.createObjectURL(blob); + link.setAttribute('download', filename); + link.setAttribute('href', url); + link.style.visibility = 'hidden'; + createTarget.appendChild(link); + link.click(); + createTarget.removeChild(link); + + try { + EventsRestApi.recordActivity({ status: 'success', log: 'generating version summary' }); + } catch (e) { + Log.error('unable to record version summary/gathering', e); + } + } + } + render = () => { const translate = this.props.translate; const progress_width = isNaN(this.props.width) ? 0 : this.props.width; @@ -707,6 +764,7 @@ export class Settings extends Component { )} + {this.renderVersionDebug(translate)} {this.renderDataManagement(translate)} {window && window.location && window.location.href && window.location.href.includes('debug') && this.renderDeleteSection(translate)} diff --git a/packages/apollo/src/components/Settings/_settings.scss b/packages/apollo/src/components/Settings/_settings.scss index ccb7b654..26400f86 100644 --- a/packages/apollo/src/components/Settings/_settings.scss +++ b/packages/apollo/src/components/Settings/_settings.scss @@ -92,9 +92,9 @@ margin-bottom: 2rem; } -.ibp-settings-bulk-data-container { +/*.ibp-settings-bulk-data-container { padding-top: 1rem; -} +}*/ #ibp-progress-bar-wrap { width: 19rem; diff --git a/packages/apollo/src/rest/NodeRestApi.js b/packages/apollo/src/rest/NodeRestApi.js index e28451ff..74ecd69f 100644 --- a/packages/apollo/src/rest/NodeRestApi.js +++ b/packages/apollo/src/rest/NodeRestApi.js @@ -1168,6 +1168,11 @@ class NodeRestApi { static async deleteAllComponents() { return await RestApi.delete('/saas/api/v3/components/purge'); } + + // get version summary on all components + static async getVersionSummary() { + return await RestApi.get('/api/v3/versions'); + } } export { NodeRestApi, CREATED_COMPONENT_LOCATION, isCreatedComponentLocation }; diff --git a/packages/apollo/src/utils/helper.js b/packages/apollo/src/utils/helper.js index 985e2f83..51ce089d 100644 --- a/packages/apollo/src/utils/helper.js +++ b/packages/apollo/src/utils/helper.js @@ -301,6 +301,7 @@ const Helper = { createTarget.removeChild(link); } }, + /* * Export a nodes as Zip */ diff --git a/packages/athena/libs/component_lib.js b/packages/athena/libs/component_lib.js index 5a88b197..300f26b1 100644 --- a/packages/athena/libs/component_lib.js +++ b/packages/athena/libs/component_lib.js @@ -1700,6 +1700,95 @@ module.exports = function (logger, ev, t) { return null; }; + //-------------------------------------------------- + // Get the version of a component by trying to reach its fabric version endpoint + //-------------------------------------------------- + /* + opts: { + timeout_ms: 0, // [optional] http timeout for asking the component + _max_attempts: 2 // [optional] max number of http reqs to send including orig and retries + } + */ + exports.get_version = (comp_doc, opts, cb) => { + if (!opts) { opts = {}; } + const options = { + method: 'GET', + baseUrl: null, + url: exports.build_version_url(comp_doc), + headers: { 'Accept': 'application/json' }, + timeout: !isNaN(opts.timeout_ms) ? Number(opts.timeout_ms) : ev.HTTP_STATUS_TIMEOUT, // give up quickly b/c we don't want status api to hang + rejectUnauthorized: false, // self signed certs are okay + _name: 'ver_req', + _max_attempts: opts._max_attempts || 2, + _retry_codes: { // list of codes we will retry + '429': '429 rate limit exceeded aka too many reqs', + //'408': '408 timeout', // version calls should not retry a timeout, takes too long + } + }; + + if (options.url === null) { // no url to hit... error out + logger.error('[component] unable to get component version b/c url to use in doc is missing... id:', comp_doc._id); + return cb({ + statusCode: 500, + version: '-' + }); + } else { + t.misc.retry_req(options, (err, resp) => { + const code = t.ot_misc.get_code(resp); + const body = format_body(resp); + return cb(null, { + statusCode: code, + _body: body, + version_url: options.url, + version: t.misc.prettyPrintVersion(body ? body.Version : null), + }); + }); + } + + // json parse the body if asked for + function format_body(resp) { + let body = null; + if (resp && resp.body) { // parse body to JSON + if (typeof resp.body === 'string') { + try { body = JSON.parse(resp.body); } + catch (e) { + logger.error('[component] unable to format version response as JSON for component id', comp_doc._id, e); + return null; + } + } else { + return resp.body; + } + } + return body; + } + }; + + //-------------------------------------------------- + // build a version url for the component, from a component doc + //-------------------------------------------------- + exports.build_version_url = (comp_doc) => { + if (comp_doc) { + + // if its been migrated use the legacy routes + if (comp_doc.migrated_from === ev.STR.LOCATION_IBP_SAAS) { + if (comp_doc.type === ev.STR.CA && comp_doc.api_url) { + return comp_doc.api_url_saas + '/version'; // CA's use this route + } else if (comp_doc.operations_url_saas && (comp_doc.type === ev.STR.ORDERER || comp_doc.type === ev.STR.PEER)) { + return comp_doc.operations_url_saas + '/version'; // peers and orderers use this route + } + } + + // if it hasn't been migrated use regular routes + if (comp_doc.type === ev.STR.CA && comp_doc.api_url) { + return comp_doc.api_url + '/version'; // CA's use this route + } else if (comp_doc.operations_url && (comp_doc.type === ev.STR.ORDERER || comp_doc.type === ev.STR.PEER)) { + return comp_doc.operations_url + '/version'; // peers and orderers use this route + } + } + + return null; + }; + //-------------------------------------------------- // Get /cainfo data from all the CAs in the components array //-------------------------------------------------- diff --git a/packages/athena/libs/misc.js b/packages/athena/libs/misc.js index aeb70812..40f73ae8 100644 --- a/packages/athena/libs/misc.js +++ b/packages/athena/libs/misc.js @@ -1524,5 +1524,25 @@ module.exports = function (logger, t) { } }; + // turn version into a 3 part version string + // 'V2_0' -> 'v2.0.0' + // '2.0.0' -> 'v2.0.0' + // 'V1_4_2' -> 'v1.4.2' + exports.prettyPrintVersion = (str) => { + if (typeof str === 'string') { + if (str === 'unknown') { return '-'; } + str = str.trim(); + if (str[0].toUpperCase() === 'V') { + str = str.substring(1); // cut off the 'V' + } + const parts = str.includes('_') ? str.split('_') : str.split('.'); + while (parts.length < 3) { + parts.push('0'); + } + return 'v' + parts.join('.'); + } + return '-'; + }; + return exports; }; diff --git a/packages/athena/libs/other_apis_lib.js b/packages/athena/libs/other_apis_lib.js index b5b8ef78..394522ec 100644 --- a/packages/athena/libs/other_apis_lib.js +++ b/packages/athena/libs/other_apis_lib.js @@ -899,5 +899,135 @@ module.exports = function (logger, ev, t) { } }; + //----------------------------------------------------------------------------- + // Return a console/component/k8s version summary for support/debug purposes + //----------------------------------------------------------------------------- + exports.version_summary = (req, cb) => { + const console_data = t.ot_misc.parse_versions(); + const ret = { + console: { + version: (console_data && console_data.tag) ? t.misc.prettyPrintVersion(console_data.tag) : '-', + commit: (console_data && console_data.athena) ? console_data.athena : '-', + }, + components: [], + cluster: { + type: '', + version: '' + }, + operator: { + version: '', + available_fabric_versions: {} + }, + timestamp: Date.now(), + }; + + t.async.parallel([ + + // ---- Get component docs from athena db ---- // + (join) => { + t.component_lib.get_all_runnable_components(req, (err, resp) => { + if (err) { + logger.error('[version] unable to get runnable component docs:', err); + return join(null); + } else { + + // iter on each component + t.async.eachLimit(resp, 8, (comp_doc, cb_version) => { + const comp = t.comp_fmt.fmt_component_resp(req, comp_doc); + + if (!comp_doc || !comp_doc.operations_url) { + ret.components.push({ + id: comp.id, + display_name: comp.display_name, + version: '-', + imported: comp.imported, + type: comp.type, + }); + return cb_version(); + } else { + const options = { + method: 'GET', + baseUrl: null, + url: t.component_lib.build_version_url(comp_doc), + headers: { 'Accept': 'application/json' }, + timeout: ev.HTTP_STATUS_TIMEOUT, + rejectUnauthorized: false, // self signed certs are okay + _name: 'status_req', + _max_attempts: 2, + _retry_codes: { // list of codes we will retry + '429': '429 rate limit exceeded aka too many reqs', + } + }; + t.misc.retry_req(options, (err_ver, resp_ver) => { + let body = {}; + if (resp_ver) { + try { + body = JSON.parse(resp_ver.body); + } catch (e) { + logger.error('[version] unable to parse response from component', comp_doc.id); + } + } + ret.components.push({ + id: comp.id, + display_name: comp.display_name, + version: t.misc.prettyPrintVersion(body.Version), + imported: comp.imported, + type: comp.type, + }); + return cb_version(); + }); + } + }, () => { + return join(null, ret); + }); + } + }); + }, + + // ---- Get k8s version ---- // + (join) => { + t.deployer.get_k8s_version((err, resp) => { + if (err) { + // error already logged + return join(null); + } else { + ret.cluster.version = (resp && resp._version) ? t.misc.prettyPrintVersion(resp._version) : '-'; + return join(null, resp); + } + }); + }, + + // ---- Get cluster type ---- // + (join) => { + t.deployer.get_cluster_type((err, resp) => { + if (err) { + // error already logged + return join(null); + } else { + ret.cluster.type = (resp && resp.type) ? resp.type : '-'; + return join(null, resp); + } + }); + }, + + // ---- Get available fabric versions ---- // + (join) => { + t.deployer.get_fabric_versions(req, (err, resp) => { + if (err) { + // error already logged + join(null); + } else { + ret.operator.versions = (resp && resp.versions) ? resp.versions : {}; + join(null, resp.versions); + } + }); + } + + ], (_, results) => { + logger.info('[version] returning version summary'); + return cb(null, t.misc.sortItOut(ret)); + }); + }; + return exports; }; diff --git a/packages/athena/routes/other_apis.js b/packages/athena/routes/other_apis.js index 2ef38337..880187c4 100644 --- a/packages/athena/routes/other_apis.js +++ b/packages/athena/routes/other_apis.js @@ -528,5 +528,19 @@ module.exports = function (logger, ev, t) { }); }); + //----------------------------------------------------------------------------- + // Return the debug/support version summary + // dsh todo - doc this api + //----------------------------------------------------------------------------- + app.get('/api/v[3]/versions', t.middleware.verify_view_action_session, (req, res) => { + t.other_apis_lib.version_summary(req, (err, ret) => { + if (err) { + return res.status(t.ot_misc.get_code(err)).json(err); + } else { + return res.status(200).json(ret); + } + }); + }); + return app; }; From fc473e12d4dd187950199be019481faedc834a99 Mon Sep 17 00:00:00 2001 From: David Huffman Date: Tue, 14 Nov 2023 13:15:09 -0500 Subject: [PATCH 2/2] format resp Signed-off-by: David Huffman --- packages/athena/libs/other_apis_lib.js | 17 ++++++++++++++--- packages/athena/routes/other_apis.js | 1 - 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/packages/athena/libs/other_apis_lib.js b/packages/athena/libs/other_apis_lib.js index 394522ec..bd629f9a 100644 --- a/packages/athena/libs/other_apis_lib.js +++ b/packages/athena/libs/other_apis_lib.js @@ -912,10 +912,10 @@ module.exports = function (logger, ev, t) { components: [], cluster: { type: '', - version: '' + version: '', + go_version: '', }, operator: { - version: '', available_fabric_versions: {} }, timestamp: Date.now(), @@ -992,6 +992,7 @@ module.exports = function (logger, ev, t) { return join(null); } else { ret.cluster.version = (resp && resp._version) ? t.misc.prettyPrintVersion(resp._version) : '-'; + ret.cluster.go_version = (resp && resp.goVersion) ? resp.goVersion : '-'; return join(null, resp); } }); @@ -1017,7 +1018,17 @@ module.exports = function (logger, ev, t) { // error already logged join(null); } else { - ret.operator.versions = (resp && resp.versions) ? resp.versions : {}; + const tmp = (resp && resp.versions) ? resp.versions : {}; + const types = ['peer', 'orderer', 'ca']; + for (let i in types) { + const fab_type = types[i]; + if (tmp && tmp[fab_type]) { + ret.operator.available_fabric_versions[fab_type] = []; + for (let ver in tmp.peer) { + ret.operator.available_fabric_versions[fab_type].push(t.misc.prettyPrintVersion(ver)); + } + } + } join(null, resp.versions); } }); diff --git a/packages/athena/routes/other_apis.js b/packages/athena/routes/other_apis.js index 880187c4..d027e103 100644 --- a/packages/athena/routes/other_apis.js +++ b/packages/athena/routes/other_apis.js @@ -530,7 +530,6 @@ module.exports = function (logger, ev, t) { //----------------------------------------------------------------------------- // Return the debug/support version summary - // dsh todo - doc this api //----------------------------------------------------------------------------- app.get('/api/v[3]/versions', t.middleware.verify_view_action_session, (req, res) => { t.other_apis_lib.version_summary(req, (err, ret) => {