#!/usr/bin/env node /*! * @license * Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import { Command } from 'commander'; import fetch from 'node-fetch'; import * as fs from 'fs'; import { logger } from './logger'; import { AlfrescoApi, AlfrescoApiConfig } from '@alfresco/js-api'; import { argv, exit } from 'node:process'; const program = new Command(); const ACTIVITI_CLOUD_APPS = require('./resources').ACTIVITI_CLOUD_APPS; let alfrescoJsApiModeler: AlfrescoApi; let alfrescoJsApiDevops: AlfrescoApi; let args: ConfigArgs; let isValid = true; export interface ConfigArgs { modelerUsername: string; modelerPassword: string; devopsUsername: string; devopsPassword: string; oauth: string; tokenEndpoint: string; clientId: string; secret: string; scope: string; host: string; tag: string; envs: string[]; } export const AAE_MICROSERVICES = ['deployment-service', 'modeling-service', 'dmn-service']; /** * Perform a health check * * @param nameService service name */ async function healthCheck(nameService: string) { const url = `${args.host}/${nameService}/actuator/health`; const pathParams = {}; const queryParams = {}; const headerParams = {}; const formParams = {}; const bodyParam = {}; const contentTypes = ['application/json']; const accepts = ['application/json']; try { const health = await alfrescoJsApiModeler.oauth2Auth.callCustomApi( url, 'GET', pathParams, queryParams, headerParams, formParams, bodyParam, contentTypes, accepts ); if (health.status !== 'UP') { logger.error(`${nameService} is DOWN `); isValid = false; } else { const reset = '\x1b[0m'; const green = '\x1b[32m'; logger.info(`${green}${nameService} is UP!${reset}`); } } catch (error) { logger.error(`${nameService} is not reachable error: `, error); isValid = false; } } /** * Get deployed application * */ async function getApplications(): Promise<{ list: { entries: any[] } }> { const url = `${args.host}/deployment-service/v1/applications`; const pathParams = {}; const queryParams = {}; const headerParams = {}; const formParams = {}; const bodyParam = {}; const contentTypes = ['application/json']; const accepts = ['application/json']; try { await alfrescoJsApiDevops.login(args.devopsUsername, args.devopsPassword); const result = alfrescoJsApiDevops.oauth2Auth.callCustomApi( url, 'GET', pathParams, queryParams, headerParams, formParams, bodyParam, contentTypes, accepts ); result.on('error', (error) => { logger.error(`Get application by status ${error} `); }); return result; } catch (error) { logger.error(`Get application by status ${error.status} `); isValid = false; return null; } } /** * Get descriptors * * @returns deployment service descriptors */ function getDescriptors() { const url = `${args.host}/deployment-service/v1/descriptors`; const pathParams = {}; const queryParams = {}; const headerParams = {}; const formParams = {}; const bodyParam = {}; const contentTypes = ['application/json']; const accepts = ['application/json']; try { return alfrescoJsApiDevops.oauth2Auth.callCustomApi( url, 'GET', pathParams, queryParams, headerParams, formParams, bodyParam, contentTypes, accepts ); } catch (error) { logger.error(`Get Descriptors ${error.status} `); isValid = false; return null; } } /** * Get projects * * @returns deployment service projects */ function getProjects() { const url = `${args.host}/modeling-service/v1/projects`; const pathParams = {}; const queryParams = { maxItems: 1000 }; const headerParams = {}; const formParams = {}; const bodyParam = {}; const contentTypes = ['application/json']; const accepts = ['application/json']; try { return alfrescoJsApiModeler.oauth2Auth.callCustomApi( url, 'GET', pathParams, queryParams, headerParams, formParams, bodyParam, contentTypes, accepts ); } catch (error) { logger.error('Get Projects' + error.status); isValid = false; return null; } } /** * Get project release * * @param projectId project id * @returns project release */ function getProjectRelease(projectId: string) { const url = `${args.host}/modeling-service/v1/projects/${projectId}/releases`; const pathParams = {}; const queryParams = {}; const headerParams = {}; const formParams = {}; const bodyParam = {}; const contentTypes = ['application/json']; const accepts = ['application/json']; try { return alfrescoJsApiModeler.oauth2Auth.callCustomApi( url, 'GET', pathParams, queryParams, headerParams, formParams, bodyParam, contentTypes, accepts ); } catch (error) { logger.error('Get Projects Release' + error.status); isValid = false; return null; } } /** * Release project * * @param projectId project id */ async function releaseProject(projectId: string) { const url = `${args.host}/modeling-service/v1/projects/${projectId}/releases`; const pathParams = {}; const queryParams = {}; const headerParams = {}; const formParams = {}; const bodyParam = {}; const contentTypes = ['application/json']; const accepts = ['application/json']; try { return alfrescoJsApiModeler.oauth2Auth.callCustomApi( url, 'POST', pathParams, queryParams, headerParams, formParams, bodyParam, contentTypes, accepts ); } catch (error) { await deleteProject(projectId); logger.error('Post Projects Release' + error.status); isValid = false; } } /** * Delete project * * @param projectId project id * @returns response payload */ function deleteProject(projectId: string) { const url = `${args.host}/modeling-service/v1/projects/${projectId}`; const pathParams = {}; const queryParams = {}; const headerParams = {}; const formParams = {}; const bodyParam = {}; const contentTypes = ['application/json']; const accepts = ['application/json']; try { return alfrescoJsApiModeler.oauth2Auth.callCustomApi( url, 'DELETE', pathParams, queryParams, headerParams, formParams, bodyParam, contentTypes, accepts ); } catch (error) { logger.error('Delete project error' + error.status); isValid = false; return null; } } /** * Import and release project * * @param absoluteFilePath path to project file */ async function importAndReleaseProject(absoluteFilePath: string) { const fileContent = fs.createReadStream(absoluteFilePath); try { const project = await alfrescoJsApiModeler.oauth2Auth.callCustomApi( `${args.host}/modeling-service/v1/projects/import`, 'POST', {}, {}, {}, { file: fileContent }, {}, ['multipart/form-data'], ['application/json'] ); logger.info(`Project imported`); logger.info(`Create release`); return await alfrescoJsApiModeler.oauth2Auth.callCustomApi( `${args.host}/modeling-service/v1/projects/${project.entry.id}/releases`, 'POST', {}, {}, {}, {}, {}, ['application/json'], ['application/json'] ); } catch (error) { logger.error(`Not able to import the project/create the release ${absoluteFilePath} with status: ${error}`); isValid = false; throw error; } } /** * Deletes descriptor * * @param name descriptor name * @returns response payload */ function deleteDescriptor(name: string) { const url = `${args.host}/deployment-service/v1/descriptors/${name}`; const pathParams = {}; const queryParams = {}; const headerParams = {}; const formParams = {}; const bodyParam = {}; const contentTypes = ['application/json']; const accepts = ['application/json']; try { return alfrescoJsApiDevops.oauth2Auth.callCustomApi( url, 'DELETE', pathParams, queryParams, headerParams, formParams, bodyParam, contentTypes, accepts ); } catch (error) { logger.error('Delete descriptor' + error.status); isValid = false; return null; } } /** * Deploys a model * * @param model model object * @returns response payload */ function deploy(model: any) { const url = `${args.host}/deployment-service/v1/applications`; const pathParams = {}; const queryParams = {}; const headerParams = {}; const formParams = {}; const bodyParam = model; const contentTypes = ['application/json']; const accepts = ['application/json']; try { return alfrescoJsApiDevops.oauth2Auth.callCustomApi( url, 'POST', pathParams, queryParams, headerParams, formParams, bodyParam, contentTypes, accepts ); } catch (error) { logger.error('Deploy post' + error.status); isValid = false; return null; } } /** * Initialise default endpoint token * * @param options token options * @returns options */ function initializeDefaultToken(options: ConfigArgs): any { options.tokenEndpoint = options.tokenEndpoint.replace('${clientId}', options.clientId); return options; } /** * Get a new Alfresco Api instance * * @param configArgs command params * @returns a new instance of Alfresco Api client */ function getAlfrescoJsApiInstance(configArgs: ConfigArgs): AlfrescoApi { let ssoHost = configArgs.oauth; ssoHost = ssoHost ?? configArgs.host; const config: AlfrescoApiConfig = { provider: 'BPM', hostBpm: `${configArgs.host}`, authType: 'OAUTH', contextRoot: 'alfresco', oauth2: { host: `${ssoHost}`, tokenUrl: `${ssoHost}/${configArgs.tokenEndpoint}`, clientId: `${configArgs.clientId}`, scope: `${configArgs.scope}`, secret: `${configArgs.secret}`, implicitFlow: false, silentLogin: false, redirectUri: '/' } }; return new AlfrescoApi(config); } /** * Deploy missing applications * * @param tag tag * @param envs environments */ async function deployMissingApps(tag?: string, envs?: string[]) { const deployedApps = await getApplications(); const failingApps = findFailingApps(deployedApps.list.entries); const missingApps = findMissingApps(deployedApps.list.entries); if (failingApps.length > 0) { failingApps.forEach((app) => { const reset = '\x1b[0m'; const bright = '\x1b[1m'; const red = '\x1b[31m'; logger.error(`${red}${bright}ERROR: App ${app.entry.name} down or inaccessible ${reset}${red} with status ${app.entry.status}${reset}`); }); exit(1); } else if (missingApps.length > 0) { logger.warn(`Missing apps: ${JSON.stringify(missingApps)}`); await checkIfAppIsReleased(missingApps, tag, envs); } else { const reset = '\x1b[0m'; const green = '\x1b[32m'; logger.info(`${green}All the apps are correctly deployed${reset}`); } } /** * Check if application is released * * @param missingApps applications * @param tag tag * @param envs environments */ async function checkIfAppIsReleased(missingApps: any[], tag?: string, envs?: string[]) { const projectList = await getProjects(); let TIME = 5000; let noError = true; for (let i = 0; i < missingApps.length; i++) { noError = true; const currentAbsentApp = missingApps[i]; const project = projectList.list.entries.find((currentApp: any) => currentAbsentApp.name === currentApp.entry.name); let projectRelease: any; if (project === undefined) { logger.warn('Missing project: Create the project for ' + currentAbsentApp.name); try { projectRelease = await importProjectAndRelease(currentAbsentApp, tag); } catch (error) { logger.info(`error status ${error.status}`); if (error.status !== 409) { logger.info(`Not possible to upload the project ${currentAbsentApp.name} status : ${JSON.stringify(error)}`); exit(1); } else { logger.error(`Not possible to upload the project because inconsistency CS - Modelling try to delete manually the node`); exit(1); } } } else { TIME += 5000; logger.info('Project ' + project.entry.name + ' found'); const projectReleaseList = await getProjectRelease(project.entry.id); if (projectReleaseList.list.entries.length === 0) { logger.warn('Project needs release'); projectRelease = await releaseProject(project); logger.warn(`Project released: ${projectRelease.id}`); } else { logger.info('Project already has release'); // getting the latest project release let currentReleaseVersion = -1; projectReleaseList.list.entries.forEach((currentRelease: any) => { if (currentRelease.entry.version > currentReleaseVersion) { currentReleaseVersion = currentRelease.entry.version; projectRelease = currentRelease; } }); } } if (noError) { await checkDescriptorExist(currentAbsentApp.name); await sleep(TIME); if (envs && envs.length > 0) { for (const envId of envs) { await deployWithPayload(currentAbsentApp, projectRelease, envId); } } else { await deployWithPayload(currentAbsentApp, projectRelease); } } } } /** * Deploy with a payload * * @param currentAbsentApp current application * @param projectRelease project release * @param envId environment id */ async function deployWithPayload(currentAbsentApp: any, projectRelease: any, envId?: string): Promise { const deployPayload = { name: currentAbsentApp.name, releaseId: projectRelease.entry.id, security: currentAbsentApp.security, infrastructure: currentAbsentApp.infrastructure, variables: currentAbsentApp.variables, enableLocalDevelopment: currentAbsentApp.enableLocalDevelopment, environmentId: envId }; logger.info(`Deploying ${currentAbsentApp.name} ${envId ? 'on env: ' + envId : ''}`); await deploy(deployPayload); logger.info(`Deployed ${currentAbsentApp.name} ${envId ? 'on env: ' + envId : ''}`); } /** * Check if descriptor exists * * @param name descriptor name */ async function checkDescriptorExist(name: string): Promise { logger.info(`Check descriptor ${name} exist in the list `); const descriptorList = await getDescriptors(); if (descriptorList?.list && descriptorList.entries) { for (const descriptor of descriptorList.list.entries) { if (descriptor.entry.name === name) { if (descriptor.entry.deployed === false) { await deleteDescriptor(descriptor.entry.name); } } } } return false; } /** * Import and release project * * @param app application * @param tag tag */ async function importProjectAndRelease(app: any, tag?: string) { const appLocationReplaced = app.file_location(tag); logger.warn('App fileLocation ' + appLocationReplaced); await getFileFromRemote(appLocationReplaced, app.name); logger.warn('Project imported ' + app.name); const projectRelease = await importAndReleaseProject(`${app.name}.zip`); await deleteLocalFile(`${app.name}`); return projectRelease; } /** * Find missing applications * * @param deployedApps applications * @returns list of missing apps */ function findMissingApps(deployedApps: any[]): any[] { const result = []; Object.keys(ACTIVITI_CLOUD_APPS).forEach((key) => { const isPresent = deployedApps.find((currentApp: any) => ACTIVITI_CLOUD_APPS[key].name === currentApp.entry.name); if (!isPresent) { result.push(ACTIVITI_CLOUD_APPS[key]); } }); return result; } /** * Find failing applications * * @param deployedApps applications * @returns list of failing apps */ function findFailingApps(deployedApps: any[]): any[] { const result = []; Object.keys(ACTIVITI_CLOUD_APPS).forEach((key) => { const failingApp = deployedApps.filter( (currentApp: any) => ACTIVITI_CLOUD_APPS[key].name === currentApp.entry.name && 'Deployed' !== currentApp.entry.status ); if (failingApp?.length > 0) { result.push(...failingApp); } }); return result; } /** * Get file from the remote * * @param url url to file * @param name name */ async function getFileFromRemote(url: string, name: string): Promise { return fetch(url) .then((response) => { if (!response.ok) { throw new Error(`HTTP error! Status: ${response.status}`); } return response; }) .then( (response) => new Promise((resolve, reject) => { const outputFile = fs.createWriteStream(`${name}.zip`); response.body.pipe(outputFile); outputFile.on('finish', () => { logger.info(`The file is finished downloading.`); resolve(); }); outputFile.on('error', (error) => { logger.error(`Not possible to download the project form remote`); reject(error); }); }) ) .catch((error) => { logger.error(`Failed to fetch file from remote: ${error.message}`); throw error; }); } /** * Deletes local file * * @param name file name */ async function deleteLocalFile(name: string) { logger.info(`Deleting local file ${name}.zip`); fs.unlinkSync(`${name}.zip`); } /** * Perform a timeout * * @param time delay in milliseconds */ async function sleep(time: number) { logger.info(`Waiting for ${time} sec...`); await new Promise((done) => setTimeout(done, time)); logger.info(`Done...`); return; } /** * Init AAE environment command */ export default async function main() { program .version('0.1.0') .description( 'The following command is in charge of Initializing the activiti cloud env with the default apps' + 'adf-cli init-aae-env --host "gateway_env" --modelerUsername "modelerusername" --modelerPassword "modelerpassword" --devopsUsername "devevopsusername" --devopsPassword "devopspassword"' ) .option('-h, --host [type]', 'Host gateway') .option('--oauth [type]', 'SSO host') .option('--clientId [type]', 'sso client') .option('--secret [type]', 'sso secret', '') .option('--scope [type]', 'sso scope', 'openid') .option('--tokenEndpoint [type]', 'discovery token Endpoint', 'auth/realms/${clientId}/protocol/openid-connect/token') .option('--modelerUsername [type]', 'username of a user with role ACTIVIT_MODELER') .option('--modelerPassword [type]', 'modeler password') .option('--devopsUsername [type]', 'username of a user with role ACTIVIT_DEVOPS') .option('--devopsPassword [type]', 'devops password') .option('--tag [type]', 'tag name of the codebase') .option('--envs [type...]', 'environment ids of the envs where to deploy the app') .parse(argv); if (argv.includes('-h') || argv.includes('--help')) { program.outputHelp(); return; } const options = initializeDefaultToken(program.opts() as ConfigArgs); args = { host: options.host, clientId: options.clientId, devopsUsername: options.devopsUsername, devopsPassword: options.devopsPassword, modelerUsername: options.modelerUsername, modelerPassword: options.modelerPassword, oauth: options.oauth, tokenEndpoint: options.tokenEndpoint, scope: options.scope, secret: options.secret, tag: options.tag, envs: options.envs }; alfrescoJsApiModeler = getAlfrescoJsApiInstance(args); AAE_MICROSERVICES.map(async (serviceName) => { await healthCheck(serviceName); }); await alfrescoJsApiModeler.login(args.modelerUsername, args.modelerPassword).then( () => { const reset = '\x1b[0m'; const green = '\x1b[32m'; logger.info(`${green}login SSO ok${reset}`); }, (error) => { logger.error(`login SSO error ${JSON.stringify(error)} ${args.modelerUsername}`); exit(1); } ); if (isValid) { const reset = '\x1b[0m'; const green = '\x1b[32m'; logger.info(`${green}The environment is up and running ${reset}`); alfrescoJsApiDevops = getAlfrescoJsApiInstance(args); await alfrescoJsApiDevops.login(args.devopsUsername, args.devopsPassword).then( () => { logger.info('login SSO ok devopsUsername'); }, (error) => { logger.error(`login SSO error ${JSON.stringify(error)} ${args.devopsUsername}`); exit(1); } ); await deployMissingApps(args.tag, args.envs); } else { logger.error('The environment is not up'); exit(1); } }