UNPKG

apim-developer-portal1

Version:

API management developer portal

289 lines (263 loc) 11.2 kB
/** * Important: this script is no longer maintained. It can automate deployments of self-hosted portals with explicitly-defined Storage Accounts only. Refer to documentation for more details. New scripts are located in the scripts.v2 folder. * * This script automates deployments between developer portal instances. * In order to run it, you need to: * * 1) Clone the api-management-developer-portal repository * 2) `npm install` in the root of the project * 3) Install az-cli (https://docs.microsoft.com/en-us/cli/azure/install-azure-cli?view=azure-cli-latest) * 4) Run this script with a valid combination of arguments * * Managed portal command example: * node migrate --sourceEndpoint from.management.azure-api.net --destEndpoint to.management.azure-api.net --publishEndpoint to.developer.azure-api.net --sourceToken "SharedAccessSignature integration&2020..." --destToken "SharedAccessSignature integration&2020..." * * If you run with the --selfHosted flag, you are expected to supply a sourceStorage and destStorage parameters. * Auto-publishing is not supported for self-hosted versions, so make sure you publish the portal (for example, locally) and upload the generated static files to your hosting after the migration is completed. * * You can specify the SAS tokens directly (via sourceToken and destToken), or you can supply an identifier and key, * and the script will generate tokens that expire in 1 hour. (via sourceId, sourceKey, destId, destKey) */ const https = require('https'); const moment = require('moment'); const crypto = require('crypto'); const mkdirSync = require('fs').mkdirSync; const execSync = require('child_process').execSync; const yargs = require('yargs') .example('$0 \ --publishEndpoint <name.developer.azure-api.net> \ --sourceEndpoint <name.management.azure-api.net> \ --sourceToken <token> \ --destEndpoint <name.management.azure-api.net> \ --destToken <token>\n', 'Managed') .example('$0 --selfHosted \ --sourceEndpoint <name.management.azure-api.net> \ --sourceToken <token> \ --sourceStorage <connectionString> \ --destEndpoint <name.management.azure-api.net> \ --destToken <token> \ --destStorage <connectionString>', 'Self-Hosted') /*.option('interactive', { alias: 'i', type: 'boolean', description: 'Whether to use interactive login', conflicts: ['sourceToken', 'sourceId', 'sourceKey', 'destToken', 'destId', 'destKey'] })*/ .option('selfHosted', { alias: 'h', type: 'boolean', description: 'If the portal is self-hosted', implies: ['sourceStorage', 'destStorage'] }) .option('publishEndpoint', { alias: 'p', type: 'string', description: 'Endpoint of the destination managed developer portal; if empty, destination portal will not be published; unsupported in self-hosted scenario', example: '<name.developer.azure-api.net>' }) .option('sourceEndpoint', { type: 'string', description: 'The hostname of the management endpoint of the source API Management service', example: '<name.management.azure-api.net>', demandOption: true }) .option('sourceId', { type: 'string', description: 'The management API identifier', implies: 'sourceKey', conflicts: 'sourceToken' }) .option('sourceKey', { type: 'string', description: 'The management API key (primary or secondary)', implies: 'sourceId', conflicts: 'sourceToken' }) .option('sourceToken', { type: 'string', description: 'A SAS token for the source portal', example: 'SharedAccessSignature…', conflicts: ['sourceId, sourceToken'] }) .option('sourceStorage', { type: 'string', description: 'The connection string for self-hosted portals', example: 'DefaultEndpointsProtocol=…', implies: 'selfHosted' }) .option('destEndpoint', { type: 'string', description: 'The hostname of the management endpoint of the destination API Management service', example: '<name.management.azure-api.net>', demandOption: true }) .option('destId', { type: 'string', description: 'The management API identifier', implies: 'destKey', conflicts: 'destToken' }) .option('destKey', { type: 'string', description: 'The management API key (primary or secondary)', implies: 'destId', conflicts: 'destToken' }) .option('destToken', { type: 'string', example: 'SharedAccessSignature…', description: 'A SAS token for the destination portal', conflicts: ['destId, destToken'] }) .option('destStorage', { type: 'string', description: 'The connection string for self-hosted portals', example: 'DefaultEndpointsProtocol=…', implies: 'selfHosted' }) .argv; async function run() { const sourceEndpoint = yargs.sourceEndpoint; const sourceToken = await getTokenOrThrow(yargs.sourceToken, yargs.sourceId, yargs.sourceKey); const sourceStorage = await getStorageConnectionOrThrow(yargs.sourceStorage, sourceEndpoint, sourceToken); const destEndpoint = yargs.destEndpoint; const destToken = await getTokenOrThrow(yargs.destToken, yargs.destId, yargs.destKey); const destStorage = await getStorageConnectionOrThrow(yargs.destStorage, destEndpoint, destToken); const publishEndpoint = yargs.publishEndpoint; // the rest of this mirrors migrate.bat, but since we're JS, we're platform-agnostic. const dataFile = '../dist/data.json'; const mediaFolder = '../dist/content'; const mediaContainer = 'content'; // capture the content of the source portal (excl. media) execSync(`node ./capture ${sourceEndpoint} "${sourceToken}" ${dataFile}`); // remove all content of the target portal (incl. media) execSync(`node ./cleanup ${destEndpoint} "${destToken}" "${destStorage}"`); // upload the content of the source portal (excl. media) execSync(`node ./generate ${destEndpoint} "${destToken}" ${dataFile}`); // download media files from the source portal mkdirSync(mediaFolder, { recursive: true }); execSync(`az storage blob download-batch --source ${mediaContainer} --destination ${mediaFolder} --connection-string "${sourceStorage}"`); // upload media files to the target portal execSync(`az storage blob upload-batch --source ${mediaFolder} --destination ${mediaContainer} --connection-string "${destStorage}"`); if (publishEndpoint && !yargs.selfHosted) { process.env.NODE_TLS_REJECT_UNAUTHORIZED = 0; publish(publishEndpoint, destToken); } else if (publishEndpoint) { console.warn("Auto-publishing self-hosted portal is not supported."); } } /** * A wrapper for making a request and returning its response body. * @param {Object} options https options */ async function request(url, options) { return new Promise((resolve, reject) => { const req = https.request(url, options, (resp) => { let data = ''; resp.on('data', (chunk) => { data += chunk; }); resp.on('end', () => { try { resolve(data); } catch (e) { reject(e); } }); }); req.on('error', (e) => { reject(e); }); req.end(); }); } /** * Attempts to get a SAS token in two ways: * 1) if the token is explicitly set by the user, use that token. * 2) if the id and key are specified, manually generate a SAS token. * @param {string} token an optionally specified token * @param {string} id the Management API identifier * @param {string} key the Management API key */ async function getTokenOrThrow(token, id, key) { if (token) { return token; } if (id && key) { return await generateSASToken(id, key); } throw Error('You need to specify either: token or id AND key'); } /** * Generates a SAS token from the specified Management API id and key. Optionally * specify the expiry time, in seconds. * * See https://docs.microsoft.com/en-us/rest/api/apimanagement/apimanagementrest/azure-api-management-rest-api-authentication#ManuallyCreateToken * @param {string} id The Management API identifier. * @param {string} key The Management API key (primary or secondary) * @param {number} expiresIn The number of seconds in which the token should expire. */ async function generateSASToken(id, key, expiresIn = 3600) { const now = moment.utc(moment()); const expiry = now.clone().add(expiresIn, 'seconds'); const expiryString = expiry.format(`YYYY-MM-DD[T]HH:mm:ss.SSSSSSS[Z]`); const dataToSign = `${id}\n${expiryString}`; const signedData = crypto.createHmac('sha512', key).update(dataToSign).digest('base64'); return `SharedAccessSignature uid=${id}&ex=${expiryString}&sn=${signedData}`; } /** * Attempts to get a develoer portal storage connection string in two ways: * 1) if the connection string is explicitly set by the user, use it. * 2) retrieving the connection string from the management API using the instance endpoint and SAS token * @param {string} storage an optionally specified storage connection string * @param {string} endpoint the management endpoint of service instance * @param {string} token the SAS token */ async function getStorageConnectionOrThrow(storage, endpoint, token) { if (storage) { return storage; } if (token) { // token should always be available, because we call // getTokenOrThrow before this return await getStorageConnection(endpoint, token); } throw Error('Storage connection could not be retrieved'); } /** * Gets a storage connection string from the management API for the specified APIM instance and * SAS token. * @param {string} endpoint the management endpoint of service instance * @param {string} token the SAS token */ async function getStorageConnection(endpoint, token) { const options = { port: 443, method: 'GET', headers: { 'Authorization': token } }; const raw = await request(`https://${endpoint}/tenant/settings?api-version=2018-01-01`, options); const body = JSON.parse(raw); return body.settings.PortalStorageConnectionString; } /** * Publishes the content of the specified APIM instance using a SAS token. * @param {string} endpoint the publishing endpoint of the destination developer portal instance * @param {string} token the SAS token */ async function publish(endpoint, token) { const options = { port: 443, method: 'POST', headers: { 'Authorization': token } }; const url = `https://${endpoint}/publish`; // returns with literal OK (missing quotes), which is invalid json. await request(url, options); } run();