From 9463c20a9c83f14b67a52f0370402ee3c69b3f1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Tr=C4=99bski?= Date: Mon, 24 Apr 2017 14:25:16 +0200 Subject: [PATCH] Adjusting to uwsgi deployed keystone Commit moves away from using url and port separetely for keystone configuration. Instead a singular auth_uri can be specified. It still supports setting up port explicitly, however if none is provided, a default port 80 is assumed. Story: 2000995 Task: 4174 Needed-By: Ia95b3bef2734d639c6fec57484b60bc5377d659f Change-Id: I22686d05670fc6c947611f8044dea498239a4212 --- index.js | 35 ++- package.json | 6 +- server/__tests__/binding.spec.js | 45 +++- server/__tests__/healthcheck.spec.js | 318 +++++++++++++++------------ server/__tests__/util.spec.js | 69 +++++- server/binding/index.js | 10 +- server/healthcheck/index.js | 13 +- server/util/index.js | 23 +- 8 files changed, 347 insertions(+), 172 deletions(-) diff --git a/index.js b/index.js index 1d34ccf..7f4d46b 100644 --- a/index.js +++ b/index.js @@ -1,5 +1,5 @@ /* - * Copyright 2016 FUJITSU LIMITED + * Copyright 2016-2017 FUJITSU LIMITED * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at @@ -43,15 +43,30 @@ export default (kibana) => { .default(60 * 60 * 1000) // 1 hour }).default(); - return Joi.object({ - enabled: Joi.boolean().default(true), - url : Joi.string() - .uri({scheme: ['http', 'https']}) - .required(), - port : Joi.number().required(), - defaultTimeField: Joi.string().default('@timestamp'), - cookie : cookie - }).default(); + const deprecated_keystone = Joi.object({ + url : Joi.string().uri({scheme: ['http', 'https']}), + port: Joi.number(), + }) + .tags(['deprecated']) + .notes(['url,port settings have been deprecated in favour of auth_uri']) + .default(); + + const valid_keystone = Joi.object({ + auth_uri: Joi.string().uri({scheme: ['http', 'https']}) + }) + .default(); + + return Joi + .object({ + enabled: Joi.boolean().default(true), + defaultTimeField: Joi.string().default('@timestamp'), + cookie: cookie + }) + .concat(deprecated_keystone) + .concat(valid_keystone) + .and('url', 'port') + .without('auth_uri', ['url', 'port']) + .default(); } function init(server) { diff --git a/package.json b/package.json index 64a773a..547c914 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "monasca-kibana-plugin", - "version": "0.0.5", + "version": "0.0.6", "description": "Keystone authentication & multitenancy support for Kibana 4.6.x", "author": "OpenStack", "license": "Apache-2.0", @@ -18,8 +18,8 @@ "test": "gulp test" }, "engines": { - "node": "0.12.9", - "npm": "2.14.3" + "node": "4.4.7", + "npm": "2.15.8" }, "main": "gulpfile.js", "dependencies": { diff --git a/server/__tests__/binding.spec.js b/server/__tests__/binding.spec.js index 1f3cd96..58b6c33 100644 --- a/server/__tests__/binding.spec.js +++ b/server/__tests__/binding.spec.js @@ -1,5 +1,5 @@ /* - * Copyright 2016 FUJITSU LIMITED + * Copyright 2016-2017 FUJITSU LIMITED * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at @@ -19,15 +19,21 @@ const proxyRequire = require('proxyquire'); describe('monasca-kibana-plugin', () => { describe('binding', () => { - it('should expose tokens & users', () => { + it('should expose tokens & users [url,port]', () => { let tokens = sinon.spy(); let users = sinon.spy(); + let configGet = sinon.stub(); + + configGet.withArgs('monasca-kibana-plugin.url').returns('http://localhost'); + configGet.withArgs('monasca-kibana-plugin.port').returns(5000); + configGet.withArgs('monasca-kibana-plugin.auth_uri').returns(undefined); let server = { config: sinon.stub().returns({ - get: sinon.spy() + get: configGet }), + log : sinon.spy(), expose: sinon.spy() }; @@ -36,6 +42,39 @@ describe('monasca-kibana-plugin', () => { 'keystone-v3-client/lib/keystone/users' : users })(server).start(); + chai.expect(configGet.callCount).to.be.eq(4); + + chai.expect(server.expose.callCount).to.be.eq(2); + chai.expect(server.expose.calledWith('tokens', tokens)); + chai.expect(server.expose.calledWith('users', users)); + + }); + + it('should expose tokens & users [auth_uri]', () => { + + let tokens = sinon.spy(); + let users = sinon.spy(); + let configGet = sinon.stub(); + + configGet.withArgs('monasca-kibana-plugin.url').returns(undefined); + configGet.withArgs('monasca-kibana-plugin.port').returns(undefined); + configGet.withArgs('monasca-kibana-plugin.auth_uri').returns('http://localhost/identity_admin'); + + let server = { + config: sinon.stub().returns({ + get: configGet + }), + log : sinon.spy(), + expose: sinon.spy() + }; + + proxyRequire('../binding', { + 'keystone-v3-client/lib/keystone/tokens': tokens, + 'keystone-v3-client/lib/keystone/users' : users + })(server).start(); + + chai.expect(configGet.callCount).to.be.eq(3); + chai.expect(server.expose.callCount).to.be.eq(2); chai.expect(server.expose.calledWith('tokens', tokens)); chai.expect(server.expose.calledWith('users', users)); diff --git a/server/__tests__/healthcheck.spec.js b/server/__tests__/healthcheck.spec.js index 63e5d93..fa1f36f 100644 --- a/server/__tests__/healthcheck.spec.js +++ b/server/__tests__/healthcheck.spec.js @@ -1,5 +1,5 @@ /* - * Copyright 2016 FUJITSU LIMITED + * Copyright 2016-2017 FUJITSU LIMITED * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at @@ -21,11 +21,13 @@ describe('plugins/monasca-kibana-plugin', ()=> { const keystoneUrl = 'http://localhost'; // mocking http const keystonePort = 9000; + const keystoneUri = `${keystoneUrl}:${keystonePort}`; let healthcheck; // placeholder for the require healthcheck let plugin; let configGet; + let configHas; let server; let clock; @@ -49,177 +51,213 @@ describe('plugins/monasca-kibana-plugin', ()=> { configGet = sinon.stub(); configGet.withArgs('monasca-kibana-plugin.url').returns(keystoneUrl); configGet.withArgs('monasca-kibana-plugin.port').returns(keystonePort); + configGet.withArgs('monasca-kibana-plugin.auth_uri').returns(keystoneUri); + + configHas = sinon.stub(); server = { log : sinon.stub(), on : sinon.stub(), config: function () { return { - get: configGet + get: configGet, + has: configHas }; } }; }); - it('should set status to green if keystone available', (done)=> { - let expectedCode = 200; - let expectedStatus = true; - let healthcheck = proxyRequire('../healthcheck', { - 'http': { - request: (_, callback)=> { - return { - end: () => { - let res = { - statusCode: expectedCode - }; - callback(res); - }, - on : sinon.stub() - }; + for (let mode = 0; mode < 2; mode++) { + + let modeLabel; + switch (mode) { + case 0: + modeLabel = 'url:port'; + break; + default: + modeLabel = 'auth_uri'; + } + + it(`should set status to green if keystone available [${modeLabel}]`, (done)=> { // eslint-disable-line no-loop-func + configHas.withArgs('monasca-kibana-plugin.url').returns(mode === 0); + configHas.withArgs('monasca-kibana-plugin.port').returns(mode === 0); + configHas.withArgs('monasca-kibana-plugin.auth_uri').returns(mode === 1); + + let expectedCode = 200; + let expectedStatus = true; + let healthcheck = proxyRequire('../healthcheck', { + 'http': { + request: (_, callback)=> { + return { + end: () => { + let res = { + statusCode: expectedCode + }; + callback(res); + }, + on : sinon.stub() + }; + } } - } - }); - let check = healthcheck(plugin, server); + }); + let check = healthcheck(plugin, server); - check - .run() - .then((status) => { - chai.expect(expectedStatus).to.be.equal(status); - chai.expect(plugin.status.green.calledWith('Ready')).to.be.ok; - }) - .finally(done); - - }); - - it('should set status to red if keystone not available', (done) => { - let expectedCode = 500; - let expectedStatus = false; - let healthcheck = proxyRequire('../healthcheck', { - 'http': { - request: (_, callback)=> { - return { - end: () => { - let res = { - statusCode: expectedCode - }; - callback(res); - }, - on : sinon.stub() - }; - } - } - }); - let check = healthcheck(plugin, server); - - check - .run() - .catch((status) => { - chai.expect(expectedStatus).to.be.equal(status); - chai.expect(plugin.status.red.calledWith('Unavailable')).to.be.ok; - }) - .finally(done); - - }); - - it('should set status to red if available but cannot communicate', (done)=> { - let errorListener; - let healthcheck = proxyRequire('../healthcheck', { - 'http': { - request: ()=> { - return { - on : (_, listener)=> { - errorListener = sinon.spy(listener); - }, - end: ()=> { - errorListener(new Error('test')); - } - }; - } - } - }); - let check = healthcheck(plugin, server); - - check - .run() - .catch((error)=> { - let msg = 'Unavailable: Failed to communicate with Keystone'; - chai.expect(errorListener).to.be.ok; - chai.expect(errorListener.calledOnce).to.be.ok; - chai.expect(plugin.status.red.calledWith(msg)).to.be.ok; - - chai.expect(error.message).to.be.equal('test'); - }) - .done(done); - - }); - - it('should run check in period `10000`', ()=> { - let healthcheck = proxyRequire('../healthcheck', { - 'http': { - request: sinon.stub().returns({ - end: sinon.stub(), - on : sinon.stub() + check + .run() + .then((status) => { + chai.expect(expectedStatus).to.be.equal(status); + chai.expect(plugin.status.green.calledWith('Ready')).to.be.ok; }) - } + .finally(done); + }); - let runChecks = 3; - let timeout = 10000; + it(`should set status to red if keystone not available [${modeLabel}]`, (done) => { // eslint-disable-line no-loop-func + configHas.withArgs('monasca-kibana-plugin.url').returns(mode === 0); + configHas.withArgs('monasca-kibana-plugin.port').returns(mode === 0); + configHas.withArgs('monasca-kibana-plugin.auth_uri').returns(mode === 1); - let check = healthcheck(plugin, server); - sinon.spy(check, 'run'); + let expectedCode = 500; + let expectedStatus = false; + let healthcheck = proxyRequire('../healthcheck', { + 'http': { + request: (_, callback)=> { + return { + end: () => { + let res = { + statusCode: expectedCode + }; + callback(res); + }, + on : sinon.stub() + }; + } + } + }); + let check = healthcheck(plugin, server); - // first call - chai.expect(check.isRunning()).to.be.eq(false); - check.start(); - validateFirstCall(); + check + .run() + .catch((status) => { + chai.expect(expectedStatus).to.be.equal(status); + chai.expect(plugin.status.red.calledWith('Unavailable')).to.be.ok; + }) + .finally(done); - // next calls - for (let it = 0; it < runChecks; it++) { - validateNextCallWithTick(it); - } + }); - function validateFirstCall() { - clock.tick(1); // first call is immediate - chai.expect(check.run.calledOnce).to.be.ok; - chai.expect(check.isRunning()).to.be.eq(true); - } + it(`should set status to red if available but cannot communicate [${modeLabel}]`, (done)=> { // eslint-disable-line no-loop-func + configHas.withArgs('monasca-kibana-plugin.url').returns(mode === 0); + configHas.withArgs('monasca-kibana-plugin.port').returns(mode === 0); + configHas.withArgs('monasca-kibana-plugin.auth_uri').returns(mode === 1); - function validateNextCallWithTick(it) { - // should be called once for the sake of first call - chai.assert.equal(check.run.callCount, it + 1); + let errorListener; + let healthcheck = proxyRequire('../healthcheck', { + 'http': { + request: ()=> { + return { + on : (_, listener)=> { + errorListener = sinon.spy(listener); + }, + end: ()=> { + errorListener(new Error('test')); + } + }; + } + } + }); + let check = healthcheck(plugin, server); - // run check again + check + .run() + .catch((error)=> { + let msg = 'Unavailable: Failed to communicate with Keystone'; + chai.expect(errorListener).to.be.ok; + chai.expect(errorListener.calledOnce).to.be.ok; + chai.expect(plugin.status.red.calledWith(msg)).to.be.ok; + + chai.expect(error.message).to.be.equal('test'); + }) + .done(done); + + }); + + it(`should run check in period '10000' [${modeLabel}]`, ()=> { // eslint-disable-line no-loop-func + configHas.withArgs('monasca-kibana-plugin.url').returns(mode === 0); + configHas.withArgs('monasca-kibana-plugin.port').returns(mode === 0); + configHas.withArgs('monasca-kibana-plugin.auth_uri').returns(mode === 1); + + let healthcheck = proxyRequire('../healthcheck', { + 'http': { + request: sinon.stub().returns({ + end: sinon.stub(), + on : sinon.stub() + }) + } + }); + + let runChecks = 3; + let timeout = 10000; + + let check = healthcheck(plugin, server); + sinon.spy(check, 'run'); + + // first call + chai.expect(check.isRunning()).to.be.eq(false); check.start(); + validateFirstCall(); - // assert that tick did not kick in - chai.assert.equal(check.run.callCount, it + 1); + // next calls + for (let it = 0; it < runChecks; it++) { + validateNextCallWithTick(it); + } - // kick it in - clock.tick(timeout); + function validateFirstCall() { + clock.tick(1); // first call is immediate + chai.expect(check.run.calledOnce).to.be.ok; + chai.expect(check.isRunning()).to.be.eq(true); + } - // and we have another call - chai.expect(check.run.callCount).to.be.eq(it + 2); - } - }); + function validateNextCallWithTick(it) { + // should be called once for the sake of first call + chai.assert.equal(check.run.callCount, it + 1); - it('should return false from stop if not run before', ()=> { - let healthcheck = proxyRequire('../healthcheck', { - 'http': { - request: sinon.stub().returns({ - end: sinon.stub(), - on : sinon.stub() - }) + // run check again + check.start(); + + // assert that tick did not kick in + chai.assert.equal(check.run.callCount, it + 1); + + // kick it in + clock.tick(timeout); + + // and we have another call + chai.expect(check.run.callCount).to.be.eq(it + 2); } }); - let check = healthcheck(plugin, server); - sinon.spy(check, 'run'); + it(`should return false from stop if not run before [${modeLabel}]`, ()=> { // eslint-disable-line no-loop-func + configHas.withArgs('monasca-kibana-plugin.url').returns(mode === 0); + configHas.withArgs('monasca-kibana-plugin.port').returns(mode === 0); + configHas.withArgs('monasca-kibana-plugin.auth_uri').returns(mode === 1); - chai.expect(check.stop()).to.be.eq(false); - chai.expect(check.run.called).to.be.eq(false); - }); + let healthcheck = proxyRequire('../healthcheck', { + 'http': { + request: sinon.stub().returns({ + end: sinon.stub(), + on : sinon.stub() + }) + } + }); + + let check = healthcheck(plugin, server); + sinon.spy(check, 'run'); + + chai.expect(check.stop()).to.be.eq(false); + chai.expect(check.run.called).to.be.eq(false); + }); + } }); }); diff --git a/server/__tests__/util.spec.js b/server/__tests__/util.spec.js index f74ad98..4a941b5 100644 --- a/server/__tests__/util.spec.js +++ b/server/__tests__/util.spec.js @@ -1,5 +1,5 @@ /* - * Copyright 2016 FUJITSU LIMITED + * Copyright 2016-2017 FUJITSU LIMITED * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at @@ -12,6 +12,7 @@ * the License. */ +const sinon = require('sinon'); const chai = require('chai'); const util = require('../util'); @@ -20,16 +21,68 @@ describe('plugins/monasca-kibana-plugin', ()=> { const CHECK_STR = 'test.str'; - it('should return true if starts with ok', ()=> { - chai.expect(util.startsWith(CHECK_STR, 'test')).to.be.ok; + describe('startsWith', () => { + it('should return true if starts with ok', ()=> { + chai.expect(util.startsWith(CHECK_STR, 'test')).to.be.ok; + }); + + it('should return false if does not start with', ()=> { + chai.expect(util.startsWith(CHECK_STR, 'str')).not.to.be.ok; + }); + + it('should return false if no prefixes supplied', ()=> { + chai.expect(util.startsWith(CHECK_STR)).not.to.be.ok; + }); }); - it('should return false if does not start with', ()=> { - chai.expect(util.startsWith(CHECK_STR, 'str')).not.to.be.ok; - }); + describe('keystoneUrl', () => { + const keystoneUrl = 'http://localhost'; // mocking http + const keystonePort = 9000; + const keystoneUri = `${keystoneUrl}:${keystonePort}`; + + let configGet; + let config; + + beforeEach(() => { + configGet = sinon.stub(); + config = { + get: configGet + }; + + }); + + it('should return url if url&port present', () => { + configGet.withArgs('monasca-kibana-plugin.url').returns(keystoneUrl); + configGet.withArgs('monasca-kibana-plugin.port').returns(keystonePort); + configGet.withArgs('monasca-kibana-plugin.auth_uri').returns(undefined); + + chai.expect(util.keystoneUrl(config)).to.be.equal(keystoneUri); + chai.expect(configGet.callCount).to.be.eq(4); + }); + + it('should return url if auth_uri present', () => { + configGet.withArgs('monasca-kibana-plugin.url').returns(undefined); + configGet.withArgs('monasca-kibana-plugin.port').returns(undefined); + configGet.withArgs('monasca-kibana-plugin.auth_uri').returns(keystoneUri); + + chai.expect(util.keystoneUrl(config)).to.be.equal(keystoneUri); + chai.expect(configGet.callCount).to.be.eq(3); + }); + + it('should error if neither present', () => { + configGet.withArgs('monasca-kibana-plugin.url').returns(undefined); + configGet.withArgs('monasca-kibana-plugin.port').returns(undefined); + configGet.withArgs('monasca-kibana-plugin.auth_uri').returns(undefined); + + function fn() { + util.keystoneUrl(config); + } + + chai.expect(fn).to.throw(Error, /Unexpected error, neither/); + + chai.expect(configGet.callCount).to.be.eq(2); + }); - it('should return false if no prefixes supplied', ()=> { - chai.expect(util.startsWith(CHECK_STR)).not.to.be.ok; }); }); diff --git a/server/binding/index.js b/server/binding/index.js index c1b8a61..b704954 100644 --- a/server/binding/index.js +++ b/server/binding/index.js @@ -1,5 +1,5 @@ /* - * Copyright 2016 FUJITSU LIMITED + * Copyright 2016-2017 FUJITSU LIMITED * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at @@ -15,10 +15,16 @@ import TokensApi from 'keystone-v3-client/lib/keystone/tokens'; import UsersApi from 'keystone-v3-client/lib/keystone/users'; +import util from '../util'; + module.exports = function binding(server) { const config = server.config(); + const url = util.keystoneUrl(config); + + server.log(['keystone', 'binding', 'debug'], `keystone url is ${url}`); + const keystoneCfg = { - url: `${config.get('monasca-kibana-plugin.url')}:${config.get('monasca-kibana-plugin.port')}` + url: url }; return { diff --git a/server/healthcheck/index.js b/server/healthcheck/index.js index 0430306..f0c244e 100644 --- a/server/healthcheck/index.js +++ b/server/healthcheck/index.js @@ -1,5 +1,5 @@ /* - * Copyright 2016 FUJITSU LIMITED + * Copyright 2016-2017 FUJITSU LIMITED * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at @@ -19,10 +19,11 @@ import util from '../util'; module.exports = function healthcheck(plugin, server) { const config = server.config(); - const keystoneUrl = config.get('monasca-kibana-plugin.url'); - const keystonePort = config.get('monasca-kibana-plugin.port'); + const keystoneUrl = util.keystoneUrl(config); const request = getRequest(); + server.log(['keystone', 'healthcheck', 'debug'], `keystone url is ${keystoneUrl}`); + let timeoutId; const service = { @@ -42,7 +43,7 @@ module.exports = function healthcheck(plugin, server) { return new Promise((resolve, reject)=> { const req = request({ hostname: getHostname(), - port : keystonePort, + port : getPort(), method : 'HEAD' }, (res)=> { const statusCode = res.statusCode; @@ -100,6 +101,10 @@ module.exports = function healthcheck(plugin, server) { return url.parse(keystoneUrl).hostname; } + function getPort() { + return url.parse(keystoneUrl).port; + } + function getRequest() { let required; if (util.startsWith(keystoneUrl, 'https')) { diff --git a/server/util/index.js b/server/util/index.js index 3211722..e686e27 100644 --- a/server/util/index.js +++ b/server/util/index.js @@ -1,5 +1,5 @@ /* - * Copyright 2016 FUJITSU LIMITED + * Copyright 2016-2017 FUJITSU LIMITED * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at @@ -15,9 +15,28 @@ module.exports = { startsWith: startsWith, requestPath: getRequestPath, - isESRequest: isESRequest + isESRequest: isESRequest, + keystoneUrl: keystoneUrl }; +function keystoneUrl(config) { + const urlKey = 'monasca-kibana-plugin.url'; + const portKey = 'monasca-kibana-plugin.port'; + const authUriKey = 'monasca-kibana-plugin.auth_uri'; + + let url; + + if (config.get(urlKey) && config.get(portKey)) { + url = `${config.get(urlKey)}:${config.get(portKey)}`; + } else if (config.get(authUriKey)) { + url = `${config.get(authUriKey)}`; + } else { + throw new Error(`Unexpected error, neither [${urlKey}, ${portKey}] nor ${authUriKey} found in config`); + } + + return url; +} + function startsWith(str) { var prefixes = Array.prototype.slice.call(arguments, 1); for (var i = 0; i < prefixes.length; ++i) {