Skip to content
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
26 changes: 23 additions & 3 deletions Control/lib/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,10 @@ const config = require('./config/configProvider.js');
const { DetectorId } = require('./common/detectorId.enum.js');

// middleware
const {minimumRoleMiddleware} = require('./middleware/minimumRole.middleware.js');
const {addDetectorIdMiddleware} = require('./middleware/addDetectorId.middleware.js');
const {logDeploymentRequestMiddleware} = require('./middleware/logDeploymentRequest.middleware.js');
const {minimumRoleMiddleware} = require('./middleware/minimumRole.middleware.js');
const {requireDetectorOrGlobalRoleMiddleware} = require('./middleware/requireDetectorOrGlobalRole.middleware.js');

const {
setDetectorsFromEnvironmentMiddlewareFactory
} = require('./middleware/setDetectorsFromEnvironmentMiddlewareFactory.js');
Expand All @@ -34,6 +34,7 @@ const {

// controllers
const {ConsulController} = require('./controllers/Consul.controller.js');
const {DeploymentController} = require('./controllers/Deployment.controller.js');
const {EnvironmentController} = require('./controllers/Environment.controller.js');
const {LockController} = require('./controllers/Lock.controller.js');
const {RunController} = require('./controllers/Run.controller.js');
Expand All @@ -45,8 +46,9 @@ const {WorkflowTemplateController} = require('./controllers/WorkflowTemplate.con
const {BookkeepingService} = require('./services/Bookkeeping.service.js');
const {BroadcastService} = require('./services/Broadcast.service.js');
const {CacheService} = require('./services/Cache.service.js');
const {EnvironmentCacheService} = require('./services/environment/EnvironmentCache.service.js');
const {DeploymentService} = require('./services/Deployment.service.js');
const {DetectorService} = require('./services/Detector.service.js');
const {EnvironmentCacheService} = require('./services/environment/EnvironmentCache.service.js');
const {EnvironmentService} = require('./services/Environment.service.js');
const {Intervals} = require('./services/Intervals.service.js');
const {LockService} = require('./services/Lock.service.js');
Expand Down Expand Up @@ -82,6 +84,11 @@ if (!config.grafana) {

module.exports.setup = (http, ws) => {
const eventEmitter = new EventEmitter();

/**
* Services are initialized with the configuration they need and in order of their dependencies.
* The services are then used by the controllers to perform actions.
*/
let consulService;
if (config.consul) {
consulService = new ConsulService(config.consul);
Expand All @@ -108,9 +115,14 @@ module.exports.setup = (http, ws) => {
ctrlProxy, apricotService, cacheService, broadcastService, environmentCacheService
);
const workflowService = new WorkflowTemplateService(ctrlProxy, apricotService);
const deploymentService = new DeploymentService(environmentService, workflowService);

/**
* Controllers are initialized with the services they depend on.
*/
const envCtrl = new EnvironmentController(environmentService, workflowService, lockService, detectorService);
const workflowController = new WorkflowTemplateController(workflowService);
const deploymentController = new DeploymentController(deploymentService);

const aliecsReqHandler = new AliecsRequestHandler(ctrlService, apricotService);
aliecsReqHandler.setWs(ws);
Expand Down Expand Up @@ -188,6 +200,14 @@ module.exports.setup = (http, ws) => {
envCtrl.destroyEnvironmentHandler.bind(envCtrl),
);

http.post('/deploy',
coreMiddleware,
logDeploymentRequestMiddleware,
minimumRoleMiddleware(Role.DETECTOR),
verifyLockOwnershipMiddleware,
deploymentController.newAsyncDeploymentHandler.bind(deploymentController)
);

http.post('/core/environments/configuration/save', (req, res) => apricotService.saveCoreEnvConfig(req, res));
http.post('/core/environments/configuration/update', (req, res) => apricotService.updateCoreEnvConfig(req, res));

Expand Down
87 changes: 87 additions & 0 deletions Control/lib/controllers/Deployment.controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/**
* @license
* Copyright 2019-2020 CERN and copyright holders of ALICE O2.
* See http://alice-o2.web.cern.ch/copyright for details of the copyright holders.
* All rights not expressly granted are reserved.
*
* This software is distributed under the terms of the GNU General Public
* License v3 (GPL Version 3), copied verbatim in the file "COPYING".
*
* In applying this license CERN does not waive the privileges and immunities
* granted to it by virtue of its status as an Intergovernmental Organization
* or submit itself to any jurisdiction.
*/
const {
LogManager,
LogLevel,
updateAndSendExpressResponseFromNativeError,
InvalidInputError
} = require('@aliceo2/web-ui');

const {User} = require('./../dtos/User.js');

/**
* Controller Class for managing deployments via the AliECS system
*/
class DeploymentController {

/**
* Constructor for initializing controller with a deployment service
* @param {DeploymentService} deploymentService - service to use to request AliECS a new deployment
*/
constructor(deploymentService) {
this._logger = LogManager.getLogger(`${process.env.npm_config_log_label ?? 'cog'}/deployment-ctrl`);

/**
* @type {DeploymentService}
*/
this._deploymentService = deploymentService;
}

/**
* Handles the request to make a deployment by:
* - validating the user built request
* - preparing the request payload for ECS
* - calling the ECS service to deploy the environment
*
* User must be authenticated and authorized to perform this action and this is verified via middlewares
*
* The result of a deployment is an environment
*
* @param {Express.Request} req - the request object
* @param {Express.Response} res - the response object
* @returns {Promise<void>}
*/
async newAsyncDeploymentHandler(req, res) {
/**
* @type {DeploymentRequest}
*/
const { workflowTemplate, selectedConfiguration, userVars } = req.body;

if (!workflowTemplate && !selectedConfiguration) {
updateAndSendExpressResponseFromNativeError(
res,
new InvalidInputError('Invalid input: workflowTemplate or selectedConfiguration must be provided')
)
return;
}

const { personid, name, username } = req.session || {};
const user = new User(username, name, personid);

try {
const environment = await this._deploymentService.deployEnvironment({
userVars,
selectedConfiguration,
workflowTemplate,
user,
});
res.status(201).json(environment);
} catch (error) {
this._logger.errorMessage(error, { level: LogLevel.SUPPORT });
updateAndSendExpressResponseFromNativeError(res, error);
}
}
}

module.exports = { DeploymentController };
44 changes: 44 additions & 0 deletions Control/lib/middleware/logDeploymentRequest.middleware.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/**
* @license
* Copyright 2019-2024 CERN and copyright holders of ALICE O2.
* See http://alice-o2.web.cern.ch/copyright for details of the copyright holders.
* All rights not expressly granted are reserved.
*
* This software is distributed under the terms of the GNU General Public
* License v3 (GPL Version 3), copied verbatim in the file "COPYING".
*
* In applying this license CERN does not waive the privileges and immunities
* granted to it by virtue of its status as an Intergovernmental Organization
* or submit itself to any jurisdiction.
*/

const {LogManager, LogLevel} = require('@aliceo2/web-ui');

/**
* Middleware to log deployment requests with user and request context.
* @param {Express.Request} req - Request object
* @param {Express.Response} _ - Response object
* @param {Express.Next} next - Next middleware function
* @return {void}
*/
const logDeploymentRequestMiddleware = (req, _, next) => {
/**
* @type {DeploymentRequest}
*/
const { selectedConfiguration, workflowTemplate, detectors = [] } = req.body;
const { username } = req.session;

const logMessage = `Deployment request from user: ${ username }`
+ (workflowTemplate ? ` for workflow: ${workflowTemplate}` : '')
+ (selectedConfiguration ? ` with configuration: ${selectedConfiguration}` : '')
+ ` with detectors: ${detectors.length > 0 ? detectors.join(', ') : 'none'}`;

LogManager
.getLogger(`${process.env.npm_config_log_label ?? 'cog'}/deployment-request`)
.infoMessage(logMessage, {
level: LogLevel.OPERATIONS,
});
next();
}

module.exports = { logDeploymentRequestMiddleware };
23 changes: 23 additions & 0 deletions Control/lib/typedefs/DeploymentRequest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/**
* @license
* Copyright CERN and copyright holders of ALICE O2. This software is
* distributed under the terms of the GNU General Public License v3 (GPL
* Version 3), copied verbatim in the file "COPYING".
*
* See http://alice-o2.web.cern.ch/license for full licensing information.
*
* In applying this license CERN does not waive the privileges and immunities
* granted to it by virtue of its status as an Intergovernmental Organization
* or submit itself to any jurisdiction.
*/

/**
* @typedef DeploymentRequest
*
* Deployment request as needed to be sent by the user to the API
*
* @property {string} workflowTemplate - the workflow template to use for the deployment, can be optional if selectedConfiguration is provided
* @property {string} [selectedConfiguration] - the selected configuration for the deployment, can be optional if workflowTemplate is provided
* @property {Map<string, object>} [userVars] - user variables to be used in the deployment, if none provided, ECS will use the default ones
* @property {string[]} detectors - list of detectors to be deployed
*/
94 changes: 94 additions & 0 deletions Control/test/api/deployment/api-post-deployment.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/**
* @license
* Copyright 2019-2025 CERN and copyright holders of ALICE O2.
* See http://alice-o2.web.cern.ch/copyright for details of the copyright holders.
* All rights not expressly granted are reserved.
*
* This software is distributed under the terms of the GNU General Public
* License v3 (GPL Version 3), copied verbatim in the file "COPYING".
*
* In applying this license CERN does not waive the privileges and immunities
* granted to it by virtue of its status as an Intergovernmental Organization
* or submit itself to any jurisdiction.
*/

const request = require('supertest');
const { DET_MID_TEST_TOKEN, GUEST_TEST_TOKEN, TEST_URL } = require('../generateToken.js');
const { DetectorLockAction } = require('../../../lib/common/lock/detectorLockAction.enum.js');

describe('POST /deploy', function () {
it('should reject unauthenticated requests from WebUI server', async function () {
await request(`${TEST_URL}/api`)
.post('/deploy')
.send({
workflowTemplate: 'test-template',
selectedConfiguration: 'test-config',
userVars: { foo: 'bar' }
})
.expect(403, {
message: 'You must provide a JWT token',
error: '403 - Json Web Token Error'
});
});

it('should reject deployment request due to user not being detector as minimum role', async function () {
await request(`${TEST_URL}/api`)
.post(`/deploy?token=${GUEST_TEST_TOKEN}`)
.send({
workflowTemplate: 'test-template',
selectedConfiguration: 'test-config',
userVars: { foo: 'bar' }
})
.expect(403, {
message: 'Not enough permissions for this operation',
status: 403,
title: 'Unauthorized Access'
});
});

it('should reject deployment request due to missing lock ownership', async function () {
await request(`${TEST_URL}/api`)
.post(`/deploy?token=${DET_MID_TEST_TOKEN}`)
.send({
workflowTemplate: 'test-template',
selectedConfiguration: 'test-config',
detectors: ['MID'],
userVars: { foo: 'bar' }
})
.expect(403, {
message: 'Action not allowed for user Detector User due to missing ownership of lock(s)',
});
});

it('should reject requests with missing workflowTemplate and selectedConfiguration', async function () {
// First we need to acquire the lock for MID detector
await request(`${TEST_URL}/api/locks`)
.put(`/${DetectorLockAction.TAKE}/MID?token=${DET_MID_TEST_TOKEN}`)
.expect(200, {
MID: { name: 'MID', state: 'TAKEN', owner: { username: 'det-mid', fullName: 'Detector User', personid: 2 } },
DCS: { name: 'DCS', state: 'FREE' },
ODC: { name: 'ODC', state: 'FREE' }
});

await request(`${TEST_URL}/api`)
.post(`/deploy?token=${DET_MID_TEST_TOKEN}`)
.send({
detectors: ['MID'],
userVars: { foo: 'bar' }
})
.expect(400, {
message: 'Invalid input: workflowTemplate or selectedConfiguration must be provided',
status: 400,
title: 'Invalid Input'
});

// Release the lock after the test so that tests are not chained
await request(`${TEST_URL}/api/locks`)
.put(`/${DetectorLockAction.RELEASE}/MID?token=${DET_MID_TEST_TOKEN}`)
.expect(200, {
MID: { name: 'MID', state: 'FREE' },
DCS: { name: 'DCS', state: 'FREE' },
ODC: { name: 'ODC', state: 'FREE' }
});
});
});
Loading
Loading