Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
f2631ea
feat: add configuration endpoint
piechnikk Jun 4, 2025
82407cd
feat: implement getConfigurationByKey endpoint, refactor QCConfigurat…
piechnikk Jun 10, 2025
1e340a8
small code refactor
piechnikk Jun 11, 2025
09ad06f
change error handling in qcConfigurationController and add mocha test…
Jun 11, 2025
bf8c19b
implement api end-test
piechnikk Jun 17, 2025
25fd74a
feat: add configuration endpoint
piechnikk Jun 4, 2025
c9b39c3
feat: implement getConfigurationByKey endpoint, refactor QCConfigurat…
piechnikk Jun 10, 2025
07ffad4
small code refactor
piechnikk Jun 11, 2025
b63fed3
change error handling in qcConfigurationController and add mocha test…
Jun 11, 2025
1a35aba
implement api end-test
piechnikk Jun 17, 2025
42d62eb
feature: add endpoint for retrieving configuration restrictions
Deaponn Jun 23, 2025
1ae5ffe
chore: add test suites for the code
Deaponn Jun 23, 2025
2ecef47
Merge branch 'dev' into feature/CNF/OGUI-1698/implement-configuration…
piechnikk Jun 23, 2025
aad8275
Merge branch 'feature/CNF/OGUI-1698/implement-configuration-endpoint'…
Deaponn Jun 23, 2025
96c65ac
Merge branch 'dev' into feature/CNF/OGUI-1729/restrictions-endpoint
Deaponn Oct 31, 2025
7265a22
fix: remove public true from the endpoints
Deaponn Oct 31, 2025
4cbf5cc
test: fix test on non-existing configuration file
Deaponn Oct 31, 2025
dc15fb0
Merge branch 'dev' into feature/CNF/OGUI-1729/restrictions-endpoint
Deaponn Oct 31, 2025
6b30fe9
Merge branch 'dev' into feature/CNF/OGUI-1729/restrictions-endpoint
Deaponn Oct 31, 2025
c2c6d0c
docs: improve documentation
Deaponn Nov 3, 2025
c0cf971
test: fix failing test
Deaponn Nov 4, 2025
ff91d63
test: improve code coverage
Deaponn Nov 4, 2025
bcb6c45
refactor: move helpers to adapters directory
Deaponn Nov 8, 2025
e626a7b
test: fix tests
Deaponn Nov 8, 2025
97392f3
test: add test coverage for new features
Deaponn Nov 11, 2025
a25c636
Merge branch 'dev' into feature/CNF/OGUI-1729/restrictions-endpoint
Deaponn Nov 12, 2025
fba02a3
feature: address review comments
Deaponn Nov 17, 2025
7d3fd2b
docs: minor improvements
Deaponn Nov 17, 2025
f441b4c
Merge branch 'dev' into feature/CNF/OGUI-1729/restrictions-endpoint
Deaponn Nov 17, 2025
79b18ea
Merge branch 'dev' into feature/CNF/OGUI-1729/restrictions-endpoint
Deaponn Nov 19, 2025
32d941f
docs: improve docs
Deaponn Nov 20, 2025
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
58 changes: 58 additions & 0 deletions Control/lib/adapters/QCConfigurationAdapter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/**
* @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.
*/

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

/**
* QCConfigurationAdapter - Given aconfiguration object, construct Restrictions
* which is a set of restrictions based on the values contained in this configuration
*/
class QCConfigurationAdapter {
/**
* Derive type of value for every key-val pair
* of given configuration object
* @param {Object} configuration object we want to get restrictions of
* @returns {Restrictions} derived restrictions for a given configuration
*/
static computeRestrictions = (value) => {
const restrictions = {};
if (typeof value !== 'object' || Array.isArray(value) || value === null) {
return restrictions;
}
Object.entries(value).forEach(([key, val]) => (restrictions[key] = QCConfigurationAdapter.deriveValueType(val)));
return restrictions;
};

/**
* Derive the type of value and return it as a string
* possible types are 'string', 'boolean', 'number', 'array<`${NestedRestrictions}`>', `${NestedRestrictions}`
* @param {string | Array | Object} value that we want to get the Restrictions of
* @returns {string | Restrictions} derived type from the given value, could be a string, or further nested Restrictions
*/
static deriveValueType = (value) => {
// TODO OGUI-1803: implement function _combineTypes, so we can derive Type of value[0] and
// then combine it with Types of value[1], value[2] and so on to get the overall Type of values held in the array
if (Array.isArray(value)) { return 'array'; }
if (value instanceof Object) { return QCConfigurationAdapter.computeRestrictions(value); }
if (value.toLocaleLowerCase() === 'true' || value.toLocaleLowerCase() === 'false') {
return 'boolean';
}
if (!Number.isNaN(Number(value))) { return 'number'; }
if (typeof value === 'string') { return 'string'; }
const logger = LogManager.getLogger(`${process.env.npm_config_log_label ?? 'cog'}/qc-conf-adapter`);
logger.warnMessage(`Unknown value encountered while calculating restrictions from a configuration: ${value}`);
return 'unknown';
};
}

module.exports = QCConfigurationAdapter;
7 changes: 6 additions & 1 deletion Control/lib/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -102,8 +102,8 @@ module.exports.setup = (http, ws) => {
const broadcastService = new BroadcastService(ws);
const cacheService = new CacheService(broadcastService);
const environmentCacheService = new EnvironmentCacheService(broadcastService, eventEmitter);

const qcConfigurationService = new QCConfigurationService(consulService);

const qcConfigurationController = new QCConfigurationController(qcConfigurationService, config.consul);

const consulController = new ConsulController(consulService, config.consul);
Expand Down Expand Up @@ -284,6 +284,11 @@ module.exports.setup = (http, ws) => {
);

// Configuration
// this order of registering endpoints is necessary
http.get(
'/configurations/restrictions/:key(*)', validateConsulServiceMiddleware,
qcConfigurationController.getConfigurationRestrictionsByKeyHandler.bind(qcConfigurationController)
);
http.get(
'/configurations', validateConsulServiceMiddleware,
qcConfigurationController.getConfigurationsKeysHandler.bind(qcConfigurationController)
Expand Down
36 changes: 33 additions & 3 deletions Control/lib/controllers/QCConfiguration.controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ class QCConfigurationController {
* Method to get configurations names
* @param {Request} req - HTTP Request object
* @param {Response} res - HTTP Response object
* @returns {Promise<void>}
*/
async getConfigurationsKeysHandler(req, res) {
const { prefix = '', recurse = false } = req.query;
Expand Down Expand Up @@ -72,9 +73,10 @@ class QCConfigurationController {
* Method to get configuration value by key
* @param {Request} req - HTTP Request object
* @param {Response} res - HTTP Response object
* @returns {Promise<void>}
*/
async getConfigurationByKeyHandler(req, res) {
const { key } = req.params;
const { key = '' } = req.params;

if (!key || key.trim() === '') {
updateAndSendExpressResponseFromNativeError(res, new InvalidInputError('Missing configuration key'));
Expand All @@ -94,10 +96,37 @@ class QCConfigurationController {
}
}

/**
* Method to get configuration restrictions by key
* @param {Request} req - HTTP Request object
* @param {Response} res - HTTP Response object
* @returns {Promise<void>}
*/
async getConfigurationRestrictionsByKeyHandler(req, res) {
const { key = '' } = req.params;
if (!key || key.trim() === '') {
updateAndSendExpressResponseFromNativeError(res, new InvalidInputError('Missing configuration key'));
return;
}

try {
const restrictions = await this._qcConfigurationService.getConfigurationRestrictionsByKey(key);
res.status(200).json(restrictions);
} catch (error) {
errorLogger(error, this._logger);
if (error.message?.includes('Non-2xx status code: 404')) {
updateAndSendExpressResponseFromNativeError(res, new NotFoundError(`Configuration not found for key: ${key}`));
} else {
updateAndSendExpressResponseFromNativeError(res, new ServiceUnavailableError('Consul service unavailable'));
}
}
}

/**
* Method to edit configuration value
* @param {Request} req
* @param {Response} res
* @param {Request} req - HTTP Request object
* @param {Response} res - HTTP Response object
* @returns {Promise<void>}
*/
async putConfigurationByKeyHandler(req, res) {
const { key } = req.params;
Expand All @@ -111,6 +140,7 @@ class QCConfigurationController {
const editStatus = await this._qcConfigurationService.editConfigurationByKey(key, configuration);
if (!editStatus) {
updateAndSendExpressResponseFromNativeError(res, new ServiceUnavailableError('Could not edit configuration'));
return;
}

res.status(200).json(editStatus);
Expand Down
19 changes: 17 additions & 2 deletions Control/lib/services/QCConfiguration.service.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
*/

const { LogManager } = require('@aliceo2/web-ui');
const QCConfigurationAdapter = require('../adapters/QCConfigurationAdapter');

/**
* @class
Expand All @@ -37,6 +38,7 @@ class QCConfigurationService {
* Get keys of configurations stored in Consul
* @param {String} prefix - prefix to filter the keys
* @param {boolean} [recurse=false] - whether to recurse into subdirectories
* @returns {Promise<Array<string>>} names of configurations which are valid JSON
*/
async retrieveKeysOfValidConfigurations(prefix, recurse = false) {
const data = await this._consulService.getOnlyRawValuesByKeyPrefix(prefix);
Expand All @@ -46,6 +48,7 @@ class QCConfigurationService {
/**
* Get configuration by key from Consul
* @param {string} key - the key of the configuration
* @returns {Promise<string>} - the raw value stored for the requested key
*/
async retrieveConfigurationByKey(key) {
return await this._consulService.getOnlyRawValueByKey(key);
Expand All @@ -57,6 +60,7 @@ class QCConfigurationService {
* @param {object} configs - an object with string values to be checked.
* @param {boolean} recurse - whether to recurse into subdirectories
* @param {string} prefix - the prefix to filter keys
* @returns {Array<string>} names of configurations which are valid JSON
*/
filterConfigurations(configs, recurse, prefix) {
const parsedData = [];
Expand All @@ -79,10 +83,21 @@ class QCConfigurationService {
return parsedData;
}

/**
* Get configuration restrictions by key from Consul
* @param {string} key - the key of the configuration
* @returns {Promise<Restrictions>}
*/
async getConfigurationRestrictionsByKey(key) {
const configuration = await this._consulService.getOnlyRawValueByKey(key);
return QCConfigurationAdapter.computeRestrictions(configuration);
}

/**
* Edit configuration by key in Consul
* @param {String} key - the key of the configuration
* @param {String} value - the configuration
* @param {string} key - the key of the configuration
* @param {string} value - the configuration
* @returns {Promise<Object>} - JSON object with the status of the transaction
*/
async editConfigurationByKey(key, value) {
const listOfConfigurationsToEdit = [{ [key]: JSON.stringify(value, null, 2) }];
Expand Down
81 changes: 81 additions & 0 deletions Control/lib/typedefs/Restrictions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/**
* @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 {Object.<string, RestrictionsEntry>} Restrictions
*
* Object which is a map of types.
* Keys are taken from existing configuration.
* Values are describing what is expected type of value held there.
*
* For example for the given configuration:
* ```
* {
active: 'false',
moduleName: 'QualityControl',
extendedTaskParameters: {
default: {
default: {
verbose : 'false',
retryDelay: '10',
databaseUrl: 'https://alice-ccdb.cern.ch'
}
}
},
dataSources: [
{
name: 'CTP Config',
path: 'CTP/Config/Config',
active: 'true'
},
{
name: 'CTP Scalers',
path: 'CTP/Calib/Scalers',
active: 'false'
}
],
}
* ```
*
* The Restrictions are:
* ```
* {
* active: 'boolean',
* moduleName: 'string',
* extendedTaskParameters: {
* default: {
* default: {
* verbose: 'boolean',
* retryDelay: 'number',
* databaseUrl: 'string'
* }
* }
* },
* dataSources: [
* {
* name: 'string',
* path: 'string',
* active: 'boolean'
* }
* ]
* }
* ```
*/

/**
* A value in a `Restrictions` object can be:
* - a string literal 'string', 'boolean', 'number' or 'array'
* - nested Restrictions
*
* @typedef { 'string' | 'boolean' | 'number' | Restrictions | Restrictions[] } RestrictionsEntry
*/
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@

/**
* @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 request = require('supertest');
const { ADMIN_TEST_TOKEN, TEST_URL } = require('../generateToken.js');

describe(`'API - GET - /configurations/restrictions' test suite`, () => {
it('should successfully get a Restrictions object for given configurations', async () => {
await request(`${TEST_URL}/api/configurations/restrictions`)
.get(`/key1?token=${ADMIN_TEST_TOKEN}`)
.expect(200);
});

it('should return 400 when the key parameter is missing', async () => {
const expectedError = {
message: 'Missing configuration key',
status: 400,
title: 'Invalid Input'
};
await request(`${TEST_URL}/api/configurations/restrictions`)
.get(`/%20?token=${ADMIN_TEST_TOKEN}`)
.expect(400, expectedError);
});

it('should return 403 unauthorized for missing token requests', async () => {
await request(`${TEST_URL}/api/configurations/restrictions`)
.get('/')
.expect(403, {
error: '403 - Json Web Token Error',
message: 'You must provide a JWT token'
});
});

it('should return 403 unauthorized for invalid token requests', async () => {
await request(`${TEST_URL}/api/configurations/restrictions`)
.get('/?token=invalid-token')
.expect(403, {
error: '403 - Json Web Token Error',
message: 'Invalid JWT token provided'
});
});

it('should return 404 when the configuration key does not exist', async () => {
const expectedError = {
message: 'Configuration not found for key: nonexistent',
status: 404,
title: 'Not Found'
};
await request(`${TEST_URL}/api/configurations/restrictions`)
.get(`/nonexistent?token=${ADMIN_TEST_TOKEN}`)
.expect(404, expectedError);
});

it('should return 503 when Consul fails to respond', async () => {
const expectedError = {
message: 'Consul service unavailable',
status: 503,
title: 'Service Unavailable'
};
await request(`${TEST_URL}/api/configurations/restrictions`)
.get(`/consul-failure?token=${ADMIN_TEST_TOKEN}`)
.expect(503, expectedError);
});
});
Loading