Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add gather version button #560

Merged
merged 3 commits into from
Nov 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion packages/apollo/src/assets/i18n/en/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
58 changes: 58 additions & 0 deletions packages/apollo/src/components/Settings/Settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -671,6 +672,62 @@ export class Settings extends Component {
);
}

// show section on version gathering (debug)
renderVersionDebug(translate) {
return (
<div className="ibp-settings-bulk-data-container">
<div className="settings-section">
<h3 className="settings-label">
<BlockchainTooltip direction="right"
triggerText={translate('version_debug_msg')}
>
{translate('version_debug_tooltip')}
</BlockchainTooltip>
</h3>
<div className="settings-button-container">
<Button
id="version_export_button"
disabled={this.props.saving}
onClick={async () => {
try {
const resp = await NodeRestApi.getVersionSummary();
this.downloadVersion(resp);
} catch (e) {
Log.error('error generating version summary', e);
}
}}
>
{translate('generate')}
</Button>
</div>
</div>
</div>
);
}

// 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;
Expand Down Expand Up @@ -707,6 +764,7 @@ export class Settings extends Component {
</div>
</div>)}
</div>
{this.renderVersionDebug(translate)}
{this.renderDataManagement(translate)}
{window && window.location && window.location.href && window.location.href.includes('debug') && this.renderDeleteSection(translate)}
</div>
Expand Down
4 changes: 2 additions & 2 deletions packages/apollo/src/components/Settings/_settings.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
5 changes: 5 additions & 0 deletions packages/apollo/src/rest/NodeRestApi.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
1 change: 1 addition & 0 deletions packages/apollo/src/utils/helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,7 @@ const Helper = {
createTarget.removeChild(link);
}
},

/*
* Export a nodes as Zip
*/
Expand Down
89 changes: 89 additions & 0 deletions packages/athena/libs/component_lib.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
//--------------------------------------------------
Expand Down
20 changes: 20 additions & 0 deletions packages/athena/libs/misc.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};
141 changes: 141 additions & 0 deletions packages/athena/libs/other_apis_lib.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};
Loading
Loading