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
This commit is contained in:
Tomasz Trębski 2017-04-24 14:25:16 +02:00
parent 85eaa51a10
commit 9463c20a9c
8 changed files with 347 additions and 172 deletions

View File

@ -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) {

View File

@ -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": {

View File

@ -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));

View File

@ -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);
});
}
});
});

View File

@ -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;
});
});

View File

@ -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 {

View File

@ -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')) {

View File

@ -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) {