diff --git a/Control/lib/api.js b/Control/lib/api.js index ab44267d9..b572b21bf 100644 --- a/Control/lib/api.js +++ b/Control/lib/api.js @@ -103,7 +103,7 @@ module.exports.setup = (http, ws) => { const wsService = new WebSocketService(ws); const broadcastService = new BroadcastService(ws); const cacheService = new CacheService(broadcastService); - const environmentCacheService = new EnvironmentCacheService(broadcastService, eventEmitter); + const environmentCacheService = new EnvironmentCacheService(broadcastService, eventEmitter, cacheService); const qcConfigurationService = new QCConfigurationService(consulService); const qcConfigurationController = new QCConfigurationController(qcConfigurationService, config.consul); @@ -121,10 +121,10 @@ module.exports.setup = (http, ws) => { const detectorService = new DetectorService(ctrlProxy, apricotProxy); const environmentService = new EnvironmentService( - ctrlProxy, apricotService, cacheService, broadcastService, environmentCacheService + ctrlProxy, detectorService, cacheService, broadcastService, environmentCacheService ); const workflowService = new WorkflowTemplateService(ctrlProxy, apricotService); - const deploymentService = new DeploymentService(environmentService, workflowService, environmentCacheService); + const deploymentService = new DeploymentService(environmentService, workflowService, environmentCacheService, cacheService, broadcastService); const taskService = new TaskService(ctrlProxy); /** @@ -179,6 +179,13 @@ module.exports.setup = (http, ws) => { const verifyLockOwnershipMiddleware = getDetectorsLockOwnershipMiddlewareFactory(lockService); const validateConsulServiceMiddleware = validateConsulServiceMiddlewareFactory(consulService); const verifyDetectorsAvailabilityMiddleware = verifyDetectorsAvailabilityMiddlewareFactory(detectorService); + const deploymentMandatoryMiddleware = [ + ...coreMiddleware, + logDeploymentRequestMiddleware, + minimumRoleMiddleware(Role.DETECTOR), + verifyLockOwnershipMiddleware, + verifyDetectorsAvailabilityMiddleware, + ]; ctrlProxy.methods.forEach( (method) => http.post(`/${method}`, coreMiddleware, (req, res) => ctrlService.executeCommand(req, res)), @@ -196,7 +203,7 @@ module.exports.setup = (http, ws) => { http.get('/environments', coreMiddleware, envCtrl.getEnvironmentsHandler.bind(envCtrl), {public: true}); http.get('/environment/:id/:source?', coreMiddleware, envCtrl.getEnvironmentHandler.bind(envCtrl), {public: true}); - http.post('/environment/auto', coreMiddleware, envCtrl.newAutoEnvironmentHandler.bind(envCtrl)); + http.put('/environment/:id', coreMiddleware, minimumRoleMiddleware(Role.DETECTOR), @@ -213,14 +220,8 @@ module.exports.setup = (http, ws) => { envCtrl.destroyEnvironmentHandler.bind(envCtrl), ); - http.post('/deploy', - coreMiddleware, - logDeploymentRequestMiddleware, - minimumRoleMiddleware(Role.DETECTOR), - verifyLockOwnershipMiddleware, - verifyDetectorsAvailabilityMiddleware, - deploymentController.newAsyncDeploymentHandler.bind(deploymentController) - ); + http.post('/deploy', deploymentMandatoryMiddleware, deploymentController.newAsyncDeploymentHandler.bind(deploymentController)); + http.post('/deploy/calibration', deploymentMandatoryMiddleware, deploymentController.newAsyncDeploymentCalibrationHandler.bind(deploymentController)); http.delete('/deploy/:id', minimumRoleMiddleware(Role.DETECTOR), diff --git a/Control/lib/controllers/Deployment.controller.js b/Control/lib/controllers/Deployment.controller.js index c9200e6fe..a23f060b8 100644 --- a/Control/lib/controllers/Deployment.controller.js +++ b/Control/lib/controllers/Deployment.controller.js @@ -19,6 +19,7 @@ const { } = require('@aliceo2/web-ui'); const {User} = require('./../dtos/User.js'); +const LOG_FACILITY = 'cog/deployment-ctrl'; /** * Controller Class for managing deployments via the AliECS system @@ -121,6 +122,58 @@ class DeploymentController { } } + /** + * API - POST endpoint for requesting a new deployment for calibration purposes. + * This is a specific endpoint separated from the generic deployment one as it has specific input requirements and validations. + * @param {Request} req - HTTP Request object which expects a body with the following properties + * @param {string[]} req.body.detectors - list of detectors for which the calibration environment should be deployed. Must contain exactly one detector. + * @param {string} req.body.runType - the type of the calibration run to be performed which determines the workflow template to use + * @param {string} req.body.configurationName - the name of the saved configuration to use for the deployment + * @param {Response} res - HTTP Response object with result of the deployment request + * @returns {void} + */ + async newAsyncDeploymentCalibrationHandler(req, res) { + const {personid, name, username} = req.session; + const user = new User(username, name, personid); + const { detectors, runType, selectedConfiguration } = req.body; + + if (detectors?.length !== 1) { + updateAndSendExpressResponseFromNativeError( + res, + new InvalidInputError('Exactly one detector must be specified for deployment') + ); + return; + } + const [detector] = detectors; + + if (!selectedConfiguration) { + updateAndSendExpressResponseFromNativeError( + res, + new InvalidInputError('Missing Configuration Name for deployment') + ); + return; + } + + // Attempt to deploy environment + try { + this._logger.infoMessage(`Request by username(${username}) to deploy configuration ${selectedConfiguration}`, + {level: LogLevel.OPERATIONS, system: 'GUI', facility: LOG_FACILITY} + ); + const environment = await this._deploymentService.deployEnvironmentCalibration( + { + detector, runType, selectedConfiguration, user, + } + ); + res.status(201).json(environment); + } catch (error) { + this._logger.errorMessage( + `Unable to deploy request by username(${username}) for ${selectedConfiguration} due to ${error.message}`, + {level: LogLevel.OPERATIONS, system: 'GUI', facility: LOG_FACILITY} + ); + updateAndSendExpressResponseFromNativeError(res, error); + } + } + /** * API - DELETE endpoint for acknowledging an environment deployment failure * @param {Request} req - HTTP Request object which expects an `id` as mandatory parameter diff --git a/Control/lib/controllers/Environment.controller.js b/Control/lib/controllers/Environment.controller.js index 1416724a8..8ffa80a80 100644 --- a/Control/lib/controllers/Environment.controller.js +++ b/Control/lib/controllers/Environment.controller.js @@ -12,9 +12,7 @@ * or submit itself to any jurisdiction. */ const {LogManager, LogLevel} = require('@aliceo2/web-ui'); -const { - updateAndSendExpressResponseFromNativeError, InvalidInputError, UnauthorizedAccessError -} = require('@aliceo2/web-ui'); +const {updateAndSendExpressResponseFromNativeError, InvalidInputError} = require('@aliceo2/web-ui'); const LOG_FACILITY = 'cog/env-ctrl'; const {EnvironmentTransitionType} = require('./../common/environmentTransitionType.enum.js'); @@ -165,87 +163,6 @@ class EnvironmentController { this._logger.debug(`DESTROY_ENVIRONMENT,${id},${runNumber},${destroyRequestedAt},${Date.now()}`); } } - - /** - * API - POST endpoint for deploying a new environment based on a given configuration name - * @param {Request} req - HTTP Request object - * @param {Response} res - HTTP Response object with EnvironmentDetails - * @returns {void} - */ - async newAutoEnvironmentHandler(req, res) { - const {personid, name, username} = req.session; - const user = new User(username, name, personid); - const {detector, runType, configurationName} = req.body; - - if (!this._lockService.isLockOwnedByUser(detector, user)) { - updateAndSendExpressResponseFromNativeError(res, new UnauthorizedAccessError('Lock not taken')); - return; - } - - if (!configurationName) { - updateAndSendExpressResponseFromNativeError( - res, - new InvalidInputError('Missing Configuration Name for deployment') - ); - return; - } - - try { - const areDetectorsAvailable = await this._detectorService.areDetectorsAvailable([detector]); - if (!areDetectorsAvailable) { - updateAndSendExpressResponseFromNativeError( - res, - new InvalidInputError(`Detector ${detector} is already active`) - ); - return; - } - } catch (error) { - updateAndSendExpressResponseFromNativeError(res, error); - return; - } - - // Retrieve latest configuration version for given name - let variables; - try { - const configuration = await this._workflowService.retrieveWorkflowSavedConfiguration(configurationName); - if (!configuration.variables) { - throw new InvalidInputError(`No configuration variables found for ${configurationName}`); - } - variables = configuration.variables; - } catch (error) { - this._logger.debug(`Unable to retrieve saved configuration for ${configurationName} due to`); - this._logger.debug(error); - updateAndSendExpressResponseFromNativeError(res, error); - return; - } - - // Retrieve latest default workflow to use - let workflowTemplatePath; - try { - const {template, repository, revision} = await this._workflowService.getDefaultTemplateSource(); - workflowTemplatePath = `${repository}/workflows/${template}@${revision}`; - } catch (error) { - this._logger.debug(`Unable to retrieve default workflow template due to ${error}`); - updateAndSendExpressResponseFromNativeError(res, error); - return; - } - // Attempt to deploy environment - try { - this._logger.infoMessage(`Request by username(${username}) to deploy configuration ${configurationName}`, - {level: LogLevel.OPERATIONS, system: 'GUI', facility: LOG_FACILITY} - ); - const environment = await this._envService.newAutoEnvironment( - workflowTemplatePath, variables, detector, runType, user - ); - res.status(200).json(environment); - } catch (error) { - this._logger.errorMessage( - `Unable to deploy request by username(${username}) for ${configurationName} due to error`, - {level: LogLevel.OPERATIONS, system: 'GUI', facility: LOG_FACILITY} - ); - updateAndSendExpressResponseFromNativeError(res, error); - } - } } module.exports = {EnvironmentController}; diff --git a/Control/lib/services/Deployment.service.js b/Control/lib/services/Deployment.service.js index 289b1cb90..af3fcabd7 100644 --- a/Control/lib/services/Deployment.service.js +++ b/Control/lib/services/Deployment.service.js @@ -12,8 +12,9 @@ * or submit itself to any jurisdiction. */ -const {LogManager, LogLevel, NotFoundError} = require('@aliceo2/web-ui'); +const {LogManager, LogLevel, NotFoundError, InvalidInputError} = require('@aliceo2/web-ui'); const CoreUtils = require('./../control-core/CoreUtils.js'); +const {CacheKeys} = require('../common/cacheKeys.enum.js'); /** * **high-level service for deployment** @@ -29,11 +30,16 @@ class DeploymentService { * Constructor for inserting dependencies needed to retrieve environment data * @param {EnvironmentService} environmentService - to use for creating new environments * @param {WorkflowService} workflowService - to use for retrieving template workflow information + * @param {EnvironmentCacheService} environmentCacheService - to use for retrieving and updating environment data in cache + * @param {CacheService} cacheService - to use for retrieving and updating general data in cache, e.g. calibration runs requests + * @param {BroadcastService} broadcastService - to use for broadcasting updates to clients, e.g. calibration runs requests updates */ - constructor(environmentService, workflowService, environmentCacheService) { + constructor(environmentService, workflowService, environmentCacheService, cacheService, _broadcastService) { this._environmentService = environmentService; this._workflowService = workflowService; this._environmentCacheService = environmentCacheService; + this._generalCacheService = cacheService; + this._broadcastService = _broadcastService; this._logger = LogManager.getLogger(`${process.env.npm_config_log_label ?? 'cog'}/deployment-service`); } @@ -66,6 +72,69 @@ class DeploymentService { return environment; } + /** + * High-level service method to gather the necessary information and request the deployment of a calibration environment for a given detector and run type. + * @param {object} deploymentConfiguration - the configuration to be used for deployment + * @param {string} deploymentConfiguration.detector - the detector for which the calibration environment should be deployed + * @param {string} deploymentConfiguration.runType - the run type to be used for deployment, needed to retrieve the hosts to ignore for deployment + * @param {string} deploymentConfiguration.selectedConfiguration - the name of the saved configuration to be used for deployment, needed to retrieve the variables for deployment + * @param {User} deploymentConfiguration.user - the user to be used for deployment + * @returns {EnvironmentInfo} - the id of the environment created + * @throws {Error} - if the deployment fails or invalid input + */ + async deployEnvironmentCalibration({ detector, runType, selectedConfiguration, user }) { + // Retrieve latest configuration version for given name + const { variables } = await this._workflowService.retrieveWorkflowSavedConfiguration(selectedConfiguration); + if (!variables) { + throw new InvalidInputError(`No configuration variables found for ${selectedConfiguration}`); + } + + // Retrieve latest default workflow to use + const { template, repository, revision } = await this._workflowService.getDefaultTemplateSource(); + const workflowTemplatePath = `${repository}/workflows/${template}@${revision}`; + + const environment = await this._environmentService.newEnvironmentAsync({ + workflowTemplate: workflowTemplatePath, + userVars: variables, + user, + shouldAutoTransition: true, + detectors: [detector], + }); + + /** + * A dedicated calibration page environment object is created to follow only change of environment state events + */ + const calibrationEnvironment = { + inProgress: true, + detector, + runType, + events: [ + { + type: 'ENVIRONMENT', + payload: { + id: environment.id, + message: 'request was sent to AliECS', + at: Date.now(), + } + } + ], + }; + let calibrationRunsRequests = this._generalCacheService.getByKey(CacheKeys.CALIBRATION_RUNS_REQUESTS); + if (!calibrationRunsRequests) { + calibrationRunsRequests = {}; + } + if (!calibrationRunsRequests[detector]) { + calibrationRunsRequests[detector] = {}; + } + if (!calibrationRunsRequests[detector][runType]) { + calibrationRunsRequests[detector][runType] = calibrationEnvironment; + + } + this._generalCacheService.updateByKeyAndBroadcast(CacheKeys.CALIBRATION_RUNS_REQUESTS, calibrationRunsRequests); + this._broadcastService.broadcast(CacheKeys.CALIBRATION_RUNS_REQUESTS, calibrationRunsRequests[detector][runType]); + return environment; + } + /** * Method to acknowledge a deployment failure for a given environment. * A failed deployment is not considered active anymore by ECS, thus it will only be present in the GUI cache diff --git a/Control/lib/services/Environment.service.js b/Control/lib/services/Environment.service.js index 09129ba36..9fcfca6c2 100644 --- a/Control/lib/services/Environment.service.js +++ b/Control/lib/services/Environment.service.js @@ -13,7 +13,6 @@ */ const {LogManager,grpcErrorToNativeError, NotFoundError} = require('@aliceo2/web-ui'); -const { CacheKeys } = require('./../common/cacheKeys.enum.js'); const { BroadcastKeys: { ENVIRONMENTS_OVERVIEW } } = require('./../common/broadcastKeys.enum'); const EnvironmentInfoAdapter = require('./../adapters/EnvironmentInfoAdapter.js'); const {EnvironmentTransitionResultAdapter} = require('./../adapters/EnvironmentTransitionResultAdapter.js'); @@ -25,21 +24,21 @@ class EnvironmentService { /** * Constructor for inserting dependencies needed to retrieve environment data * @param {GrpcServiceClient} coreGrpc - * @param {ApricotProxy} apricotGrpc + * @param {DetectorService} detectorService - to use for retrieving detector and host information * @param {CacheService} cacheService - to use for updating information on environments * @param {BroadcastService} broadcastService - to use for broadcasting information * @param {EnvironmentCacheService} environmentCacheService - to use for caching environments */ - constructor(coreGrpc, apricotGrpc, cacheService, broadcastService, environmentCacheService) { + constructor(coreGrpc, detectorService, cacheService, broadcastService, environmentCacheService) { /** * @type {GrpcServiceClient} */ this._coreGrpc = coreGrpc; /** - * @type {ApricotProxy} + * @type {DetectorService} */ - this._apricotGrpc = apricotGrpc; + this._detectorService = detectorService; /** * @type {CacheService} */ @@ -130,8 +129,8 @@ class EnvironmentService { if (!environment) { throw new NotFoundError(`Environment (id: ${id}) not found`); } - const detectorsAll = this._apricotGrpc.detectors ?? []; - const hostsByDetector = this._apricotGrpc.hostsByDetector ?? {}; + const detectorsAll = this._detectorService.detectors; + const hostsByDetector = this._detectorService.hostsByDetector; const environmentInfo = EnvironmentInfoAdapter.toEntity( environment, taskSource, detectorsAll, hostsByDetector ); @@ -204,8 +203,8 @@ class EnvironmentService { throw grpcErrorToNativeError(grpcError); } - const detectorsAll = this._apricotGrpc.detectors ?? []; - const hostsByDetector = this._apricotGrpc.hostsByDetector ?? {}; + const detectorsAll = this._detectorService.detectors; + const hostsByDetector = this._detectorService.hostsByDetector; /** * Transition is not yet started as per ECS, but we set the state to DEPLOYING to ensure that the UI * is updated accordingly. The state will be updated once the environment is created and the transition @@ -238,146 +237,6 @@ class EnvironmentService { this._environmentCacheService.addOrUpdateEnvironment(environmentInfo, true); return environmentInfo; } - - /** - * Given the workflowTemplate and variables configuration, it will generate a unique string and send all to AliECS to create a - * new auto transitioning environment - * @param {String} workflowTemplate - name in format `repository/revision/template` - * @param {Object} vars - KV string pairs to define environment configuration - * @param {String} detector - on which the environment is deployed - * @param {String} runType - for which the environment is deployed - * @return {AutoEnvironmentDeployment} - if environment request was successfully sent - */ - async newAutoEnvironment(workflowTemplate, vars, detector, runType, user) { - const channelIdString = (Math.floor(Math.random() * (999999 - 100000) + 100000)).toString(); - const autoEnvironment = { - channelIdString, - inProgress: true, - detector, - runType, - events: [ - { - type: 'ENVIRONMENT', - payload: { - id: '-', - message: 'request was sent to AliECS', - at: Date.now(), - } - } - ], - }; - let calibrationRunsRequests = this._cacheService.getByKey(CacheKeys.CALIBRATION_RUNS_REQUESTS); - if (!calibrationRunsRequests) { - calibrationRunsRequests = {}; - } - if (!calibrationRunsRequests[detector]) { - calibrationRunsRequests[detector] = {}; - } - if (!calibrationRunsRequests[detector[runType]]) { - calibrationRunsRequests[detector][runType] = autoEnvironment; - - } - this._cacheService.updateByKeyAndBroadcast(CacheKeys.CALIBRATION_RUNS_REQUESTS, calibrationRunsRequests); - this._broadcastService.broadcast(CacheKeys.CALIBRATION_RUNS_REQUESTS, calibrationRunsRequests[detector][runType]); - - const subscribeChannel = this._coreGrpc.client.Subscribe({id: channelIdString}); - subscribeChannel.on('data', (data) => this._onData(data, detector, runType)); - subscribeChannel.on('error', (error) => this._onError(error, detector, runType)); - subscribeChannel.on('end', () => this._onEnd(detector, runType)); - - - this._coreGrpc.NewAutoEnvironment({ - vars, - workflowTemplate, - id: channelIdString, - requestUser: user.toEcsFormat() - }); - - return autoEnvironment; - } - - /** - * Method to parse incoming messages from stream channel - * @param {Event} event - AliECS Event (proto) - * @param {String} detector - detector name for which the event was triggered - * @param {String} runType - run type for which the event was triggered - * @return {void} - */ - _onData(event, detector, runType) { - const events = []; - const {taskEvent, environmentEvent, timestamp = Date.now()} = event; - if (taskEvent && (taskEvent.state === 'ERROR' || taskEvent.status === 'TASK_FAILED')) { - events.push({ - type: 'TASK', - payload: { - ...taskEvent, - at: Number(timestamp), - message: 'Please ensure environment is killed before retrying', - } - }); - } else if (environmentEvent) { - events.push({ - type: 'ENVIRONMENT', - payload: { - ...environmentEvent, - at: Number(timestamp), - } - }); - } - if (events.length > 0) { - const calibrationRunsRequests = this._cacheService.getByKey(CacheKeys.CALIBRATION_RUNS_REQUESTS); - calibrationRunsRequests[detector][runType].events.push(...events); - this._cacheService.updateByKeyAndBroadcast(CacheKeys.CALIBRATION_RUNS_REQUESTS, calibrationRunsRequests); - this._broadcastService.broadcast(CacheKeys.CALIBRATION_RUNS_REQUESTS, calibrationRunsRequests[detector][runType]); - } - } - - /** - * Method to be used in case of AliECS environment creation request error - * @param {Error} error - error encountered during the creation of environment - * @param {String} detector - detector name for which the event was triggered - * @param {String} runType - run type for which the event was triggered - * @return {void} - */ - _onError(error, detector, runType) { - const calibrationRunsRequests = this._cacheService.getByKey(CacheKeys.CALIBRATION_RUNS_REQUESTS); - calibrationRunsRequests[detector][runType].events.push({ - type: 'ERROR', - payload: { - error, - at: Date.now() - } - }); - calibrationRunsRequests[detector][runType].events.push({ - type: 'ERROR', - payload: { - error: 'Please ensure environment is killed before retrying', - at: Date.now() - } - }); - this._cacheService.updateByKeyAndBroadcast(CacheKeys.CALIBRATION_RUNS_REQUESTS, calibrationRunsRequests); - this._broadcastService.broadcast(CacheKeys.CALIBRATION_RUNS_REQUESTS, calibrationRunsRequests[detector][runType]); - } - - /** - * Method to be used for when environment successfully finished transitioning - * @param {String} detector - detector name for which the event was triggered - * @param {String} runType - run type for which the event was triggered - * @return {void} - */ - _onEnd(detector, runType) { - const calibrationRunsRequests = this._cacheService.getByKey(CacheKeys.CALIBRATION_RUNS_REQUESTS); - calibrationRunsRequests[detector][runType].events.push({ - type: 'ENVIRONMENT', - payload: { - at: Date.now(), - message: 'Stream has now ended' - } - }); - calibrationRunsRequests[detector][runType].inProgress = false; - this._cacheService.updateByKeyAndBroadcast(CacheKeys.CALIBRATION_RUNS_REQUESTS, calibrationRunsRequests); - this._broadcastService.broadcast(CacheKeys.CALIBRATION_RUNS_REQUESTS, calibrationRunsRequests[detector][runType]); - } } module.exports = {EnvironmentService}; diff --git a/Control/lib/services/Run.service.js b/Control/lib/services/Run.service.js index f8aaea6f4..fae8d664e 100644 --- a/Control/lib/services/Run.service.js +++ b/Control/lib/services/Run.service.js @@ -136,10 +136,10 @@ class RunService { const calibrationMappings = await this._apricotService.getRuntimeEntryByComponent(COG, CALIBRATION_MAPPING); return JSON.parse(calibrationMappings); } catch (error) { - const err = grpcErrorToNativeError(error); - this._logger.errorMessage(`Unable to load calibration mapping due to: ${err}`, + const nativeError = error instanceof Error ? error : grpcErrorToNativeError(error); + this._logger.errorMessage(`Unable to load calibration mapping due to: ${nativeError.message}`, {level: LogLevel.OPERATIONS, system: 'GUI', facility: 'calibration-service'} - ) + ); } return {}; } diff --git a/Control/lib/services/environment/EnvironmentCache.service.js b/Control/lib/services/environment/EnvironmentCache.service.js index de35a11e3..ee3082f2d 100644 --- a/Control/lib/services/environment/EnvironmentCache.service.js +++ b/Control/lib/services/environment/EnvironmentCache.service.js @@ -25,6 +25,7 @@ const { EnvironmentState } = require('../../common/environmentState.enum.js'); const { TaskState } = require('../../common/taskState.enum.js'); const { EnvironmentTransitionType } = require('../../common/environmentTransitionType.enum.js'); const { EcsOperationAndStepStatus } = require('../../common/ecsOperationAndStepStatus.enum.js'); +const { CacheKeys } = require('../../common/cacheKeys.enum.js'); const EPN_PATH_IN_ENVIRONMENT_INFO = 'hardware.epn.info'; /** @@ -38,13 +39,15 @@ class EnvironmentCacheService { * - optional service for broadcasting information * @param {BroadcastService} broadcastService - which is to be used for broadcasting * @param {EventEmitter} eventEmitter - which is to be used for listening to events + * @param {CacheService} cacheService - which is to be used for storing and retrieving cached data */ - constructor(broadcastService, eventEmitter) { + constructor(broadcastService, eventEmitter, cacheService) { this._environments = new Map(); this._lastUpdate = undefined; this._broadcastService = broadcastService; this._eventEmitter = eventEmitter; + this._cacheService = cacheService; this._logger = LogManager.getLogger(`${process.env.npm_config_log_label ?? 'cog'}/env-cache-service`); this._listenToEventsAndBroadcast(); @@ -275,6 +278,36 @@ class EnvironmentCacheService { this._broadcastService.broadcast(ENVIRONMENT_EVENTS, cachedEnvironment); this._lastUpdate = Date.now(); + + try { + /** + * Check if ID of environment is available in the general cache of CALIBRATION_RUNS_REQUESTS. + * If yes, find the detector and runType for this environment, push the event to the cache and broadcast it to clients. + */ + const calibrationRunsRequests = this._cacheService?.getByKey(CacheKeys.CALIBRATION_RUNS_REQUESTS); + const { userVars } = cachedEnvironment; + const { includedDetectors = [], runType } = userVars ?? {}; + + if (includedDetectors.length === 1 && runType) { + // One detector only, it means environment may be of calibration type. + const [detector] = includedDetectors; + if (calibrationRunsRequests?.[detector]?.[runType]) { + calibrationRunsRequests[detector][runType].events.push( + { + type: 'ENVIRONMENT', + payload: { ...environmentEvent, at: environmentEvent.timestamp ?? Date.now() }, + }); + calibrationRunsRequests[detector][runType].inProgress = cachedEnvironment.isDeploying; + this._cacheService.updateByKeyAndBroadcast(CacheKeys.CALIBRATION_RUNS_REQUESTS, calibrationRunsRequests); + this._broadcastService.broadcast( + CacheKeys.CALIBRATION_RUNS_REQUESTS, + calibrationRunsRequests[detector][runType] + ); + } + } + } catch (error) { + console.trace(error); + } } } diff --git a/Control/public/pages/CalibrationRuns/CalibrationRuns.model.js b/Control/public/pages/CalibrationRuns/CalibrationRuns.model.js index 1a55ebde2..08676a173 100644 --- a/Control/public/pages/CalibrationRuns/CalibrationRuns.model.js +++ b/Control/public/pages/CalibrationRuns/CalibrationRuns.model.js @@ -92,9 +92,9 @@ export class CalibrationRunsModel extends Observable { this.notify(); const payload = { - detector, runType, configurationName + detectors: [detector], runType, selectedConfiguration: configurationName }; - const {result, ok} = await this._model.loader.post('/api/environment/auto', payload, true); + const {result, ok} = await this._model.loader.post('/api/deploy/calibration', payload, true); this._calibrationRuns.payload[detector][runType].ongoingCalibrationRun = ok ? RemoteData.success(result) : RemoteData.failure(result.message); diff --git a/Control/test/lib/controllers/mocha-deployment.controller.js b/Control/test/lib/controllers/mocha-deployment.controller.js index bad17c9a4..3f78eaba2 100644 --- a/Control/test/lib/controllers/mocha-deployment.controller.js +++ b/Control/test/lib/controllers/mocha-deployment.controller.js @@ -179,4 +179,128 @@ describe('DeploymentController test suite', function() { assert.ok(res.json.calledWith({ message: 'Environment deployment failure acknowledged' })); }); }); + + describe('newAsyncDeploymentCalibrationHandler - tests', function() { + let mockEnvService; + + beforeEach(function() { + mockWorkflowService.retrieveWorkflowSavedConfiguration = sinon.stub(); + mockEnvService = { newAutoEnvironment: sinon.stub() }; + deploymentController._envService = mockEnvService; + req.body = {}; + res = { + status: sinon.stub().returnsThis(), + json: sinon.stub() + }; + }); + + it('should return 400 if detectors list is empty', async function() { + req.body = { detectors: [], configurationName: 'test-config' }; + await deploymentController.newAsyncDeploymentCalibrationHandler(req, res); + assert.ok(res.status.calledWith(400)); + assert.ok(res.json.calledWith({ + message: 'Exactly one detector must be specified for deployment', + status: 400, + title: 'Invalid Input' + })); + }); + + it('should return 400 if more than one detector is provided', async function() { + req.body = { detectors: ['DET1', 'DET2'], configurationName: 'test-config' }; + await deploymentController.newAsyncDeploymentCalibrationHandler(req, res); + assert.ok(res.status.calledWith(400)); + assert.ok(res.json.calledWith({ + message: 'Exactly one detector must be specified for deployment', + status: 400, + title: 'Invalid Input' + })); + }); + + it('should return 400 if configurationName is missing', async function() { + req.body = { detectors: ['DET1'] }; + await deploymentController.newAsyncDeploymentCalibrationHandler(req, res); + assert.ok(res.status.calledWith(400)); + assert.ok(res.json.calledWith({ + message: 'Missing Configuration Name for deployment', + status: 400, + title: 'Invalid Input' + })); + }); + + it('should return error when retrieveWorkflowSavedConfiguration rejects', async function() { + req.body = { detectors: ['DET1'], configurationName: 'test-config' }; + mockWorkflowService.retrieveWorkflowSavedConfiguration.rejects(new Error('Configuration not found')); + await deploymentController.newAsyncDeploymentCalibrationHandler(req, res); + assert.ok(res.status.calledWith(500)); + assert.ok(res.json.calledWith({ + message: 'Configuration not found', + status: 500, + title: 'Unknown Error' + })); + }); + + it('should return 400 when no variables found in the saved configuration', async function() { + req.body = { detectors: ['DET1'], configurationName: 'test-config' }; + mockWorkflowService.retrieveWorkflowSavedConfiguration.resolves({ variables: null }); + await deploymentController.newAsyncDeploymentCalibrationHandler(req, res); + assert.ok(res.status.calledWith(400)); + assert.ok(res.json.calledWith({ + message: 'No configuration variables found for test-config', + status: 400, + title: 'Invalid Input' + })); + }); + + it('should return error when getDefaultTemplateSource rejects', async function() { + req.body = { detectors: ['DET1'], configurationName: 'test-config' }; + mockWorkflowService.retrieveWorkflowSavedConfiguration.resolves({ variables: { key: 'val' } }); + mockWorkflowService.getDefaultTemplateSource.rejects(new Error('Template source unavailable')); + await deploymentController.newAsyncDeploymentCalibrationHandler(req, res); + assert.ok(res.status.calledWith(500)); + assert.ok(res.json.calledWith({ + message: 'Template source unavailable', + status: 500, + title: 'Unknown Error' + })); + }); + + it('should return error when environment deployment fails', async function() { + req.body = { detectors: ['DET1'], configurationName: 'test-config', runType: 'PHYSICS' }; + mockWorkflowService.retrieveWorkflowSavedConfiguration.resolves({ variables: { key: 'val' } }); + mockWorkflowService.getDefaultTemplateSource.resolves({ template: 'readout', repository: 'repo', revision: '1.0' }); + mockEnvService.newAutoEnvironment.rejects(new Error('Deployment failed')); + await deploymentController.newAsyncDeploymentCalibrationHandler(req, res); + assert.ok(res.status.calledWith(500)); + assert.ok(res.json.calledWith({ + message: 'Deployment failed', + status: 500, + title: 'Unknown Error' + })); + }); + + it('should successfully deploy calibration environment and return 200', async function() { + const variables = { key: 'val' }; + const template = 'readout'; + const repository = 'repo'; + const revision = '1.0'; + const configurationName = 'test-config'; + const detector = 'DET1'; + const runType = 'PHYSICS'; + + req.body = { detectors: [detector], configurationName, runType }; + mockWorkflowService.retrieveWorkflowSavedConfiguration.resolves({ variables }); + mockWorkflowService.getDefaultTemplateSource.resolves({ template, repository, revision }); + mockEnvService.newAutoEnvironment.resolves({ id: 'env123' }); + + await deploymentController.newAsyncDeploymentCalibrationHandler(req, res); + + assert.ok(mockEnvService.newAutoEnvironment.calledOnce); + assert.strictEqual(mockEnvService.newAutoEnvironment.firstCall.args[0], `${repository}/workflows/${template}@${revision}`); + assert.deepStrictEqual(mockEnvService.newAutoEnvironment.firstCall.args[1], variables); + assert.strictEqual(mockEnvService.newAutoEnvironment.firstCall.args[2], detector); + assert.strictEqual(mockEnvService.newAutoEnvironment.firstCall.args[3], runType); + assert.ok(res.status.calledWith(200)); + assert.ok(res.json.calledWith({ id: 'env123' })); + }); + }); });