diff --git a/packages/apollo/src/assets/i18n/en/messages.json b/packages/apollo/src/assets/i18n/en/messages.json
index 73d7ff1d..269a984a 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 60ef842c..c64ca0b1 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..bd629f9a 100644
--- a/packages/athena/libs/other_apis_lib.js
+++ b/packages/athena/libs/other_apis_lib.js
@@ -899,5 +899,146 @@ 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: '',
+ go_version: '',
+ },
+ operator: {
+ 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) : '-';
+ ret.cluster.go_version = (resp && resp.goVersion) ? resp.goVersion : '-';
+ 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 {
+ 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);
+ }
+ });
+ }
+
+ ], (_, 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..d027e103 100644
--- a/packages/athena/routes/other_apis.js
+++ b/packages/athena/routes/other_apis.js
@@ -528,5 +528,18 @@ module.exports = function (logger, ev, t) {
});
});
+ //-----------------------------------------------------------------------------
+ // Return the debug/support version summary
+ //-----------------------------------------------------------------------------
+ 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;
};