diff --git a/docs/rest-server/API.md b/docs/rest-server/API.md index 55a21c83b9..13742a34da 100644 --- a/docs/rest-server/API.md +++ b/docs/rest-server/API.md @@ -1018,4 +1018,4 @@ the version upgrade, which has no namespaces. They are called "legacy jobs", whi but cannot be created. To figure out them, there is a "legacy: true" field of them in list apis. In the next versions, all operations of legacy jobs may be disabled, so please re-create them as namespaced -job as soon as possible. +job as soon as possible. \ No newline at end of file diff --git a/src/hadoop-resource-manager/deploy/hadoop-resource-manager-configuration/yarn-site.xml b/src/hadoop-resource-manager/deploy/hadoop-resource-manager-configuration/yarn-site.xml index 9ef7105a42..85f64f240b 100644 --- a/src/hadoop-resource-manager/deploy/hadoop-resource-manager-configuration/yarn-site.xml +++ b/src/hadoop-resource-manager/deploy/hadoop-resource-manager-configuration/yarn-site.xml @@ -44,6 +44,12 @@ 1048576 default is 8GB, here we set 1024G + + + yarn.scheduler.configuration.store.class + zk + default is file, change it to zk to enable config by rest api + yarn.resourcemanager.scheduler.class diff --git a/src/rest-server/package.json b/src/rest-server/package.json index 9ff097096e..2609c7f7f5 100644 --- a/src/rest-server/package.json +++ b/src/rest-server/package.json @@ -49,10 +49,11 @@ "node-cache": "~4.2.0", "node-etcd": "~5.1.0", "nyc": "~11.6.0", + "ssh-keygen": "~0.4.2", "statuses": "~1.5.0", "unirest": "~0.5.1", "winston": "~2.4.0", - "ssh-keygen": "~0.4.2" + "xml2js": "~0.4.19" }, "scripts": { "coveralls": "nyc report --reporter=text-lcov | coveralls ..", diff --git a/src/rest-server/src/config/vc.js b/src/rest-server/src/config/vc.js new file mode 100644 index 0000000000..5e571b543f --- /dev/null +++ b/src/rest-server/src/config/vc.js @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation +// All rights reserved. +// +// MIT License +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +// documentation files (the "Software"), to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and +// to permit persons to whom the Software is furnished to do so, subject to the following conditions: +// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING +// BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +// module dependencies +const Joi = require('joi'); + +// define the input schema for the 'update vc' api +const vcPutInputSchema = Joi.object().keys({ + vcCapacity: Joi.number() + .integer() + .min(0) + .max(100) + .required(), +}).required(); + +// define the input schema for the 'put vc status' api +const vcStatusPutInputSchema = Joi.object().keys({ + vcStatus: Joi.string() + .valid(['stopped', 'running']) + .required(), +}).required(); + +// module exports +module.exports = { + vcPutInputSchema: vcPutInputSchema, + vcStatusPutInputSchema: vcStatusPutInputSchema, +}; diff --git a/src/rest-server/src/config/yarn.js b/src/rest-server/src/config/yarn.js index c4fe666a3e..fede335fb2 100644 --- a/src/rest-server/src/config/yarn.js +++ b/src/rest-server/src/config/yarn.js @@ -18,6 +18,9 @@ // module dependencies const Joi = require('joi'); +const unirest = require('unirest'); +const config = require('./index'); +const logger = require('./logger'); // get config from environment variables let yarnConfig = { @@ -26,8 +29,13 @@ let yarnConfig = { 'Accept': 'application/json', }, yarnVcInfoPath: `${process.env.YARN_URI}/ws/v1/cluster/scheduler`, + webserviceUpdateQueueHeaders: { + 'Content-Type': 'application/xml', + }, + yarnVcUpdatePath: `${process.env.YARN_URI}/ws/v1/cluster/scheduler-conf`, }; + const yarnConfigSchema = Joi.object().keys({ yarnUri: Joi.string() .uri() @@ -37,6 +45,11 @@ const yarnConfigSchema = Joi.object().keys({ yarnVcInfoPath: Joi.string() .uri() .required(), + webserviceUpdateQueueHeaders: Joi.object() + .required(), + yarnVcUpdatePath: Joi.string() + .uri() + .required(), }).required(); const {error, value} = Joi.validate(yarnConfig, yarnConfigSchema); @@ -45,4 +58,18 @@ if (error) { } yarnConfig = value; + +// framework launcher health check +if (config.env !== 'test') { + unirest.get(yarnConfig.yarnVcInfoPath) + .timeout(2000) + .end((res) => { + if (res.status === 200) { + logger.info('connected to yarn successfully'); + } else { + throw new Error('cannot connect to yarn'); + } + }); +} + module.exports = yarnConfig; diff --git a/src/rest-server/src/controllers/vc.js b/src/rest-server/src/controllers/vc.js index 31fa38dec8..bed60bacb1 100644 --- a/src/rest-server/src/controllers/vc.js +++ b/src/rest-server/src/controllers/vc.js @@ -20,23 +20,14 @@ const VirtualCluster = require('../models/vc'); const createError = require('../util/error'); /** - * Load virtual cluster and append to req. + * Validation, not allow operation to "default" vc. */ -const load = (req, res, next, vcName) => { - new VirtualCluster(vcName, (vcInfo, error) => { - if (error) { - return next(createError.unknown(error)); - } - req.vc = vcInfo; +const validate = (req, res, next, vcName) => { + if (vcName === 'default' && req.method !== 'GET') { + return next(createError('Forbidden', 'ForbiddenUserError', `Update operation to default vc isn't allowed`)); + } else { return next(); - }); -}; - -/** - * Get virtual cluster status. - */ -const get = (req, res) => { - return res.json(req.vc); + } }; /** @@ -59,9 +50,105 @@ const list = (req, res, next) => { }); }; +/** + * Get a vc. + */ +const get = (req, res, next) => { + const vcName = req.params.vcName; + VirtualCluster.prototype.getVc(vcName, (vcInfo, err) => { + if (err) { + return next(createError.unknown(err)); + } else { + return res.status(200).json(vcInfo); + } + }); +}; + + +/** + * Add a vc. + */ +const update = (req, res, next) => { + const vcName = req.params.vcName; + const vcCapacity = parseInt(req.body.vcCapacity); + if (req.user.admin) { + VirtualCluster.prototype.updateVc(vcName, vcCapacity, (err) => { + if (err) { + return next(createError.unknown(err)); + } else { + return res.status(201).json({ + message: `update vc: ${vcName} to capacity: ${vcCapacity} successfully`, + }); + } + }); + } else { + next(createError('Forbidden', 'ForbiddenUserError', `Non-admin is not allowed to do this operation.`)); + } +}; + + +/** + * Update vc status, changing a vc from running to stopped a vc will only prevent new job in this vc. + */ +const updateStatus = (req, res, next) => { + const vcName = req.params.vcName; + const vcStatus = req.body.vcStatus; + if (req.user.admin) { + if (vcStatus === 'stopped') { + VirtualCluster.prototype.stopVc(vcName, (err) => { + if (err) { + return next(createError.unknown(err)); + } else { + return res.status(201).json({ + message: `stop vc ${vcName} successfully`, + }); + } + }); + } else if (vcStatus === 'running') { + VirtualCluster.prototype.activeVc(vcName, (err) => { + if (err) { + return next(createError.unknown(err)); + } else { + return res.status(201).json({ + message: `active vc ${vcName} successfully`, + }); + } + }); + } else { + next(createError('Bad Request', 'BadConfigurationError', `Unknown vc status: ${vcStatus}`)); + } + } else { + next(createError('Forbidden', 'ForbiddenUserError', `Non-admin is not allowed to do this operation.`)); + } +}; + + +/** + * Remove a vc. + */ +const remove = (req, res, next) => { + const vcName = req.params.vcName; + if (req.user.admin) { + VirtualCluster.prototype.removeVc(vcName, (err) => { + if (err) { + return next(createError.unknown(err)); + } else { + return res.status(201).json({ + message: `remove vc: ${vcName} successfully`, + }); + } + }); + } else { + next(createError('Forbidden', 'ForbiddenUserError', `Non-admin is not allowed to do this operation.`)); + } +}; + // module exports module.exports = { - load, get, list, + update, + remove, + updateStatus, + validate, }; diff --git a/src/rest-server/src/models/vc.js b/src/rest-server/src/models/vc.js index 86708f4d7d..e010c2b8ff 100644 --- a/src/rest-server/src/models/vc.js +++ b/src/rest-server/src/models/vc.js @@ -18,44 +18,35 @@ // module dependencies const unirest = require('unirest'); +const xml2js = require('xml2js'); const yarnConfig = require('../config/yarn'); const createError = require('../util/error'); +const logger = require('../config/logger'); -class VirtualCluster { - constructor(name, next) { - this.getVcList((vcList, error) => { - if (error === null) { - if (name in vcList) { - for (let key of Object.keys(vcList[name])) { - this[key] = vcList[name][key]; - } - } else { - error = createError('Not Found', 'NoVirtualClusterError', `Virtual cluster ${name} is not found.`); - } - } - next(this, error); - }); - } +class VirtualCluster { getCapacitySchedulerInfo(queueInfo) { let queues = {}; + function traverse(queueInfo, queueDict) { if (queueInfo.type === 'capacitySchedulerLeafQueueInfo') { queueDict[queueInfo.queueName] = { - capacity: queueInfo.absoluteCapacity, - maxCapacity: queueInfo.absoluteMaxCapacity, + capacity: Math.round(queueInfo.absoluteCapacity), + maxCapacity: Math.round(queueInfo.absoluteMaxCapacity), usedCapacity: queueInfo.absoluteUsedCapacity, numActiveJobs: queueInfo.numActiveApplications, numJobs: queueInfo.numApplications, numPendingJobs: queueInfo.numPendingApplications, resourcesUsed: queueInfo.resourcesUsed, + status: queueInfo.state, }; } else { for (let i = 0; i < queueInfo.queues.queue.length; i++) { - traverse(queueInfo.queues.queue[i], queueDict); + traverse(queueInfo.queues.queue[i], queueDict); } } } + traverse(queueInfo, queues); return queues; } @@ -66,7 +57,7 @@ class VirtualCluster { .end((res) => { try { const resJson = typeof res.body === 'object' ? - res.body : JSON.parse(res.body); + res.body : JSON.parse(res.body); const schedulerInfo = resJson.scheduler.schedulerInfo; if (schedulerInfo.type === 'capacityScheduler') { const vcInfo = this.getCapacitySchedulerInfo(schedulerInfo); @@ -80,6 +71,253 @@ class VirtualCluster { } }); } + + generateUpdateInfo(updateData) { + let jsonBuilder = new xml2js.Builder({rootName: 'sched-conf'}); + let data = []; + if (updateData.hasOwnProperty('pendingAdd')) { + for (let item in updateData['pendingAdd']) { + if (updateData['pendingAdd'].hasOwnProperty(item)) { + let singleQueue = { + 'queue-name': 'root.' + item, + 'params': { + 'entry': { + 'key': 'capacity', + 'value': updateData['pendingAdd'][item], + }, + }, + }; + data.push({'add-queue': singleQueue}); + } + } + } + if (updateData.hasOwnProperty('pendingUpdate')) { + for (let item in updateData['pendingUpdate']) { + if (updateData['pendingUpdate'].hasOwnProperty(item)) { + let singleQueue = { + 'queue-name': 'root.' + item, + 'params': { + 'entry': { + 'key': 'capacity', + 'value': updateData['pendingUpdate'][item], + }, + }, + }; + data.push({'update-queue': singleQueue}); + } + } + } + if (updateData.hasOwnProperty('pendingStop')) { + for (let item in updateData['pendingStop']) { + if (updateData['pendingStop'].hasOwnProperty(item)) { + let singleQueue = { + 'queue-name': 'root.' + item, + 'params': { + 'entry': { + 'key': 'state', + 'value': 'STOPPED', + }, + }, + }; + data.push({'update-queue': singleQueue}); + } + } + } + if (updateData.hasOwnProperty('pendingActive')) { + for (let item in updateData['pendingActive']) { + if (updateData['pendingActive'].hasOwnProperty(item)) { + let singleQueue = { + 'queue-name': 'root.' + item, + 'params': { + 'entry': { + 'key': 'state', + 'value': 'RUNNING', + }, + }, + }; + data.push({'update-queue': singleQueue}); + } + } + } + if (updateData.hasOwnProperty('pendingRemove')) { + for (let item in updateData['pendingRemove']) { + if (updateData['pendingRemove'].hasOwnProperty(item)) { + data.push({'remove-queue': 'root.' + item}); + } + } + } + return jsonBuilder.buildObject(data); + } + + sendUpdateInfo(updateXml, callback) { + unirest.put(yarnConfig.yarnVcUpdatePath) + .headers(yarnConfig.webserviceUpdateQueueHeaders) + .send(updateXml) + .end((res) => { + if (res.ok) { + return callback(null); + } else { + return callback(createError('Internal Server Error', 'UnknownError', res.body)); + } + }); + } + + getVc(vcName, callback) { + this.getVcList((vcList, err) => { + if (err) { + return callback(err); + } else if (!vcList) { + // Unreachable + logger.warn('list virtual clusters error, no virtual cluster found'); + } else if (!vcList.hasOwnProperty(vcName)) { + return callback(null, createError('Not Found', 'NoVirtualClusterError', `Vc ${vcName} not found`)); + } else { + return callback(vcList[vcName], null); + } + }); + } + + updateVc(vcName, capacity, callback) { + this.getVcList((vcList, err) => { + if (err) { + return callback(err); + } else if (!vcList) { + // Unreachable + logger.warn('list virtual clusters error, no virtual cluster found'); + } else { + if (!vcList.hasOwnProperty('default')) { + return callback(createError('Not Found', 'NoVirtualClusterError', `No default vc found, can't allocate quota`)); + } else { + let defaultQuotaIfUpdated = vcList['default']['capacity'] + (vcList[vcName] ? vcList[vcName]['capacity'] : 0) - capacity; + if (defaultQuotaIfUpdated < 0) { + return callback(createError('Forbidden', 'NoEnoughQuotaError', `No enough quota`)); + } + + let data = {'pendingAdd': {}, 'pendingUpdate': {}}; + if (vcList.hasOwnProperty(vcName)) { + data['pendingUpdate'][vcName] = capacity; + } else { + data['pendingAdd'][vcName] = capacity; + } + data['pendingUpdate']['default'] = defaultQuotaIfUpdated; + + // logger.debug('raw data to generate: ', data); + const vcdataXml = this.generateUpdateInfo(data); + // logger.debug('Xml send to yarn: ', vcdataXml); + this.sendUpdateInfo(vcdataXml, (err) => { + if (err) { + return callback(err); + } else { + return callback(null); + } + }); + } + } + }); + } + + stopVc(vcName, callback) { + this.getVcList((vcList, err) => { + if (err) { + return callback(err); + } else if (!vcList) { + // Unreachable + logger.warn('list virtual clusters error, no virtual cluster found'); + } else { + if (!vcList.hasOwnProperty(vcName)) { + return callback(createError('Not Found', 'NoVirtualClusterError', `Vc ${vcName} not found, can't stop`)); + } else { + let data = {'pendingStop': {}}; + data['pendingStop'][vcName] = null; + + // logger.debug('raw data to generate: ', data); + const vcdataXml = this.generateUpdateInfo(data); + // logger.debug('Xml send to yarn: ', vcdataXml); + this.sendUpdateInfo(vcdataXml, (err) => { + if (err) { + return callback(err); + } else { + return callback(null); + } + }); + } + } + }); + } + + activeVc(vcName, callback) { + this.getVcList((vcList, err) => { + if (err) { + return callback(err); + } else if (!vcList) { + // Unreachable + logger.warn('list virtual clusters error, no virtual cluster found'); + } else { + if (!vcList.hasOwnProperty(vcName)) { + return callback(createError('Not Found', 'NoVirtualClusterError', `Vc ${vcName} not found, can't active`)); + } else { + let data = {'pendingActive': {}}; + data['pendingActive'][vcName] = null; + + // logger.debug('raw data to generate: ', data); + const vcdataXml = this.generateUpdateInfo(data); + // logger.debug('Xml send to yarn: ', vcdataXml); + this.sendUpdateInfo(vcdataXml, (err) => { + if (err) { + return callback(err); + } else { + return callback(null); + } + }); + } + } + }); + } + + removeVc(vcName, callback) { + this.getVcList((vcList, err) => { + if (err) { + return callback(err); + } else if (!vcList) { + // Unreachable + logger.warn('list virtual clusters error, no virtual cluster found'); + } else { + if (!vcList.hasOwnProperty('default')) { + return callback(createError('Not Found', 'NoVirtualClusterError', `No default vc found, can't free quota`)); + } else if (!vcList.hasOwnProperty(vcName)) { + return callback(createError('Not Found', 'NoVirtualClusterError', `Can't delete a nonexistent vc ${vcName}`)); + } else { + this.stopVc(vcName, (err) => { + if (err) { + return callback(err); + } else { + let defaultQuotaIfUpdated = vcList['default']['capacity'] + vcList[vcName]['capacity']; + let data = {'pendingRemove': {}, 'pendingUpdate': {}}; + data['pendingUpdate'][vcName] = 0; + data['pendingUpdate']['default'] = defaultQuotaIfUpdated; + data['pendingRemove'][vcName] = null; + // logger.debug('Raw data to generate: ', data); + const vcdataXml = this.generateUpdateInfo(data); + // logger.debug('Xml send to yarn: ', vcdataXml); + this.sendUpdateInfo(vcdataXml, (err) => { + if (err) { + this.activeVc(vcName, (errInfo) => { + if (errInfo) { + return callback(errInfo); + } else { + return callback(err); + } + }); + } else { + return callback(null); + } + }); + } + }); + } + } + }); + } } // module exports diff --git a/src/rest-server/src/routes/vc.js b/src/rest-server/src/routes/vc.js index ca68b1537e..8900aee636 100644 --- a/src/rest-server/src/routes/vc.js +++ b/src/rest-server/src/routes/vc.js @@ -16,6 +16,9 @@ // module dependencies const express = require('express'); const vcController = require('../controllers/vc'); +const token = require('../middlewares/token'); +const param = require('../middlewares/parameter'); +const vcConfig = require('../config/vc'); const router = new express.Router(); @@ -23,12 +26,22 @@ router.route('/') /** GET /api/v1/virtual-clusters - Return cluster virtual cluster info */ .get(vcController.list); + router.route('/:vcName') /** GET /api/v1/virtual-clusters/vcName - Return cluster specified virtual cluster info */ - .get(vcController.get); + .get(vcController.get) + /** PUT /api/v1/virtual-clusters/vcName - Create a vc */ + .put(token.check, param.validate(vcConfig.vcPutInputSchema), vcController.update) + /** DELETE /api/v1/virtual-clusters/vcName - Remove a vc */ + .delete(token.check, vcController.remove); + + +router.route('/:vcName/status') + /** PUT /api/v1/virtual-clusters/vcName - Change vc status (running or stopped) */ + .put(token.check, param.validate(vcConfig.vcStatusPutInputSchema), vcController.updateStatus); + -/** Load virtual cluster when API with vcName route parameter is hit */ -router.param('vcName', vcController.load); +router.param('vcName', vcController.validate); // module exports module.exports = router; diff --git a/src/rest-server/src/util/error.d.ts b/src/rest-server/src/util/error.d.ts index 5e91e5239c..963d4e1281 100644 --- a/src/rest-server/src/util/error.d.ts +++ b/src/rest-server/src/util/error.d.ts @@ -29,6 +29,7 @@ declare type Code = 'BadConfigurationError' | 'ConflictJobError' | 'ConflictUserError' | + 'ConflictVcError' | 'ForbiddenUserError' | 'IncorrectPasswordError' | 'InvalidParametersError' | @@ -41,6 +42,7 @@ declare type Code = 'ReadOnlyJobError' | 'RemoveAdminError' | 'UnauthorizedUserError' | + 'NoEnoughQuotaError' | 'UnknownError'; declare function createError(status: Status, code: Code, message: string): HttpError; diff --git a/src/rest-server/test/jobSubmission.js b/src/rest-server/test/jobSubmission.js index 1fdf854bae..b5845b2687 100644 --- a/src/rest-server/test/jobSubmission.js +++ b/src/rest-server/test/jobSubmission.js @@ -126,16 +126,24 @@ describe('Submit job: POST /api/v1/user/:username/jobs', () => { 'queueName': 'default', 'state': 'RUNNING', 'type': 'capacitySchedulerLeafQueueInfo', + "absoluteCapacity": 30.000002, + "absoluteMaxCapacity": 100, }, { 'queueName': 'vc1', 'state': 'RUNNING', 'type': 'capacitySchedulerLeafQueueInfo', + "capacity": 50.000002, + "absoluteCapacity": 0, + "absoluteMaxCapacity": 100, }, { 'queueName': 'vc2', 'state': 'RUNNING', 'type': 'capacitySchedulerLeafQueueInfo', + "capacity": 19.999996, + "absoluteCapacity": 0, + "absoluteMaxCapacity": 100, } ] }, @@ -165,16 +173,24 @@ describe('Submit job: POST /api/v1/user/:username/jobs', () => { 'queueName': 'default', 'state': 'RUNNING', 'type': 'capacitySchedulerLeafQueueInfo', + "absoluteCapacity": 30.000002, + "absoluteMaxCapacity": 100, }, { 'queueName': 'vc1', 'state': 'RUNNING', 'type': 'capacitySchedulerLeafQueueInfo', + "capacity": 50.000002, + "absoluteCapacity": 0, + "absoluteMaxCapacity": 100, }, { 'queueName': 'vc2', 'state': 'RUNNING', 'type': 'capacitySchedulerLeafQueueInfo', + "capacity": 19.999996, + "absoluteCapacity": 0, + "absoluteMaxCapacity": 100, } ] }, @@ -208,35 +224,43 @@ describe('Submit job: POST /api/v1/user/:username/jobs', () => { {} ); - nock(yarnUri) - .get('/ws/v1/cluster/scheduler') - .reply(200, { - 'scheduler': { - 'schedulerInfo': { - 'queues': { - 'queue': [ - { - 'queueName': 'default', - 'state': 'RUNNING', - 'type': 'capacitySchedulerLeafQueueInfo', - }, - { - 'queueName': 'vc1', - 'state': 'RUNNING', - 'type': 'capacitySchedulerLeafQueueInfo', - }, - { - 'queueName': 'vc2', - 'state': 'RUNNING', - 'type': 'capacitySchedulerLeafQueueInfo', - } - ] - }, - 'type': 'capacityScheduler', - 'usedCapacity': 0.0 - } + nock(yarnUri) + .get('/ws/v1/cluster/scheduler') + .reply(200, { + 'scheduler': { + 'schedulerInfo': { + 'queues': { + 'queue': [ + { + 'queueName': 'default', + 'state': 'RUNNING', + 'type': 'capacitySchedulerLeafQueueInfo', + "absoluteCapacity": 30.000002, + "absoluteMaxCapacity": 100, + }, + { + 'queueName': 'vc1', + 'state': 'RUNNING', + 'type': 'capacitySchedulerLeafQueueInfo', + "capacity": 50.000002, + "absoluteCapacity": 0, + "absoluteMaxCapacity": 100, + }, + { + 'queueName': 'vc2', + 'state': 'RUNNING', + 'type': 'capacitySchedulerLeafQueueInfo', + "capacity": 19.999996, + "absoluteCapacity": 0, + "absoluteMaxCapacity": 100, + } + ] + }, + 'type': 'capacityScheduler', + 'usedCapacity': 0.0 } - }); + } + }); // // Mock etcd return result diff --git a/src/rest-server/test/userManagement.js b/src/rest-server/test/userManagement.js index ec88f7d794..2e274476e1 100644 --- a/src/rest-server/test/userManagement.js +++ b/src/rest-server/test/userManagement.js @@ -100,16 +100,24 @@ describe('Add new user: put /api/v1/user', () => { 'queueName': 'default', 'state': 'RUNNING', 'type': 'capacitySchedulerLeafQueueInfo', + "absoluteCapacity": 30.000002, + "absoluteMaxCapacity": 100, }, { 'queueName': 'vc1', 'state': 'RUNNING', 'type': 'capacitySchedulerLeafQueueInfo', + "capacity": 50.000002, + "absoluteCapacity": 0, + "absoluteMaxCapacity": 100, }, { 'queueName': 'vc2', 'state': 'RUNNING', 'type': 'capacitySchedulerLeafQueueInfo', + "capacity": 19.999996, + "absoluteCapacity": 0, + "absoluteMaxCapacity": 100, } ] }, @@ -587,16 +595,24 @@ describe('update user virtual cluster : put /api/v1/user/:username/virtualCluste 'queueName': 'default', 'state': 'RUNNING', 'type': 'capacitySchedulerLeafQueueInfo', + "absoluteCapacity": 30.000002, + "absoluteMaxCapacity": 100, }, { 'queueName': 'vc1', 'state': 'RUNNING', 'type': 'capacitySchedulerLeafQueueInfo', + "capacity": 50.000002, + "absoluteCapacity": 0, + "absoluteMaxCapacity": 100, }, { 'queueName': 'vc2', 'state': 'RUNNING', 'type': 'capacitySchedulerLeafQueueInfo', + "capacity": 19.999996, + "absoluteCapacity": 0, + "absoluteMaxCapacity": 100, } ] }, diff --git a/src/rest-server/test/vc.js b/src/rest-server/test/vc.js index bb5be39515..de94dbdaf3 100644 --- a/src/rest-server/test/vc.js +++ b/src/rest-server/test/vc.js @@ -15,270 +15,546 @@ // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -// test -describe('VC API /api/v1/virtual-clusters', () => { - // Mock yarn rest api - beforeEach(() => { - nock(yarnUri) - .get('/ws/v1/cluster/scheduler') - .reply(200, { - "scheduler": { - "schedulerInfo": { - "capacity": 100.0, - "maxCapacity": 100.0, + +const yarnDefaultResponse = { + "scheduler": { + "schedulerInfo": { + "type": "capacityScheduler", + "capacity": 100, + "usedCapacity": 0, + "maxCapacity": 100, "queueName": "root", "queues": { - "queue": [ - { - "absoluteCapacity": 10.5, - "absoluteMaxCapacity": 50.0, - "absoluteUsedCapacity": 0.0, - "capacity": 10.5, - "maxCapacity": 50.0, - "numApplications": 0, - "queueName": "a", - "queues": { - "queue": [ - { - "absoluteCapacity": 3.15, - "absoluteMaxCapacity": 25.0, - "absoluteUsedCapacity": 0.0, + "queue": [ + { + "type": "capacitySchedulerLeafQueueInfo", "capacity": 30.000002, - "maxCapacity": 50.0, + "usedCapacity": 0, + "maxCapacity": 100, + "absoluteCapacity": 30.000002, + "absoluteMaxCapacity": 100, + "absoluteUsedCapacity": 0, "numApplications": 0, - "queueName": "a1", - "queues": { - "queue": [ - { - "absoluteCapacity": 2.6775, - "absoluteMaxCapacity": 25.0, - "absoluteUsedCapacity": 0.0, - "capacity": 85.0, - "maxActiveApplications": 1, - "maxActiveApplicationsPerUser": 1, - "maxApplications": 267, - "maxApplicationsPerUser": 267, - "maxCapacity": 100.0, - "numActiveApplications": 0, - "numApplications": 0, - "numContainers": 0, - "numPendingApplications": 0, - "queueName": "a1a", - "resourcesUsed": { - "memory": 0, - "vCores": 0 - }, - "state": "RUNNING", - "type": "capacitySchedulerLeafQueueInfo", - "usedCapacity": 0.0, - "usedResources": "", - "userLimit": 100, - "userLimitFactor": 1.0, - "users": null - }, - { - "absoluteCapacity": 0.47250003, - "absoluteMaxCapacity": 25.0, - "absoluteUsedCapacity": 0.0, - "capacity": 15.000001, - "maxActiveApplications": 1, - "maxActiveApplicationsPerUser": 1, - "maxApplications": 47, - "maxApplicationsPerUser": 47, - "maxCapacity": 100.0, - "numActiveApplications": 0, - "numApplications": 0, - "numContainers": 0, - "numPendingApplications": 0, - "queueName": "a1b", - "resourcesUsed": { - "memory": 0, - "vCores": 0 - }, - "state": "RUNNING", - "type": "capacitySchedulerLeafQueueInfo", - "usedCapacity": 0.0, - "usedResources": "", - "userLimit": 100, - "userLimitFactor": 1.0, - "users": null - } - ] - }, + "queueName": "a", + "state": "RUNNING", "resourcesUsed": { - "memory": 0, - "vCores": 0 + "memory": 0, + "vCores": 0, + "GPUs": 0 + }, + "hideReservationQueues": false, + "nodeLabels": [ + "*" + ], + "allocatedContainers": 0, + "reservedContainers": 0, + "pendingContainers": 0, + "capacities": { + "queueCapacitiesByPartition": [ + { + "partitionName": "", + "capacity": 30.000002, + "usedCapacity": 0, + "maxCapacity": 100, + "absoluteCapacity": 30.000002, + "absoluteUsedCapacity": 0, + "absoluteMaxCapacity": 100, + "maxAMLimitPercentage": 100 + } + ] + }, + "resources": { + "resourceUsagesByPartition": [ + { + "partitionName": "", + "used": { + "memory": 0, + "vCores": 0, + "GPUs": 0 + }, + "reserved": { + "memory": 0, + "vCores": 0, + "GPUs": 0 + }, + "pending": { + "memory": 0, + "vCores": 0, + "GPUs": 0 + }, + "amUsed": { + "memory": 0, + "vCores": 0, + "GPUs": 0 + }, + "amLimit": { + "memory": 15360, + "vCores": 8, + "GPUs": 0 + } + } + ] }, - "state": "RUNNING", - "usedCapacity": 0.0, - "usedResources": "" - }, - { - "absoluteCapacity": 7.35, - "absoluteMaxCapacity": 50.0, - "absoluteUsedCapacity": 0.0, - "capacity": 70.0, - "maxActiveApplications": 1, - "maxActiveApplicationsPerUser": 100, - "maxApplications": 735, - "maxApplicationsPerUser": 73500, - "maxCapacity": 100.0, "numActiveApplications": 0, - "numApplications": 0, - "numContainers": 0, "numPendingApplications": 0, - "queueName": "a2", - "resourcesUsed": { + "numContainers": 0, + "maxApplications": 3000, + "maxApplicationsPerUser": 3000, + "userLimit": 100, + "users": null, + "userLimitFactor": 1, + "AMResourceLimit": { + "memory": 15360, + "vCores": 8, + "GPUs": 0 + }, + "usedAMResource": { "memory": 0, - "vCores": 0 + "vCores": 0, + "GPUs": 0 }, - "state": "RUNNING", + "userAMResourceLimit": { + "memory": 15360, + "vCores": 8, + "GPUs": 0 + }, + "preemptionDisabled": false, + "defaultPriority": 0 + }, + { "type": "capacitySchedulerLeafQueueInfo", - "usedCapacity": 0.0, - "usedResources": "", + "capacity": 70, + "usedCapacity": 0, + "maxCapacity": 100, + "absoluteCapacity": 70, + "absoluteMaxCapacity": 100, + "absoluteUsedCapacity": 0, + "numApplications": 0, + "queueName": "default", + "state": "RUNNING", + "resourcesUsed": { + "memory": 0, + "vCores": 0, + "GPUs": 0 + }, + "hideReservationQueues": false, + "nodeLabels": [ + "*" + ], + "allocatedContainers": 0, + "reservedContainers": 0, + "pendingContainers": 0, + "capacities": { + "queueCapacitiesByPartition": [ + { + "partitionName": "", + "capacity": 70, + "usedCapacity": 0, + "maxCapacity": 100, + "absoluteCapacity": 70, + "absoluteUsedCapacity": 0, + "absoluteMaxCapacity": 100, + "maxAMLimitPercentage": 100 + } + ] + }, + "resources": { + "resourceUsagesByPartition": [ + { + "partitionName": "", + "used": { + "memory": 0, + "vCores": 0, + "GPUs": 0 + }, + "reserved": { + "memory": 0, + "vCores": 0, + "GPUs": 0 + }, + "pending": { + "memory": 0, + "vCores": 0, + "GPUs": 0 + }, + "amUsed": { + "memory": 0, + "vCores": 0, + "GPUs": 0 + }, + "amLimit": { + "memory": 15360, + "vCores": 8, + "GPUs": 0 + } + } + ] + }, + "numActiveApplications": 0, + "numPendingApplications": 0, + "numContainers": 0, + "maxApplications": 7000, + "maxApplicationsPerUser": 7000, "userLimit": 100, - "userLimitFactor": 100.0, - "users": null + "users": null, + "userLimitFactor": 100, + "AMResourceLimit": { + "memory": 15360, + "vCores": 8, + "GPUs": 0 + }, + "usedAMResource": { + "memory": 0, + "vCores": 0, + "GPUs": 0 + }, + "userAMResourceLimit": { + "memory": 15360, + "vCores": 8, + "GPUs": 0 + }, + "preemptionDisabled": false, + "defaultPriority": 0 } ] - }, - "resourcesUsed": { - "memory": 0, - "vCores": 0 - }, - "state": "RUNNING", - "usedCapacity": 0.0, - "usedResources": "" + }, + "capacities": { + "queueCapacitiesByPartition": [ + { + "partitionName": "", + "capacity": 100, + "usedCapacity": 0, + "maxCapacity": 100, + "absoluteCapacity": 100, + "absoluteUsedCapacity": 0, + "absoluteMaxCapacity": 100, + "maxAMLimitPercentage": 0 + } + ] + }, + "health": { + "lastrun": 1543912751445, + "operationsInfo": { + "entry": { + "key": "last-release", + "value": { + "nodeId": "N/A", + "containerId": "N/A", + "queue": "N/A" + } + } }, - { - "absoluteCapacity": 89.5, - "absoluteMaxCapacity": 100.0, - "absoluteUsedCapacity": 0.0, - "capacity": 89.5, - "maxCapacity": 100.0, - "numApplications": 2, - "queueName": "b", - "queues": { - "queue": [ - { - "absoluteCapacity": 53.7, - "absoluteMaxCapacity": 100.0, - "absoluteUsedCapacity": 0.0, - "capacity": 60.000004, - "maxActiveApplications": 1, - "maxActiveApplicationsPerUser": 100, - "maxApplications": 5370, - "maxApplicationsPerUser": 537000, - "maxCapacity": 100.0, - "numActiveApplications": 1, - "numApplications": 2, - "numContainers": 0, - "numPendingApplications": 1, - "queueName": "b1", - "resourcesUsed": { - "memory": 0, - "vCores": 0 - }, - "state": "RUNNING", - "type": "capacitySchedulerLeafQueueInfo", - "usedCapacity": 0.0, - "usedResources": "", - "userLimit": 100, - "userLimitFactor": 100.0, - "users": { - "user": [ - { - "numActiveApplications": 0, - "numPendingApplications": 1, - "resourcesUsed": { - "memory": 0, - "vCores": 0 - }, - "username": "user2" - }, - { - "numActiveApplications": 1, - "numPendingApplications": 0, - "resourcesUsed": { - "memory": 0, - "vCores": 0 - }, - "username": "user1" - } - ] + "lastRunDetails": [ + { + "operation": "releases", + "count": 0, + "resources": { + "memory": 0, + "vCores": 0, + "GPUs": 0 + } + }, + { + "operation": "allocations", + "count": 0, + "resources": { + "memory": 0, + "vCores": 0, + "GPUs": 0 } }, - { - "absoluteCapacity": 35.3525, - "absoluteMaxCapacity": 100.0, - "absoluteUsedCapacity": 0.0, - "capacity": 39.5, - "maxActiveApplications": 1, - "maxActiveApplicationsPerUser": 100, - "maxApplications": 3535, - "maxApplicationsPerUser": 353500, - "maxCapacity": 100.0, - "numActiveApplications": 123, + { + "operation": "reservations", + "count": 0, + "resources": { + "memory": 0, + "vCores": 0, + "GPUs": 0 + } + } + ] + } + } + } +}; + +const yarnErrorResponse = { + "scheduler": { + "schedulerInfo": { + "type": "capacityScheduler", + "capacity": 100, + "usedCapacity": 0, + "maxCapacity": 100, + "queueName": "root", + "queues": { + "queue": [ + { + "type": "capacitySchedulerLeafQueueInfo", + "capacity": 30.000002, + "usedCapacity": 0, + "maxCapacity": 100, + "absoluteCapacity": 30.000002, + "absoluteMaxCapacity": 100, + "absoluteUsedCapacity": 0, "numApplications": 0, - "numContainers": 0, - "numPendingApplications": 0, - "queueName": "b2", + "queueName": "a", + "state": "RUNNING", "resourcesUsed": { - "memory": 0, - "vCores": 0 + "memory": 0, + "vCores": 0, + "GPUs": 0 + }, + "hideReservationQueues": false, + "nodeLabels": [ + "*" + ], + "allocatedContainers": 0, + "reservedContainers": 0, + "pendingContainers": 0, + "capacities": { + "queueCapacitiesByPartition": [ + { + "partitionName": "", + "capacity": 30.000002, + "usedCapacity": 0, + "maxCapacity": 100, + "absoluteCapacity": 30.000002, + "absoluteUsedCapacity": 0, + "absoluteMaxCapacity": 100, + "maxAMLimitPercentage": 100 + } + ] + }, + "resources": { + "resourceUsagesByPartition": [ + { + "partitionName": "", + "used": { + "memory": 0, + "vCores": 0, + "GPUs": 0 + }, + "reserved": { + "memory": 0, + "vCores": 0, + "GPUs": 0 + }, + "pending": { + "memory": 0, + "vCores": 0, + "GPUs": 0 + }, + "amUsed": { + "memory": 0, + "vCores": 0, + "GPUs": 0 + }, + "amLimit": { + "memory": 15360, + "vCores": 8, + "GPUs": 0 + } + } + ] }, - "state": "RUNNING", - "type": "capacitySchedulerLeafQueueInfo", - "usedCapacity": 0.0, - "usedResources": "", - "userLimit": 100, - "userLimitFactor": 100.0, - "users": null - }, - { - "absoluteCapacity": 0.4475, - "absoluteMaxCapacity": 100.0, - "absoluteUsedCapacity": 0.0, - "capacity": 0.5, - "maxActiveApplications": 1, - "maxActiveApplicationsPerUser": 100, - "maxApplications": 44, - "maxApplicationsPerUser": 4400, - "maxCapacity": 100.0, "numActiveApplications": 0, - "numApplications": 0, - "numContainers": 0, "numPendingApplications": 0, - "queueName": "b3", - "resourcesUsed": { - "memory": 0, - "vCores": 0 + "numContainers": 0, + "maxApplications": 3000, + "maxApplicationsPerUser": 3000, + "userLimit": 100, + "users": null, + "userLimitFactor": 1, + "AMResourceLimit": { + "memory": 15360, + "vCores": 8, + "GPUs": 0 }, - "state": "RUNNING", + "usedAMResource": { + "memory": 0, + "vCores": 0, + "GPUs": 0 + }, + "userAMResourceLimit": { + "memory": 15360, + "vCores": 8, + "GPUs": 0 + }, + "preemptionDisabled": false, + "defaultPriority": 0 + }, + { "type": "capacitySchedulerLeafQueueInfo", - "usedCapacity": 0.0, - "usedResources": "", + "capacity": 70, + "usedCapacity": 0, + "maxCapacity": 100, + "absoluteCapacity": 70, + "absoluteMaxCapacity": 100, + "absoluteUsedCapacity": 0, + "numApplications": 0, + "queueName": "b", + "state": "RUNNING", + "resourcesUsed": { + "memory": 0, + "vCores": 0, + "GPUs": 0 + }, + "hideReservationQueues": false, + "nodeLabels": [ + "*" + ], + "allocatedContainers": 0, + "reservedContainers": 0, + "pendingContainers": 0, + "capacities": { + "queueCapacitiesByPartition": [ + { + "partitionName": "", + "capacity": 70, + "usedCapacity": 0, + "maxCapacity": 100, + "absoluteCapacity": 70, + "absoluteUsedCapacity": 0, + "absoluteMaxCapacity": 100, + "maxAMLimitPercentage": 100 + } + ] + }, + "resources": { + "resourceUsagesByPartition": [ + { + "partitionName": "", + "used": { + "memory": 0, + "vCores": 0, + "GPUs": 0 + }, + "reserved": { + "memory": 0, + "vCores": 0, + "GPUs": 0 + }, + "pending": { + "memory": 0, + "vCores": 0, + "GPUs": 0 + }, + "amUsed": { + "memory": 0, + "vCores": 0, + "GPUs": 0 + }, + "amLimit": { + "memory": 15360, + "vCores": 8, + "GPUs": 0 + } + } + ] + }, + "numActiveApplications": 0, + "numPendingApplications": 0, + "numContainers": 0, + "maxApplications": 7000, + "maxApplicationsPerUser": 7000, "userLimit": 100, - "userLimitFactor": 100.0, - "users": null - } - ] - }, - "resourcesUsed": { - "memory": 0, - "vCores": 0 - }, - "state": "RUNNING", - "usedCapacity": 0.0, - "usedResources": "" - } - ] + "users": null, + "userLimitFactor": 100, + "AMResourceLimit": { + "memory": 15360, + "vCores": 8, + "GPUs": 0 + }, + "usedAMResource": { + "memory": 0, + "vCores": 0, + "GPUs": 0 + }, + "userAMResourceLimit": { + "memory": 15360, + "vCores": 8, + "GPUs": 0 + }, + "preemptionDisabled": false, + "defaultPriority": 0 + } + ] }, - "type": "capacityScheduler", - "usedCapacity": 0.0 - } + "capacities": { + "queueCapacitiesByPartition": [ + { + "partitionName": "", + "capacity": 100, + "usedCapacity": 0, + "maxCapacity": 100, + "absoluteCapacity": 100, + "absoluteUsedCapacity": 0, + "absoluteMaxCapacity": 100, + "maxAMLimitPercentage": 0 + } + ] + }, + "health": { + "lastrun": 1543912751445, + "operationsInfo": { + "entry": { + "key": "last-release", + "value": { + "nodeId": "N/A", + "containerId": "N/A", + "queue": "N/A" + } + } + }, + "lastRunDetails": [ + { + "operation": "releases", + "count": 0, + "resources": { + "memory": 0, + "vCores": 0, + "GPUs": 0 + } + }, + { + "operation": "allocations", + "count": 0, + "resources": { + "memory": 0, + "vCores": 0, + "GPUs": 0 + } + }, + { + "operation": "reservations", + "count": 0, + "resources": { + "memory": 0, + "vCores": 0, + "GPUs": 0 + } + } + ] + } } - }); + } +}; + + +// test +describe('VC API Get /api/v1/virtual-clusters', () => { + // Mock yarn rest api + beforeEach(() => { + nock(yarnUri) + .get('/ws/v1/cluster/scheduler') + .reply(200, yarnDefaultResponse); + }); + + afterEach(function() { + if (!nock.isDone()) { + nock.cleanAll(); + throw new Error('Not all nock interceptors were used!'); + } }); // GET /api/v1/virtual-clusters @@ -288,8 +564,8 @@ describe('VC API /api/v1/virtual-clusters', () => { .end((err, res) => { expect(res, 'status code').to.have.status(200); expect(res, 'json response').be.json; - expect(res.body).to.have.property('a1a'); - expect(res.body).to.nested.include({'b2.numActiveJobs': 123}); + expect(res.body).to.have.property('a'); + expect(res.body).to.nested.include({'default.status': "RUNNING"}); done(); }); }); @@ -298,11 +574,11 @@ describe('VC API /api/v1/virtual-clusters', () => { // get exist vc info it('should return virtual cluster info', (done) => { chai.request(server) - .get('/api/v1/virtual-clusters/b3') + .get('/api/v1/virtual-clusters/a') .end((err, res) => { expect(res, 'status code').to.have.status(200); expect(res, 'json response').be.json; - expect(res.body).to.have.property('capacity', 0.4475); + expect(res.body).to.have.property('capacity', 30); done(); }); }); @@ -355,3 +631,410 @@ describe('VC API /api/v1/virtual-clusters', () => { }); }); }); + + +describe('VC API PUT /api/v1/virtual-clusters', () => { + + const userToken = jwt.sign({username: 'test_user', admin: false}, process.env.JWT_SECRET, {expiresIn: 60}); + const adminToken = jwt.sign({username: 'test_admin', admin: true}, process.env.JWT_SECRET, {expiresIn: 60}); + // Mock yarn rest api + beforeEach(() => { + nock.cleanAll(); + nock(yarnUri) + .get('/ws/v1/cluster/scheduler') + .reply(200, yarnDefaultResponse); + }); + + + it('[Positive] should add vc b', (done) => { + nock(yarnUri) + .put('/ws/v1/cluster/scheduler-conf') + .reply(200); + + chai.request(server) + .put('/api/v1/virtual-clusters/b') + .set('Authorization', `Bearer ${adminToken}`) + .send({ + 'vcCapacity': 30 + }) + .end((err, res) => { + expect(res, 'status code').to.have.status(201); + done(); + }); + }); + + it('[Positive] should update vc a', (done) => { + nock(yarnUri) + .put('/ws/v1/cluster/scheduler-conf') + .reply(200); + + chai.request(server) + .put('/api/v1/virtual-clusters/a') + .set('Authorization', `Bearer ${adminToken}`) + .send({ + 'vcCapacity': 40 + }) + .end((err, res) => { + expect(res, 'status code').to.have.status(201); + done(); + }); + }); + + + it('[Negative] should not update vc default', (done) => { + chai.request(server) + .put('/api/v1/virtual-clusters/default') + .set('Authorization', `Bearer ${adminToken}`) + .send({ + 'vcCapacity': 30 + }) + .end((err, res) => { + expect(res, 'status code').to.have.status(403); + expect(res.body).to.have.property('code', 'ForbiddenUserError'); + done(); + }); + }); + + it('[Negative] Non-admin should not update vc', (done) => { + chai.request(server) + .put('/api/v1/virtual-clusters/b') + .set('Authorization', `Bearer ${userToken}`) + .send({ + 'vcCapacity': 30 + }) + .end((err, res) => { + expect(res, 'status code').to.have.status(403); + expect(res.body).to.have.property('code', 'ForbiddenUserError'); + done(); + }); + }); + + it('[Negative] Non-admin should not update vc', (done) => { + chai.request(server) + .put('/api/v1/virtual-clusters/b') + .set('Authorization', `Bearer ${userToken}`) + .send({ + 'vcCapacity': 30 + }) + .end((err, res) => { + expect(res, 'status code').to.have.status(403); + expect(res.body).to.have.property('code', 'ForbiddenUserError'); + done(); + }); + }); + + it('[Negative] should not update vc b with exceed quota', (done) => { + chai.request(server) + .put('/api/v1/virtual-clusters/b') + .set('Authorization', `Bearer ${adminToken}`) + .send({ + 'vcCapacity': 80 + }) + .end((err, res) => { + expect(res, 'status code').to.have.status(403); + expect(res.body).to.have.property('code', 'NoEnoughQuotaError'); + done(); + }); + }); + + it('[Negative] should not update vc if default vc doesn\'t exist', (done) => { + nock.cleanAll(); + nock(yarnUri) + .get('/ws/v1/cluster/scheduler') + .reply(200, yarnErrorResponse); + + chai.request(server) + .put('/api/v1/virtual-clusters/a') + .set('Authorization', `Bearer ${adminToken}`) + .send({ + 'vcCapacity': 80 + }) + .end((err, res) => { + expect(res, 'status code').to.have.status(404); + expect(res.body).to.have.property('code', 'NoVirtualClusterError'); + done(); + }); + }); + + it('[Negative] should not update vc if upstream error', (done) => { + nock(yarnUri) + .put('/ws/v1/cluster/scheduler-conf') + .reply(404, 'Error response in YARN'); + + chai.request(server) + .put('/api/v1/virtual-clusters/a') + .set('Authorization', `Bearer ${adminToken}`) + .send({ + 'vcCapacity': 80 + }) + .end((err, res) => { + expect(res, 'status code').to.have.status(500); + expect(res.body).to.have.property('code', 'UnknownError'); + expect(res.body).to.have.property('message', 'Error response in YARN'); + done(); + }); + }); +}); + + +describe('VC API PUT /api/v1/virtual-clusters/:vcName/status', () => { + + const userToken = jwt.sign({username: 'test_user', admin: false}, process.env.JWT_SECRET, {expiresIn: 60}); + const adminToken = jwt.sign({username: 'test_admin', admin: true}, process.env.JWT_SECRET, {expiresIn: 60}); + // Mock yarn rest api + beforeEach(() => { + nock.cleanAll(); + nock(yarnUri) + .get('/ws/v1/cluster/scheduler') + .reply(200, yarnDefaultResponse); + }); + + + it('[Positive] should change vc a to stopped', (done) => { + nock(yarnUri) + .put('/ws/v1/cluster/scheduler-conf') + .reply(200); + + chai.request(server) + .put('/api/v1/virtual-clusters/a/status') + .set('Authorization', `Bearer ${adminToken}`) + .send({ + 'vcStatus': "stopped" + }) + .end((err, res) => { + expect(res, 'status code').to.have.status(201); + done(); + }); + }); + + it('[Positive] should change vc a to running', (done) => { + nock(yarnUri) + .put('/ws/v1/cluster/scheduler-conf') + .reply(200); + + chai.request(server) + .put('/api/v1/virtual-clusters/a/status') + .set('Authorization', `Bearer ${adminToken}`) + .send({ + 'vcStatus': "running" + }) + .end((err, res) => { + expect(res, 'status code').to.have.status(201); + done(); + }); + }); + + it('[Negative] should not change default vc status', (done) => { + nock(yarnUri) + .put('/ws/v1/cluster/scheduler-conf') + .reply(200); + + chai.request(server) + .put('/api/v1/virtual-clusters/default/status') + .set('Authorization', `Bearer ${adminToken}`) + .send({ + 'vcStatus': "running" + }) + .end((err, res) => { + expect(res, 'status code').to.have.status(403); + expect(res.body).to.have.property('code', 'ForbiddenUserError'); + done(); + }); + }); + + it('[Negative] Non-admin should not change vc status', (done) => { + nock(yarnUri) + .put('/ws/v1/cluster/scheduler-conf') + .reply(200); + + chai.request(server) + .put('/api/v1/virtual-clusters/a/status') + .set('Authorization', `Bearer ${userToken}`) + .send({ + 'vcStatus': "stopped" + }) + .end((err, res) => { + expect(res, 'status code').to.have.status(403); + expect(res.body).to.have.property('code', 'ForbiddenUserError'); + done(); + }); + }); + + it('[Negative] should not change a non-exist vc b', (done) => { + nock(yarnUri) + .put('/ws/v1/cluster/scheduler-conf') + .reply(200); + + chai.request(server) + .put('/api/v1/virtual-clusters/b/status') + .set('Authorization', `Bearer ${adminToken}`) + .send({ + 'vcStatus': 'running' + }) + .end((err, res) => { + expect(res, 'status code').to.have.status(404); + expect(res.body).to.have.property('code', 'NoVirtualClusterError'); + done(); + }); + }); + + + it('[Negative] should not change vc if upstream error', (done) => { + nock(yarnUri) + .put('/ws/v1/cluster/scheduler-conf') + .reply(404, 'Error response in YARN'); + + chai.request(server) + .put('/api/v1/virtual-clusters/a/status') + .set('Authorization', `Bearer ${adminToken}`) + .send({ + 'vcStatus': 'stopped' + }) + .end((err, res) => { + expect(res, 'status code').to.have.status(500); + expect(res.body).to.have.property('code', 'UnknownError'); + expect(res.body).to.have.property('message', 'Error response in YARN'); + done(); + }); + }); +}); + + +describe('VC API DELETE /api/v1/virtual-clusters', () => { + + const userToken = jwt.sign({username: 'test_user', admin: false}, process.env.JWT_SECRET, {expiresIn: 60}); + const adminToken = jwt.sign({username: 'test_admin', admin: true}, process.env.JWT_SECRET, {expiresIn: 60}); + // Mock yarn rest api + before(() => { + nock.cleanAll(); + nock(yarnUri) + .get('/ws/v1/cluster/scheduler') + .reply(200, yarnDefaultResponse); + }); + + beforeEach(() => { + nock(yarnUri) + .get('/ws/v1/cluster/scheduler') + .reply(200, yarnDefaultResponse); + }); + + afterEach(() => { + nock.cleanAll(); + }); + + + it('[Positive] should delete vc a', (done) => { + nock(yarnUri) + .put('/ws/v1/cluster/scheduler-conf') + .reply(200) + .put('/ws/v1/cluster/scheduler-conf') + .reply(200); + + chai.request(server) + .delete('/api/v1/virtual-clusters/a') + .set('Authorization', `Bearer ${adminToken}`) + .end((err, res) => { + expect(res, 'status code').to.have.status(201); + done(); + }); + }); + + it('[Negative] Non-admin should not delete vc a', (done) => { + nock(yarnUri) + .put('/ws/v1/cluster/scheduler-conf') + .reply(200) + .put('/ws/v1/cluster/scheduler-conf') + .reply(200); + + chai.request(server) + .delete('/api/v1/virtual-clusters/a') + .set('Authorization', `Bearer ${userToken}`) + .end((err, res) => { + expect(res, 'status code').to.have.status(403); + expect(res.body).to.have.property('code', 'ForbiddenUserError'); + done(); + }); + }); + + it('[Negative] should not delete vc default', (done) => { + nock(yarnUri) + .put('/ws/v1/cluster/scheduler-conf') + .reply(200) + .put('/ws/v1/cluster/scheduler-conf') + .reply(200); + + chai.request(server) + .delete('/api/v1/virtual-clusters/default') + .set('Authorization', `Bearer ${adminToken}`) + .end((err, res) => { + expect(res, 'status code').to.have.status(403); + expect(res.body).to.have.property('code', 'ForbiddenUserError'); + done(); + }); + }); + + it('[Negative] should not delete a non-exist vc b', (done) => { + nock(yarnUri) + .put('/ws/v1/cluster/scheduler-conf') + .reply(200) + .put('/ws/v1/cluster/scheduler-conf') + .reply(200); + + chai.request(server) + .delete('/api/v1/virtual-clusters/b') + .set('Authorization', `Bearer ${adminToken}`) + .end((err, res) => { + expect(res, 'status code').to.have.status(404); + expect(res.body).to.have.property('code', 'NoVirtualClusterError'); + done(); + }); + }); + + it('[Negative] should not delete a vc when stop queue successfully but fail to delete', (done) => { + nock(yarnUri) + .get('/ws/v1/cluster/scheduler') + .reply(200, yarnDefaultResponse) + .get('/ws/v1/cluster/scheduler') + .reply(200, yarnDefaultResponse) + .put('/ws/v1/cluster/scheduler-conf') + .reply(200) + .put('/ws/v1/cluster/scheduler-conf') + .reply(403, 'Error in YARN when delete queue') + .put('/ws/v1/cluster/scheduler-conf') + .reply(200); + + chai.request(server) + .delete('/api/v1/virtual-clusters/a') + .set('Authorization', `Bearer ${adminToken}`) + .end((err, res) => { + expect(res, 'status code').to.have.status(500); + expect(res.body).to.have.property('code', 'UnknownError'); + expect(res.body).to.have.property('message', 'Error in YARN when delete queue'); + done(); + }); + }); + + it('[Negative] Stop queue successfully but fail to delete, then fail to reactive it', (done) => { + nock(yarnUri) + .get('/ws/v1/cluster/scheduler') + .reply(200, yarnDefaultResponse) + .get('/ws/v1/cluster/scheduler') + .reply(200, yarnDefaultResponse) + .put('/ws/v1/cluster/scheduler-conf') + .reply(200) + .put('/ws/v1/cluster/scheduler-conf') + .reply(403, 'Error in YARN when delete queue') + .put('/ws/v1/cluster/scheduler-conf') + .reply(404, 'Error in YARN when reactive queue'); + + chai.request(server) + .delete('/api/v1/virtual-clusters/a') + .set('Authorization', `Bearer ${adminToken}`) + .end((err, res) => { + expect(res, 'status code').to.have.status(500); + expect(res.body).to.have.property('code', 'UnknownError'); + expect(res.body).to.have.property('message', 'Error in YARN when reactive queue'); + done(); + }); + }); +});