UNPKG

@c8y/toolkit

Version:

Toolkit for Cumulocity applications, allows to e.g. deploy an application to an instance of Cumulocity.

409 lines 17.4 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.deployZipFile = deployZipFile; exports.getAppAsZip = getAppAsZip; exports.deploy = deploy; const client_1 = require("@c8y/client"); const constants_1 = require("../constants"); const chalk_1 = __importDefault(require("chalk")); const semver_1 = require("semver"); const fs_1 = require("fs"); const promises_1 = require("fs/promises"); const helpers_1 = require("./helpers"); const unzip_stream_1 = require("unzip-stream"); const path_1 = require("path"); const utils_1 = require("../utils"); async function deployZipFile(zipPath, options) { const details = await getAppAsZip(zipPath); const { user, password, url, tenant, tags, rollingTags, addWebSdkStableTag, deleteExistingTags, availability } = options; let client; try { client = await (0, utils_1.createClient)({ url, user, password, tenant }); } catch (e) { console.error(e.message); throw e; } try { await (0, utils_1.ensureUserHasRequiredPermissions)(client); } catch (e) { console.error(e.message); throw e; } console.log('Deploying application...'); const actualTags = tags ? tags.split(',') : []; const { buffer, json, fileName, size } = details; await deploy({ buffer, json, size, client, tags: actualTags, fileName, rollingTags, addWebSdkStableTag, deleteExistingTags, availability }); } /** * Reads zipped file by provided path, extracts application metadata, file metadata and file buffer (raw data). * * @param path - The path to the zip file. * @returns A Promise that resolves to object representing the application. */ async function getAppAsZip(path) { const stats = await (0, promises_1.stat)(path); const size = stats.size; const json = await new Promise((resolve, reject) => { const partialCumulocityJSONData = new Array(); (0, fs_1.createReadStream)(path) .pipe((0, unzip_stream_1.Parse)()) .on('entry', (entry) => { const fileName = entry.path; const isCumulocity = fileName === constants_1.OPTIONS_CONSTANTS['PATH.BUILT_MANIFEST_FILE']; const macTrash = !!fileName.match(/__MACOSX/); const readFile = isCumulocity && !macTrash; if (readFile) { entry.on('data', (data) => partialCumulocityJSONData.push(data)); } else { entry.autodrain(); } }) .on('close', () => { if (partialCumulocityJSONData.length === 0) { return reject('No manifest file found in zip file'); } try { let partialCumulocityJSONString = ''; partialCumulocityJSONData.forEach(data => { partialCumulocityJSONString += data.toString(); }); const fileData = JSON.parse(partialCumulocityJSONString); const appDataFromFile = (0, helpers_1.dataToAppData)(fileData); const name = (0, helpers_1.getName)(appDataFromFile, (0, path_1.basename)(path)); const key = (0, helpers_1.getKey)(appDataFromFile, name); const contextPath = (0, helpers_1.getContextPath)(appDataFromFile, name); Object.assign(appDataFromFile, { name, key, contextPath }); resolve(appDataFromFile); } catch (e) { reject(e); } }) .on('error', e => reject(e)); }); json.type ??= client_1.ApplicationType.HOSTED; return { fileName: (0, path_1.basename)(path), json, buffer: (0, fs_1.createReadStream)(path), size }; } /** * Uploads an application to server, make it active and subscribes tenant to this app. * * @param appToDeploy - The application to deploy. * @returns A Promise that resolves when the deployment is complete. */ async function deploy(deployOptions) { const { json, buffer, fileName, size } = deployOptions; let application = await getApplicationOrCreateIfNotExists(deployOptions, json); // removes old version(s) if limit of existing versions is reached if (await reduceApplicationVersionsIfRequired(deployOptions, application)) { application = await getApplicationOrCreateIfNotExists(deployOptions, json); } if (application.isPackage && deployOptions.addWebSdkStableTag && json.webSdkVersion) { const majorVersion = json.webSdkVersion.split('.')[0]; const isActuallyANumberRegexp = /^\d+$/; if (isActuallyANumberRegexp.test(majorVersion)) { const tagToAdd = `${majorVersion}-stable`; if (deployOptions.tags) { deployOptions.tags.push(tagToAdd); } else { deployOptions.tags = [tagToAdd]; } } else { console.warn(`WebSDK version is not in the correct format. Expected format is <major>.<minor>.<patch>.`); } } const uploadedFile = await uploadApplication(deployOptions, application, buffer, fileName, size); // roll tags around after upload of version to ensure time where a certain tag does not exists is minimized if (application.isPackage) { await rollingTags(deployOptions, application); } const isFirstVersion = !application.applicationVersions || application.applicationVersions.length === 0; const hasLatestTag = deployOptions.tags && deployOptions.tags.indexOf('latest') >= 0; // packages will be only activated if tag is set to latest // or if this is the first version of the package if (application.isPackage && (isFirstVersion || hasLatestTag)) { await deployOptions.client.application.update({ id: application.id, activeVersionId: uploadedFile.binaryId }); } if (!application.isPackage) { await activateBinary(deployOptions, uploadedFile, application); } const availability = deployOptions.availability || json.availability; if (json.type === 'HOSTED' && availability !== application.availability) { if (availability === client_1.ApplicationAvailability.MARKET) { await subscribeTenantToApp(deployOptions, application); console.info(chalk_1.default.blue('Subscribing app to current tenant.')); } else if (availability === client_1.ApplicationAvailability.PRIVATE || availability === client_1.ApplicationAvailability.SHARED) { deployOptions.client.application.update({ id: application.id, availability: availability || client_1.ApplicationAvailability.PRIVATE }); console.info(chalk_1.default.blue(`Setting availability to "${availability}".`)); } } const msg = constants_1.OPTIONS_CONSTANTS['TXT.APP_DEPLOYED'](application.contextPath); console.info(chalk_1.default.green.bold(msg)); } /** * Removes tags from other versions of app and re-adds them to the new version */ async function rollingTags(deployOptions, application) { if (!deployOptions.tags.length || !deployOptions.rollingTags) { return; } const tags = deployOptions.tags; const tagsToRemove = tags.filter(tag => tag !== 'latest'); for (const appVersion of application.applicationVersions || []) { const tagsOfVersion = appVersion.tags || []; const tagsToKeepForVersion = tagsOfVersion.filter(tag => !tagsToRemove.includes(tag)); const tagsToBeRemovedForVersion = tagsOfVersion.filter(tag => tagsToRemove.includes(tag)); if (tagsToKeepForVersion.length === tagsOfVersion.length) { continue; } console.info(chalk_1.default.yellow(`Removing tags ${tagsToBeRemovedForVersion} from version ${appVersion.version} of application ${application.contextPath}`)); await deployOptions.client.application.setPackageVersionTag(application, appVersion.version, tagsToKeepForVersion); } const versionToBeTagged = application.version; await deployOptions.client.application.setPackageVersionTag(application, versionToBeTagged, tags); } /** * Picks oldest version of application * First tries to find oldest version without a tag * If no version without tag is found, it tries to find oldest version without latest tag */ function identifyApplicationVersionToBeRemoved(application) { const appVersions = application.applicationVersions; if (!appVersions || appVersions.length === 0) { throw Error('No application versions found'); } const versionsWithoutTag = appVersions.filter(version => !version.tags || version.tags.length === 0); if (versionsWithoutTag.length > 0) { try { const sortedVersions = (0, semver_1.sort)(versionsWithoutTag.map(version => version.version)); return sortedVersions[0]; } catch (e) { console.warn('Failed to sort versions without tags', e); } return versionsWithoutTag[0].version; } const versionsWithoutLatestTag = appVersions.filter(version => !version.tags || !version.tags.includes('latest')); if (versionsWithoutLatestTag.length > 0) { try { const sortedVersions = (0, semver_1.sort)(versionsWithoutLatestTag.map(version => version.version)); return sortedVersions[0]; } catch (e) { console.warn('Failed to sort versions without tags', e); } return versionsWithoutLatestTag[0].version; } throw Error('No application version to be removed found'); } /** * Removes old application versions if the limit of versions is reached. * Returns Promise<false> if number of app versions is lower than max versions (20) and app needs no version reduction. * Returns Promise<true> if number of app versions equals or is higher than max versions (20) and app versions have been reduced. */ async function reduceApplicationVersionsIfRequired(deployOptions, application) { const maxVersions = 20; const numberOfAppVersions = application.applicationVersions?.length || 0; if (numberOfAppVersions < maxVersions) { return false; } const appVersionToRemove = identifyApplicationVersionToBeRemoved(application); console.info(chalk_1.default.yellow(`Application ${application.contextPath} has reached the maximum ${maxVersions} versions (${numberOfAppVersions}). Removing version "${appVersionToRemove}"`)); try { await deployOptions.client.application.deleteVersionPackage(application, { version: appVersionToRemove }); } catch (e) { console.error(`Failed to remove version "${appVersionToRemove}"`, e); throw e; } const { data: refreshedApp } = await deployOptions.client.application.detail(application); application = refreshedApp; // do this recursively until we have less than 20 versions await reduceApplicationVersionsIfRequired(deployOptions, application); return true; } /** * Retrieves an existing application from server or creates a new one if it doesn't exist. * * @param application - The application to retrieve or create. * @returns A Promise that resolves to the retrieved or created application. */ async function getApplicationOrCreateIfNotExists(deployOptions, application) { const remoteApplication = await getApplication(deployOptions, application); return !remoteApplication ? await createApplication(deployOptions, application) : Object.assign(remoteApplication, application); } /** * Uploads an application binary to the server. * * @param application - The application for which the binary is being uploaded. * @param buffer - The binary data to upload. * @param filePath - The file path for the binary (optional). * @param size - The size of the binary (optional). * @returns A Promise that resolves to the uploaded binary data. */ async function uploadApplication(deployOptions, application, buffer, filePath, size) { if (!filePath) { filePath = `${application.contextPath}.zip`; } try { const msg = constants_1.OPTIONS_CONSTANTS['TXT.APP_UPLOADING'](buffer.length || size); const { tags, deleteExistingTags } = deployOptions; if (tags && !application.isPackage) { console.info(chalk_1.default.yellow(constants_1.OPTIONS_CONSTANTS['TXT.NOT_PACKAGE_WARNING'])); } console.info(chalk_1.default.green(msg)); if (tags && deleteExistingTags) { console.info(chalk_1.default.yellow(constants_1.OPTIONS_CONSTANTS['TXT.DELETE_EXISTING_TAG_WARNING'])); const appVersions = application.applicationVersions?.filter(version => version.tags && version.tags.some(tag => tags.indexOf(tag) > -1)); if (appVersions && appVersions?.length > 0) { for (const appVersion of appVersions) { await deployOptions.client.application.deleteVersionPackage(application, appVersion); } } } const uploadOverrides = getPackageUploadOverrides(deployOptions, application); const { data } = await deployOptions.client.application .binary(application) .upload(buffer, filePath, uploadOverrides); return data; } catch (err) { console.debug(err); console.error(`${constants_1.OPTIONS_CONSTANTS['TXT.APP_UPLOAD_ERROR']} ${err.res.status}`); console.error(JSON.stringify(err.data, null, 2)); process.exit(9); } } /** * Activates a binary for an application. * * @param uploadedFile - The uploaded binary file. * @param application - The application to activate the binary for. */ async function activateBinary(deployOptions, uploadedFile, application) { application.activeVersionId = `${uploadedFile.id}`; delete application.type; try { console.info(chalk_1.default.green(constants_1.OPTIONS_CONSTANTS['TXT.APP_ACTIVATING'](uploadedFile.id))); await deployOptions.client.application.update(application); } catch (err) { console.debug(err); const msg = constants_1.OPTIONS_CONSTANTS['TXT.APP_ACTIVATING_FAILED'](application.contextPath); console.error(`${msg} ${err.res.status}`); console.error(JSON.stringify(err.data, null, 2)); process.exit(9); } } /** * Subscribes a tenant to an application. * * @param app - The application to subscribe to. */ async function subscribeTenantToApp(deployOptions, app) { await deployOptions.client.tenant.subscribeApplication({ name: deployOptions.client.core.tenant }, app); } /** * Retrieves an application based on the provided application object. * * @param application - The application to retrieve. * @returns A Promise that resolves to the retrieved application or null if not found. */ async function getApplication(deployOptions, application) { const { tenant } = deployOptions.client.core; try { console.info(chalk_1.default.green(constants_1.OPTIONS_CONSTANTS['TXT.APP_FETCHING'])); const { data: applications } = await deployOptions.client.application.listByOwner(tenant, { pageSize: 1000 }); return applications.find(v => v.owner?.tenant.id === tenant && v.contextPath === application.contextPath); } catch (err) { console.debug(err); console.error(`${constants_1.OPTIONS_CONSTANTS['TXT.APP_FETCHING_FAILED']} ${err.res.status}`); console.error(JSON.stringify(err.data, null, 2)); process.exit(9); } } /** * Creates a new application on the server. * * @param application - The application to create. * @returns A Promise that resolves to the created application. */ async function createApplication(deployOptions, application) { try { console.info(chalk_1.default.green(constants_1.OPTIONS_CONSTANTS['TXT.APP_CREATING'])); const { data } = await deployOptions.client.application.create(application); return data; } catch (err) { console.debug(err); console.error(`${constants_1.OPTIONS_CONSTANTS['TXT.APP_CREATE_FAILED']} ${err.res.status}`); console.error(JSON.stringify(err.data, null, 2)); process.exit(9); } } /** * Generates upload overrides for package uploads. * * @param app - The application for which overrides are generated. * @returns Upload overrides or undefined if the application is not a package. */ function getPackageUploadOverrides(deployOptions, app) { if (!app.isPackage) { return; } let tags = deployOptions.tags; // in case of rollingTags, we will add the tags later if (deployOptions.rollingTags) { tags = []; } const version = app.version; return { listUrl: 'versions', headers: { Accept: 'application/vnd.com.nsn.cumulocity.applicationVersion+json;charset=UTF-8;ver=0.9' }, bodyFileProperty: 'applicationBinary', requestBody: { applicationVersion: Object.assign({ version, tags }) } }; } //# sourceMappingURL=deploy.js.map