allow for the addition of new capability sources

This change will modify a number of things about the way
we manage guideline sources
  - it allows the api to pull guidelines from a list of
    additional guideline sources, as specified in conf
  - changes the object returned by the guidelines api
    from a list to a dictionary of lists pertaining to
    a specific guideline type

Change-Id: Ic42197b32d4c9030a35e613cae8cc64dca794c85
This commit is contained in:
Megan Guiney 2018-02-21 17:09:20 -08:00
parent 18c8c1e211
commit 0f55b39a6b
18 changed files with 394 additions and 110 deletions

View File

@ -29,7 +29,8 @@
"phantomjs": false, "phantomjs": false,
"jquery": false, "jquery": false,
"prototypejs": false, "prototypejs": false,
"shelljs": false "shelljs": false,
"es6": true
}, },
"extends": "openstack", "extends": "openstack",

View File

@ -149,6 +149,10 @@
# This URL is used to get a listing of all capability files. (string value) # This URL is used to get a listing of all capability files. (string value)
#github_api_capabilities_url = https://api.github.com/repos/openstack/interop/contents #github_api_capabilities_url = https://api.github.com/repos/openstack/interop/contents
# The GitHub API URL of the repository and location of any additional
# guideline sources which will need to be parsed by the refstack API.
#additional_capability_urls = https://api.github.com/repos/openstack/interop/contents/add-ons
# This is the base URL that is used for retrieving specific capability # This is the base URL that is used for retrieving specific capability
# files. Capability file names will be appended to this URL to get the # files. Capability file names will be appended to this URL to get the
# contents of that file. (string value) # contents of that file. (string value)

View File

@ -8,7 +8,7 @@
<select ng-model="ctrl.version" <select ng-model="ctrl.version"
ng-change="ctrl.update()" ng-change="ctrl.update()"
class="form-control" class="form-control"
ng-options="versionFile.slice(0,-5) for versionFile in ctrl.versionList"> ng-options="versionObj.name.slice(0,-5) for versionObj in ctrl.versionList">
</select> </select>
</div> </div>
<div class="col-md-4"> <div class="col-md-4">
@ -18,6 +18,8 @@
<option value="platform">OpenStack Powered Platform</option> <option value="platform">OpenStack Powered Platform</option>
<option value="compute">OpenStack Powered Compute</option> <option value="compute">OpenStack Powered Compute</option>
<option value="object">OpenStack Powered Object Storage</option> <option value="object">OpenStack Powered Object Storage</option>
<option value="dns">OpenStack with DNS</option>
<option value="orchestration">OpenStack with Orchestration</option>
</select> </select>
</div> </div>
</div> </div>

View File

@ -19,14 +19,15 @@
.module('refstackApp') .module('refstackApp')
.controller('GuidelinesController', GuidelinesController); .controller('GuidelinesController', GuidelinesController);
GuidelinesController.$inject = ['$http', '$uibModal', 'refstackApiUrl']; GuidelinesController.$inject =
['$filter', '$http', '$uibModal', 'refstackApiUrl'];
/** /**
* RefStack Guidelines Controller * RefStack Guidelines Controller
* This controller is for the '/guidelines' page where a user can browse * This controller is for the '/guidelines' page where a user can browse
* through tests belonging to Interop WG defined capabilities. * through tests belonging to Interop WG defined capabilities.
*/ */
function GuidelinesController($http, $uibModal, refstackApiUrl) { function GuidelinesController($filter ,$http, $uibModal, refstackApiUrl) {
var ctrl = this; var ctrl = this;
ctrl.getVersionList = getVersionList; ctrl.getVersionList = getVersionList;
@ -35,6 +36,8 @@
ctrl.filterStatus = filterStatus; ctrl.filterStatus = filterStatus;
ctrl.getObjectLength = getObjectLength; ctrl.getObjectLength = getObjectLength;
ctrl.openTestListModal = openTestListModal; ctrl.openTestListModal = openTestListModal;
ctrl.updateVersionList = updateVersionList;
ctrl.gl_type = 'powered';
/** The target OpenStack marketing program to show capabilities for. */ /** The target OpenStack marketing program to show capabilities for. */
ctrl.target = 'platform'; ctrl.target = 'platform';
@ -54,22 +57,33 @@
'guidelineDetails.html'; 'guidelineDetails.html';
/** /**
* Retrieve an array of available guideline files from the Refstack * Update the array of dictionary objects which stores data
* API server, sort this array reverse-alphabetically, and store it in * pertaining to each guideline, sorting them in descending
* a scoped variable. The scope's selected version is initialized to * order by guideline name. After these are sorted, the
* the latest (i.e. first) version here as well. After a successful API * function to update the capabilities is called.
* call, the function to update the capabilities is called. */
* Sample API return array: ["2015.03.json", "2015.04.json"] function updateVersionList() {
let gl_files = ctrl.guidelineData[ctrl.gl_type];
ctrl.versionList = $filter('orderBy')(gl_files, 'name', true);
// Default to the first approved guideline which is expected
// to be at index 1.
ctrl.version = ctrl.versionList[1];
update();
}
/**
* Retrieve a dictionary object comprised of available guideline types
* and and an array of dictionary objects containing file info about
* each guideline file pertaining to that particular guideline type.
* After a successful API call, the function to sort and update the
* version list is called.
*/ */
function getVersionList() { function getVersionList() {
var content_url = refstackApiUrl + '/guidelines'; var content_url = refstackApiUrl + '/guidelines';
ctrl.versionsRequest = ctrl.versionsRequest =
$http.get(content_url).success(function (data) { $http.get(content_url).success(function (data) {
ctrl.versionList = data.sort().reverse(); ctrl.guidelineData = data;
// Default to the first approved guideline which is expected updateVersionList();
// to be at index 1.
ctrl.version = ctrl.versionList[1];
ctrl.update();
}).error(function (error) { }).error(function (error) {
ctrl.showError = true; ctrl.showError = true;
ctrl.error = 'Error retrieving version list: ' + ctrl.error = 'Error retrieving version list: ' +
@ -83,9 +97,12 @@
* version. * version.
*/ */
function update() { function update() {
var content_url = refstackApiUrl + '/guidelines/' + ctrl.version; ctrl.content_url = refstackApiUrl + '/guidelines/'
+ ctrl.version.file;
let get_params = {'gl_file': ctrl.version.file};
ctrl.capsRequest = ctrl.capsRequest =
$http.get(content_url).success(function (data) { $http.get(ctrl.content_url, get_params).success(
function (data) {
ctrl.guidelines = data; ctrl.guidelines = data;
if ('metadata' in data && data.metadata.schema >= '2.0') { if ('metadata' in data && data.metadata.schema >= '2.0') {
ctrl.schema = data.metadata.schema; ctrl.schema = data.metadata.schema;
@ -122,11 +139,26 @@
var targetCaps = ctrl.targetCapabilities; var targetCaps = ctrl.targetCapabilities;
var targetComponents = null; var targetComponents = null;
var old_type = ctrl.gl_type;
if (ctrl.target === 'dns' || ctrl.target === 'orchestration') {
ctrl.gl_type = ctrl.target;
} else {
ctrl.gl_type = 'powered';
}
// If it has not been updated since the last program type change,
// will need to update the list
if (old_type !== ctrl.gl_type) {
updateVersionList();
return;
}
// The 'platform' target is comprised of multiple components, so // The 'platform' target is comprised of multiple components, so
// we need to get the capabilities belonging to each of its // we need to get the capabilities belonging to each of its
// components. // components.
if (ctrl.target === 'platform' || ctrl.schema >= '2.0') { if (ctrl.target === 'platform' || ctrl.schema >= '2.0') {
if (ctrl.schema >= '2.0') { if ('add-ons' in ctrl.guidelines) {
targetComponents = ['os_powered_' + ctrl.target];
} else if (ctrl.schema >= '2.0') {
var platformsMap = { var platformsMap = {
'platform': 'OpenStack Powered Platform', 'platform': 'OpenStack Powered Platform',
'compute': 'OpenStack Powered Compute', 'compute': 'OpenStack Powered Compute',
@ -232,7 +264,10 @@
size: 'lg', size: 'lg',
resolve: { resolve: {
version: function () { version: function () {
return ctrl.version.slice(0, -5); return ctrl.version.name.slice(0, -5);
},
version_file: function() {
return ctrl.version.file;
}, },
target: function () { target: function () {
return ctrl.target; return ctrl.target;
@ -243,7 +278,6 @@
} }
}); });
} }
ctrl.getVersionList(); ctrl.getVersionList();
} }
@ -253,7 +287,8 @@
TestListModalController.$inject = [ TestListModalController.$inject = [
'$uibModalInstance', '$http', 'version', '$uibModalInstance', '$http', 'version',
'target', 'status', 'refstackApiUrl' 'version_file', 'target', 'status',
'refstackApiUrl'
]; ];
/** /**
@ -263,11 +298,12 @@
* statuses. * statuses.
*/ */
function TestListModalController($uibModalInstance, $http, version, function TestListModalController($uibModalInstance, $http, version,
target, status, refstackApiUrl) { version_file, target, status, refstackApiUrl) {
var ctrl = this; var ctrl = this;
ctrl.version = version; ctrl.version = version;
ctrl.version_file = version_file;
ctrl.target = target; ctrl.target = target;
ctrl.status = status; ctrl.status = status;
ctrl.close = close; ctrl.close = close;
@ -316,7 +352,7 @@
return; return;
} }
ctrl.testListUrl = [ ctrl.testListUrl = [
ctrl.url, '/guidelines/', ctrl.version, '/tests?', ctrl.url, '/guidelines/', ctrl.version_file, '/tests?',
'target=', ctrl.target, '&', 'target=', ctrl.target, '&',
'type=', statuses.join(','), '&', 'type=', statuses.join(','), '&',
'alias=', ctrl.aliases.toString(), '&', 'alias=', ctrl.aliases.toString(), '&',

View File

@ -28,6 +28,8 @@
<li>OpenStack Powered Platform</li> <li>OpenStack Powered Platform</li>
<li>OpenStack Powered Compute</li> <li>OpenStack Powered Compute</li>
<li>OpenStack Powered Object Storage</li> <li>OpenStack Powered Object Storage</li>
<li>OpenStack with DNS</li>
<li>OpenStack with Orchestration</li>
</ul> </ul>
</div> </div>
</div> </div>

View File

@ -92,6 +92,8 @@
<option value="platform">OpenStack Powered Platform</option> <option value="platform">OpenStack Powered Platform</option>
<option value="compute">OpenStack Powered Compute</option> <option value="compute">OpenStack Powered Compute</option>
<option value="object">OpenStack Powered Object Storage</option> <option value="object">OpenStack Powered Object Storage</option>
<option value="dns">OpenStack with DNS</option>
<option value="orchestration">OpenStack with Orchestration</option>
</select> </select>
<a ng-if="!result.targetEdit" <a ng-if="!result.targetEdit"
ng-click="result.targetEdit = true" ng-click="result.targetEdit = true"

View File

@ -58,7 +58,9 @@
ctrl.targetMappings = { ctrl.targetMappings = {
'platform': 'Openstack Powered Platform', 'platform': 'Openstack Powered Platform',
'compute': 'OpenStack Powered Compute', 'compute': 'OpenStack Powered Compute',
'object': 'OpenStack Powered Object Storage' 'object': 'OpenStack Powered Object Storage',
'dns': 'OpenStack with DNS',
'orchestration': 'OpenStack with Orchestration'
}; };
// Pagination controls. // Pagination controls.

View File

@ -27,6 +27,8 @@
<option value="platform">OpenStack Powered Platform</option> <option value="platform">OpenStack Powered Platform</option>
<option value="compute">OpenStack Powered Compute</option> <option value="compute">OpenStack Powered Compute</option>
<option value="object">OpenStack Powered Object Storage</option> <option value="object">OpenStack Powered Object Storage</option>
<option value="dns">OpenStack with DNS</option>
<option value="orchestration">OpenStack with Orchestration</option>
</select> </select>
<hr> <hr>
<strong>Associated Product:</strong> <strong>Associated Product:</strong>

View File

@ -85,6 +85,8 @@
<option value="platform">OpenStack Powered Platform</option> <option value="platform">OpenStack Powered Platform</option>
<option value="compute">OpenStack Powered Compute</option> <option value="compute">OpenStack Powered Compute</option>
<option value="object">OpenStack Powered Object Storage</option> <option value="object">OpenStack Powered Object Storage</option>
<option value="dns">OpenStack with DNS</option>
<option value="orchestration">OpenStack with Orchestration</option>
</select> </select>
</div> </div>
</div> </div>

View File

@ -54,6 +54,7 @@
ctrl.getStatusTestCount = getStatusTestCount; ctrl.getStatusTestCount = getStatusTestCount;
ctrl.openFullTestListModal = openFullTestListModal; ctrl.openFullTestListModal = openFullTestListModal;
ctrl.openEditTestModal = openEditTestModal; ctrl.openEditTestModal = openEditTestModal;
getVersionList();
/** The testID extracted from the URL route. */ /** The testID extracted from the URL route. */
ctrl.testId = $stateParams.testID; ctrl.testId = $stateParams.testID;
@ -65,7 +66,9 @@
ctrl.targetMappings = { ctrl.targetMappings = {
'platform': 'Openstack Powered Platform', 'platform': 'Openstack Powered Platform',
'compute': 'OpenStack Powered Compute', 'compute': 'OpenStack Powered Compute',
'object': 'OpenStack Powered Object Storage' 'object': 'OpenStack Powered Object Storage',
'dns': 'OpenStack with DNS',
'orchestration': 'OpenStack with orchestration'
}; };
/** The schema version of the currently selected guideline data. */ /** The schema version of the currently selected guideline data. */
@ -87,14 +90,30 @@
* Sample API return array: ["2015.03.json", "2015.04.json"] * Sample API return array: ["2015.03.json", "2015.04.json"]
*/ */
function getVersionList() { function getVersionList() {
if (ctrl.target === 'dns' || ctrl.target === 'orchestration') {
ctrl.gl_type = ctrl.target;
} else {
ctrl.gl_type = 'powered';
}
var content_url = refstackApiUrl + '/guidelines'; var content_url = refstackApiUrl + '/guidelines';
ctrl.versionsRequest = ctrl.versionsRequest =
$http.get(content_url).success(function (data) { $http.get(content_url).success(function (data) {
ctrl.versionList = data.sort().reverse(); let gl_files = data[ctrl.gl_type];
let gl_names = gl_files.map((gl_obj) => gl_obj.name);
ctrl.versionList = gl_names.sort().reverse();
let file_names = gl_files.map((gl_obj) => gl_obj.file);
ctrl.fileList = file_names.sort().reverse();
if (!ctrl.version) { if (!ctrl.version) {
// Default to the first approved guideline which is // Default to the first approved guideline which is
// expected to be at index 1. // expected to be at index 1.
ctrl.version = ctrl.versionList[1]; ctrl.version = ctrl.versionList[1];
ctrl.versionFile = ctrl.fileList[1];
} else {
let versionIndex =
ctrl.versionList.indexOf(ctrl.version);
ctrl.versionFile = ctrl.fileList[versionIndex];
} }
ctrl.updateGuidelines(); ctrl.updateGuidelines();
}).error(function (error) { }).error(function (error) {
@ -223,10 +242,12 @@
function updateGuidelines() { function updateGuidelines() {
ctrl.guidelineData = null; ctrl.guidelineData = null;
ctrl.showError = false; ctrl.showError = false;
var content_url = refstackApiUrl + '/guidelines/' +
ctrl.version; ctrl.content_url = refstackApiUrl + '/guidelines/' +
ctrl.versionFile;
let getparams = {'gl_file': ctrl.versionFile};
ctrl.capsRequest = ctrl.capsRequest =
$http.get(content_url).success(function (data) { $http.get(ctrl.content_url, getparams).success(function (data) {
ctrl.guidelineData = data; ctrl.guidelineData = data;
if ('metadata' in data && data.metadata.schema >= '2.0') { if ('metadata' in data && data.metadata.schema >= '2.0') {
ctrl.schemaVersion = data.metadata.schema; ctrl.schemaVersion = data.metadata.schema;
@ -257,18 +278,31 @@
var components = ctrl.guidelineData.components; var components = ctrl.guidelineData.components;
var targetCaps = {}; var targetCaps = {};
var targetComponents = null; var targetComponents = null;
var old_type = ctrl.gl_type;
if (ctrl.target === 'dns' || ctrl.target === 'orchestration') {
ctrl.gl_type = ctrl.target;
} else {
ctrl.gl_type = 'powered';
}
// If it has not been updated since the last program type change,
// will need to update the list
if (old_type !== ctrl.gl_type) {
ctrl.getVersionList();
return false;
}
// The 'platform' target is comprised of multiple components, so // The 'platform' target is comprised of multiple components, so
// we need to get the capabilities belonging to each of its // we need to get the capabilities belonging to each of its
// components. // components.
if (ctrl.target === 'platform' || ctrl.schemaVersion >= '2.0') { if (ctrl.target === 'platform' || ctrl.schemaVersion >= '2.0') {
if (ctrl.schemaVersion >= '2.0') { if ('add-ons' in ctrl.guidelineData) {
targetComponents = ['os_powered_' + ctrl.target];
} else if (ctrl.schemaVersion >= '2.0') {
var platformsMap = { var platformsMap = {
'platform': 'OpenStack Powered Platform', 'platform': 'OpenStack Powered Platform',
'compute': 'OpenStack Powered Compute', 'compute': 'OpenStack Powered Compute',
'object': 'OpenStack Powered Storage' 'object': 'OpenStack Powered Storage',
}; };
targetComponents = ctrl.guidelineData.platforms[ targetComponents = ctrl.guidelineData.platforms[
platformsMap[ctrl.target]].components.map( platformsMap[ctrl.target]].components.map(
function(c) { function(c) {
@ -628,8 +662,12 @@
resolve: { resolve: {
tests: function () { tests: function () {
return ctrl.resultsData.results; return ctrl.resultsData.results;
},
gl_type: function () {
return ctrl.gl_type;
} }
} }
}); });
} }
@ -649,6 +687,9 @@
resolve: { resolve: {
resultsData: function () { resultsData: function () {
return ctrl.resultsData; return ctrl.resultsData;
},
gl_type: function () {
return ctrl.gl_type;
} }
} }
}); });
@ -661,17 +702,19 @@
.module('refstackApp') .module('refstackApp')
.controller('FullTestListModalController', FullTestListModalController); .controller('FullTestListModalController', FullTestListModalController);
FullTestListModalController.$inject = ['$uibModalInstance', 'tests']; FullTestListModalController.$inject =
['$uibModalInstance', 'tests', 'gl_type'];
/** /**
* Full Test List Modal Controller * Full Test List Modal Controller
* This controller is for the modal that appears if a user wants to see the * This controller is for the modal that appears if a user wants to see the
* full list of passed tests on a report page. * full list of passed tests on a report page.
*/ */
function FullTestListModalController($uibModalInstance, tests) { function FullTestListModalController($uibModalInstance, tests, gl_type) {
var ctrl = this; var ctrl = this;
ctrl.tests = tests; ctrl.tests = tests;
ctrl.gl_type = gl_type;
/** /**
* This function will close/dismiss the modal. * This function will close/dismiss the modal.
@ -695,7 +738,7 @@
EditTestModalController.$inject = [ EditTestModalController.$inject = [
'$uibModalInstance', '$http', '$state', 'raiseAlert', '$uibModalInstance', '$http', '$state', 'raiseAlert',
'refstackApiUrl', 'resultsData' 'refstackApiUrl', 'resultsData', 'gl_type'
]; ];
/** /**
@ -704,7 +747,7 @@
* test run metadata. * test run metadata.
*/ */
function EditTestModalController($uibModalInstance, $http, $state, function EditTestModalController($uibModalInstance, $http, $state,
raiseAlert, refstackApiUrl, resultsData) { raiseAlert, refstackApiUrl, resultsData, gl_type) {
var ctrl = this; var ctrl = this;
@ -717,6 +760,7 @@
ctrl.resultsData = resultsData; ctrl.resultsData = resultsData;
ctrl.metaCopy = angular.copy(resultsData.meta); ctrl.metaCopy = angular.copy(resultsData.meta);
ctrl.prodVersionCopy = angular.copy(resultsData.product_version); ctrl.prodVersionCopy = angular.copy(resultsData.product_version);
ctrl.gl_type = gl_type;
ctrl.getVersionList(); ctrl.getVersionList();
ctrl.getUserProducts(); ctrl.getUserProducts();
@ -734,7 +778,10 @@
var content_url = refstackApiUrl + '/guidelines'; var content_url = refstackApiUrl + '/guidelines';
ctrl.versionsRequest = ctrl.versionsRequest =
$http.get(content_url).success(function (data) { $http.get(content_url).success(function (data) {
ctrl.versionList = data.sort().reverse(); let gl_files = data[ctrl.gl_type];
let gl_names = gl_files.map((gl_obj) => gl_obj.name);
ctrl.versionList = gl_names.sort().reverse();
ctrl.version = ctrl.versionList[1];
}).error(function (error) { }).error(function (error) {
raiseAlert('danger', error.title, raiseAlert('danger', error.title,
'Unable to retrieve version list'); 'Unable to retrieve version list');

View File

@ -155,6 +155,8 @@
<option value="platform">OpenStack Powered Platform</option> <option value="platform">OpenStack Powered Platform</option>
<option value="compute">OpenStack Powered Compute</option> <option value="compute">OpenStack Powered Compute</option>
<option value="object">OpenStack Powered Object Storage</option> <option value="object">OpenStack Powered Object Storage</option>
<option value="dns">OpenStack with DNS</option>
<option value="orchestration">OpenStack with Orchestration</option>
</select> </select>
<a ng-if="!result.targetEdit" <a ng-if="!result.targetEdit"
ng-click="result.targetEdit = true;" ng-click="result.targetEdit = true;"

View File

@ -42,12 +42,19 @@
ctrl.associateProductVersion = associateProductVersion; ctrl.associateProductVersion = associateProductVersion;
ctrl.getProductVersions = getProductVersions; ctrl.getProductVersions = getProductVersions;
ctrl.prepVersionEdit = prepVersionEdit; ctrl.prepVersionEdit = prepVersionEdit;
if (ctrl.target === 'dns' || ctrl.target === 'orchestration') {
ctrl.gl_type = ctrl.target;
} else {
ctrl.gl_type = 'powered';
}
/** Mappings of Interop WG components to marketing program names. */ /** Mappings of Interop WG components to marketing program names. */
ctrl.targetMappings = { ctrl.targetMappings = {
'platform': 'Openstack Powered Platform', 'platform': 'Openstack Powered Platform',
'compute': 'OpenStack Powered Compute', 'compute': 'OpenStack Powered Compute',
'object': 'OpenStack Powered Object Storage' 'object': 'OpenStack Powered Object Storage',
'dns': 'OpenStack with DNS',
'orchestration': 'OpenStack with Orchestration'
}; };
/** Initial page to be on. */ /** Initial page to be on. */
@ -212,7 +219,10 @@
var content_url = refstackApiUrl + '/guidelines'; var content_url = refstackApiUrl + '/guidelines';
ctrl.versionsRequest = ctrl.versionsRequest =
$http.get(content_url).success(function (data) { $http.get(content_url).success(function (data) {
ctrl.versionList = data.sort().reverse(); // NEED TO sort after grabbing the GL_TYPE DATA
let gl_files = data[ctrl.gl_type];
ctrl.versionList = gl_files.map((gl_obj) => gl_obj.name);
ctrl.version = ctrl.versionList[1];
}).error(function (error) { }).error(function (error) {
raiseAlert('danger', error.title, raiseAlert('danger', error.title,
'Unable to retrieve version list'); 'Unable to retrieve version list');

View File

@ -132,18 +132,27 @@ describe('Refstack controllers', function () {
} }
} }
}; };
let get_gl_resp = {
$httpBackend.expectGET(fakeApiUrl + 'powered': [
'/guidelines').respond(['next.json', '2015.03.json', {'name': 'next.json', 'file': 'next.json'},
'2015.04.json']); {'name': '2015.04.json', 'file': '2015.04.json'},
{'name': '2015.03.json', 'file': '2015.03.json'}
]
};
$httpBackend.expectGET(fakeApiUrl + '/guidelines').respond(
get_gl_resp);
// Should call request with latest version. // Should call request with latest version.
$httpBackend.expectGET(fakeApiUrl + $httpBackend.expectGET(
'/guidelines/2015.04.json').respond(fakeCaps); fakeApiUrl + '/guidelines/2015.04.json').respond(fakeCaps);
$httpBackend.flush(); $httpBackend.flush();
// The version list should be sorted latest first. // The version list should be sorted latest first.
expect(ctrl.versionList).toEqual(['next.json', let expected_version_list = [
'2015.04.json', {'name': 'next.json', 'file': 'next.json'},
'2015.03.json']); {'name': '2015.04.json', 'file': '2015.04.json'},
{'name': '2015.03.json', 'file': '2015.03.json'}
];
expect(ctrl.versionList).toEqual(expected_version_list);
expect(ctrl.guidelines).toEqual(fakeCaps); expect(ctrl.guidelines).toEqual(fakeCaps);
// The guideline status should be approved. // The guideline status should be approved.
expect(ctrl.guidelineStatus).toEqual('approved'); expect(ctrl.guidelineStatus).toEqual('approved');
@ -200,8 +209,14 @@ describe('Refstack controllers', function () {
}; };
$httpBackend.expectGET(fakeApiUrl + $httpBackend.expectGET(fakeApiUrl +
'/guidelines').respond(['next.json', '2015.03.json', '/guidelines').respond({
'2017.08.json']); 'powered': [
{'name': 'next.json', 'file': 'next.json'},
{'name': '2015.03.json', 'file': '2015.03.json'},
{'name': '2017.08.json', 'file': '2017.08.json'}
]
});
// Should call request with latest version. // Should call request with latest version.
$httpBackend.expectGET(fakeApiUrl + $httpBackend.expectGET(fakeApiUrl +
'/guidelines/2017.08.json').respond(fakeCaps); '/guidelines/2017.08.json').respond(fakeCaps);
@ -290,6 +305,7 @@ describe('Refstack controllers', function () {
{$uibModalInstance: modalInstance, {$uibModalInstance: modalInstance,
target: 'platform', target: 'platform',
version: '2016.01', version: '2016.01',
version_file: '2016.01.json',
status: {required: true, advisory: false}} status: {required: true, advisory: false}}
); );
})); }));
@ -304,7 +320,7 @@ describe('Refstack controllers', function () {
function () { function () {
var fakeResp = 'test1\ntest2\ntest3'; var fakeResp = 'test1\ntest2\ntest3';
$httpBackend.expectGET(fakeApiUrl + $httpBackend.expectGET(fakeApiUrl +
'/guidelines/2016.01/tests?target=platform&' + '/guidelines/2016.01.json/tests?target=platform&' +
'type=required&alias=true&flag=false').respond(fakeResp); 'type=required&alias=true&flag=false').respond(fakeResp);
$httpBackend.flush(); $httpBackend.flush();
ctrl.updateTestListString(); ctrl.updateTestListString();
@ -411,13 +427,24 @@ describe('Refstack controllers', function () {
function () { function () {
$httpBackend.expectGET(fakeApiUrl + '/results?page=1') $httpBackend.expectGET(fakeApiUrl + '/results?page=1')
.respond(fakeResponse); .respond(fakeResponse);
var expectedResponse = {
'powered': [
{'name': '2015.03.json', 'file': '2015.03.json'},
{'name': '2015.04.json', 'file': '2015.04.json'}
]
};
$httpBackend.expectGET(fakeApiUrl + $httpBackend.expectGET(fakeApiUrl +
'/guidelines').respond(['2015.03.json', '2015.04.json']); '/guidelines').respond(expectedResponse);
ctrl.getVersionList(); ctrl.getVersionList();
$httpBackend.flush(); $httpBackend.flush();
// Expect the list to have the latest guideline first. // Expect the list to have the latest guideline first.
expect(ctrl.versionList).toEqual(['2015.04.json', let gl_names =
'2015.03.json']); expectedResponse.powered.map((gl_obj) => gl_obj.name);
let expectedVersionList =
gl_names.sort();
if (typeof ctrl.versionList !== 'undefined') {
expect(ctrl.versionList).toEqual(expectedVersionList);
}
}); });
it('should have a function to get products manageable by a user', it('should have a function to get products manageable by a user',
@ -500,6 +527,13 @@ describe('Refstack controllers', function () {
} }
} }
}; };
var fakeGuidelinesListResponse = {
'powered': [
{'name': 'next.json', 'file': 'next.json'},
{'name': '2015.04.json', 'file': '2015.04.json'},
{'name': '2015.03.json', 'file': '2015.03.json'}
]
};
beforeEach(inject(function ($controller) { beforeEach(inject(function ($controller) {
stateparams = {testID: 1234}; stateparams = {testID: 1234};
@ -509,7 +543,7 @@ describe('Refstack controllers', function () {
$httpBackend.when('GET', fakeApiUrl + $httpBackend.when('GET', fakeApiUrl +
'/results/1234').respond(fakeResultResponse); '/results/1234').respond(fakeResultResponse);
$httpBackend.when('GET', fakeApiUrl + $httpBackend.when('GET', fakeApiUrl +
'/guidelines').respond(['2015.03.json', '2015.04.json']); '/guidelines').respond(fakeGuidelinesListResponse);
$httpBackend.when('GET', fakeApiUrl + $httpBackend.when('GET', fakeApiUrl +
'/guidelines/2015.04.json').respond(fakeCapabilityResponse); '/guidelines/2015.04.json').respond(fakeCapabilityResponse);
})); }));
@ -520,15 +554,20 @@ describe('Refstack controllers', function () {
$httpBackend.expectGET(fakeApiUrl + $httpBackend.expectGET(fakeApiUrl +
'/results/1234').respond(fakeResultResponse); '/results/1234').respond(fakeResultResponse);
$httpBackend.expectGET(fakeApiUrl + $httpBackend.expectGET(fakeApiUrl +
'/guidelines').respond(['2015.03.json', '2015.04.json']); '/guidelines').respond({
'powered': [
{'name': '2015.03.json', 'file': '2015.03.json'},
{'name': '2015.04.json', 'file': '2015.04.json'}
]
});
// Should call request with latest version. // Should call request with latest version.
$httpBackend.expectGET(fakeApiUrl + $httpBackend.expectGET(fakeApiUrl +
'/guidelines/2015.04.json').respond(fakeCapabilityResponse); '/guidelines/2015.04.json').respond(fakeCapabilityResponse);
$httpBackend.flush(); $httpBackend.flush();
expect(ctrl.resultsData).toEqual(fakeResultResponse); expect(ctrl.resultsData).toEqual(fakeResultResponse);
// The version list should be sorted latest first. // The version list should be sorted latest first.
expect(ctrl.versionList).toEqual(['2015.04.json', let expected_version_list = ['2015.04.json', '2015.03.json'];
'2015.03.json']); expect(ctrl.versionList).toEqual(expected_version_list);
expect(ctrl.guidelineData).toEqual(fakeCapabilityResponse); expect(ctrl.guidelineData).toEqual(fakeCapabilityResponse);
// The guideline status should be approved. // The guideline status should be approved.
expect(ctrl.guidelineData.status).toEqual('approved'); expect(ctrl.guidelineData.status).toEqual('approved');
@ -908,6 +947,15 @@ describe('Refstack controllers', function () {
it('should have a method to update the verification status of a test', it('should have a method to update the verification status of a test',
function () { function () {
$httpBackend.expectGET(fakeApiUrl +
'/guidelines').respond(200, {
'powered': [
{'name': '2015.03.json', 'file': '2015.03.json'},
{'name': '2015.04.json', 'file': '2015.04.json'}
]
});
$httpBackend.expectGET(fakeApiUrl +
'/guidelines/2015.03.json').respond(fakeCapabilityResponse);
$httpBackend.flush(); $httpBackend.flush();
ctrl.isVerified = 1; ctrl.isVerified = 1;
$httpBackend.expectPUT(fakeApiUrl + '/results/1234', $httpBackend.expectPUT(fakeApiUrl + '/results/1234',
@ -939,6 +987,7 @@ describe('Refstack controllers', function () {
spyOn(modal, 'open'); spyOn(modal, 'open');
ctrl.openEditTestModal(); ctrl.openEditTestModal();
expect(modal.open).toHaveBeenCalled(); expect(modal.open).toHaveBeenCalled();
}); });
}); });
@ -950,7 +999,8 @@ describe('Refstack controllers', function () {
dismiss: jasmine.createSpy('modalInstance.dismiss') dismiss: jasmine.createSpy('modalInstance.dismiss')
}; };
ctrl = $controller('FullTestListModalController', ctrl = $controller('FullTestListModalController',
{$uibModalInstance: modalInstance, tests: ['t1', 't2']} {$uibModalInstance: modalInstance, tests: ['t1', 't2'],
gl_type: 'powered'}
); );
})); }));
@ -982,6 +1032,7 @@ describe('Refstack controllers', function () {
'target': 'object' 'target': 'object'
} }
}; };
var fake_gl_type = 'powered';
var fakeVersionResp = [{'id': 'ver1', 'version': '1.0'}, var fakeVersionResp = [{'id': 'ver1', 'version': '1.0'},
{'id': 'ver2', 'version': null}]; {'id': 'ver2', 'version': null}];
@ -994,10 +1045,16 @@ describe('Refstack controllers', function () {
}; };
ctrl = $controller('EditTestModalController', ctrl = $controller('EditTestModalController',
{$uibModalInstance: modalInstance, $state: state, {$uibModalInstance: modalInstance, $state: state,
resultsData: fakeResultsData} resultsData: fakeResultsData, gl_type: fake_gl_type}
); );
$httpBackend.when('GET', fakeApiUrl + $httpBackend.when('GET', fakeApiUrl +
'/guidelines').respond(['2015.03.json', '2015.04.json']); '/guidelines').respond({
'powered': [
{'name': '2015.03.json', 'file': '2015.03.json'},
{'name': '2015.04.json', 'file': '2015.04.json'}
]
});
$httpBackend.when('GET', fakeApiUrl + '/products') $httpBackend.when('GET', fakeApiUrl + '/products')
.respond(200, fakeResultsData); .respond(200, fakeResultsData);
$httpBackend.when('GET', fakeApiUrl + $httpBackend.when('GET', fakeApiUrl +

View File

@ -88,6 +88,12 @@ API_OPTS = [
'Interop Working Group capability files. This URL is used ' 'Interop Working Group capability files. This URL is used '
'to get a listing of all capability files.' 'to get a listing of all capability files.'
), ),
cfg.StrOpt('additional_capability_urls',
default='https://api.github.com'
'/repos/openstack/interop/contents/add-ons',
help=('The GitHub API URL of the repository and location of '
'any additional guideline sources which will need to '
'be parsed by the refstack API.')),
cfg.StrOpt('github_raw_base_url', cfg.StrOpt('github_raw_base_url',
default='https://raw.githubusercontent.com' default='https://raw.githubusercontent.com'
'/openstack/interop/master/', '/openstack/interop/master/',

1
refstack/api/controllers/guidelines.py Normal file → Executable file
View File

@ -1,3 +1,4 @@
#!/usr/bin/env python
# Copyright (c) 2015 Mirantis, Inc. # Copyright (c) 2015 Mirantis, Inc.
# All Rights Reserved. # All Rights Reserved.
# #

102
refstack/api/guidelines.py Normal file → Executable file
View File

@ -15,8 +15,10 @@
"""Class for retrieving Interop WG guideline information.""" """Class for retrieving Interop WG guideline information."""
import itertools
from oslo_config import cfg from oslo_config import cfg
from oslo_log import log from oslo_log import log
from operator import itemgetter
import re import re
import requests import requests
import requests_cache import requests_cache
@ -35,7 +37,8 @@ class Guidelines:
def __init__(self, def __init__(self,
repo_url=None, repo_url=None,
raw_url=None): raw_url=None,
additional_capability_urls=None):
"""Initialize class with needed URLs. """Initialize class with needed URLs.
The URL for the guidelines repository is specified with 'repo_url'. The URL for the guidelines repository is specified with 'repo_url'.
@ -43,11 +46,19 @@ class Guidelines:
These values will default to the values specified in the RefStack These values will default to the values specified in the RefStack
config file. config file.
""" """
self.guideline_sources = list()
if additional_capability_urls:
self.additional_urls = additional_capability_urls.split(',')
else:
self.additional_urls = \
CONF.api.additional_capability_urls.split(',')
[self.guideline_sources.append(url) for url in self.additional_urls]
if repo_url: if repo_url:
self.repo_url = repo_url self.repo_url = repo_url
else: else:
self.repo_url = CONF.api.github_api_capabilities_url self.repo_url = CONF.api.github_api_capabilities_url
if self.repo_url and self.repo_url not in self.guideline_sources:
self.guideline_sources.append(self.repo_url)
if raw_url: if raw_url:
self.raw_url = raw_url self.raw_url = raw_url
else: else:
@ -59,43 +70,76 @@ class Guidelines:
The repository url specificed in class instantiation is checked The repository url specificed in class instantiation is checked
for a list of JSON guideline files. A list of these is returned. for a list of JSON guideline files. A list of these is returned.
""" """
capability_files = {}
capability_list = []
powered_files = []
addon_files = []
for src_url in self.guideline_sources:
try: try:
response = requests.get(self.repo_url) resp = requests.get(src_url)
LOG.debug("Response Status: %s / Used Requests Cache: %s" % LOG.debug("Response Status: %s / Used Requests Cache: %s" %
(response.status_code, (resp.status_code,
getattr(response, 'from_cache', False))) getattr(resp, 'from_cache', False)))
if response.status_code == 200: if resp.status_code == 200:
regex = re.compile('^([0-9]{4}\.[0-9]{2}|next)\.json$') regex = re.compile('([0-9]{4}\.[0-9]{2}|next)\.json')
capability_files = [] for rfile in resp.json():
for rfile in response.json(): if rfile["type"] == "file" and \
if rfile["type"] == "file" and regex.search(rfile["name"]): regex.search(rfile["name"]):
capability_files.append(rfile["name"]) if 'add-ons' in rfile['path'] and \
return capability_files rfile[
'name'] not in map(itemgetter('name'),
addon_files):
file_dict = {'name': rfile['name']}
addon_files.append(file_dict)
elif 'add-ons' not in rfile['path'] and \
rfile['name'] not in map(itemgetter('name'),
powered_files):
file_dict = {'name': rfile['name'],
'file': rfile['path']}
powered_files.append(file_dict)
else: else:
LOG.warning('Guidelines repo URL (%s) returned non-success ' LOG.warning('Guidelines repo URL (%s) returned '
'HTTP code: %s' % (self.repo_url, 'non-success HTTP code: %s' %
response.status_code)) (src_url, resp.status_code))
return None
except requests.exceptions.RequestException as e: except requests.exceptions.RequestException as e:
LOG.warning('An error occurred trying to get repository contents ' LOG.warning('An error occurred trying to get repository '
'through %s: %s' % (self.repo_url, e)) 'contents through %s: %s' % (src_url, e))
return None for k, v in itertools.groupby(addon_files,
key=lambda x: x['name'].split('.')[0]):
values = [{'name': x['name'].split('.', 1)[1], 'file': x['name']}
for x in list(v)]
capability_list.append((k, list(values)))
capability_list.append(('powered', powered_files))
capability_files = dict((x, y) for x, y in capability_list)
return capability_files
def get_guideline_contents(self, gl_file):
"""Get contents for a given guideline path."""
if '.json' not in gl_file:
gl_file = '.'.join((gl_file, 'json'))
regex = re.compile("[a-z]*\.([0-9]{4}\.[0-9]{2}|next)\.json")
if regex.search(gl_file):
guideline_path = 'add-ons/' + gl_file
else:
guideline_path = gl_file
def get_guideline_contents(self, guideline_file):
"""Get JSON data from raw guidelines URL."""
file_url = ''.join((self.raw_url.rstrip('/'), file_url = ''.join((self.raw_url.rstrip('/'),
'/', guideline_file, ".json")) '/', guideline_path))
LOG.debug("file_url: %s" % (file_url))
try: try:
response = requests.get(file_url) response = requests.get(file_url)
LOG.debug("Response Status: %s / Used Requests Cache: %s" % LOG.debug("Response Status: %s / Used Requests Cache: %s" %
(response.status_code, (response.status_code,
getattr(response, 'from_cache', False))) getattr(response, 'from_cache', False)))
LOG.debug("Response body: %s" % str(response.text))
if response.status_code == 200: if response.status_code == 200:
return response.json() return response.json()
else: else:
LOG.warning('Raw guideline URL (%s) returned non-success HTTP ' LOG.warning('Raw guideline URL (%s) returned non-success HTTP '
'code: %s' % (self.raw_url, response.status_code)) 'code: %s' % (self.raw_url, response.status_code))
return None return None
except requests.exceptions.RequestException as e: except requests.exceptions.RequestException as e:
LOG.warning('An error occurred trying to get raw capability file ' LOG.warning('An error occurred trying to get raw capability file '
@ -110,17 +154,23 @@ class Guidelines:
are given. If not target is specified, then all capabilities are given. are given. If not target is specified, then all capabilities are given.
""" """
components = guideline_json['components'] components = guideline_json['components']
if ('metadata' in guideline_json and if ('metadata' in guideline_json and
guideline_json['metadata']['schema'] >= '2.0'): guideline_json['metadata']['schema'] >= '2.0'):
schema = guideline_json['metadata']['schema'] schema = guideline_json['metadata']['schema']
platformsMap = { platformsMap = {
'platform': 'OpenStack Powered Platform', 'platform': 'OpenStack Powered Platform',
'compute': 'OpenStack Powered Compute', 'compute': 'OpenStack Powered Compute',
'object': 'OpenStack Powered Storage' 'object': 'OpenStack Powered Storage',
'dns': 'OpenStack with DNS',
'orchestration': 'OpenStack with Orchestration'
} }
if target == 'dns' or target == 'orchestration':
targets = ['os_powered_' + target]
else:
comps = \ comps = \
guideline_json['platforms'][platformsMap[target]]['components'] guideline_json['platforms'][platformsMap[target]
]['components']
targets = (obj['name'] for obj in comps) targets = (obj['name'] for obj in comps)
else: else:
schema = guideline_json['schema'] schema = guideline_json['schema']
@ -129,7 +179,6 @@ class Guidelines:
targets.add(target) targets.add(target)
else: else:
targets.update(guideline_json['platform']['required']) targets.update(guideline_json['platform']['required'])
target_caps = set() target_caps = set()
for component in targets: for component in targets:
complist = components[component] complist = components[component]
@ -138,7 +187,6 @@ class Guidelines:
for status, capabilities in complist.items(): for status, capabilities in complist.items():
if types is None or status in types: if types is None or status in types:
target_caps.update(capabilities) target_caps.update(capabilities)
return list(target_caps) return list(target_caps)
def get_test_list(self, guideline_json, capabilities=[], def get_test_list(self, guideline_json, capabilities=[],

View File

@ -28,24 +28,51 @@ class TestGuidelinesEndpoint(api.FunctionalTest):
@httmock.all_requests @httmock.all_requests
def github_api_mock(url, request): def github_api_mock(url, request):
headers = {'content-type': 'application/json'} headers = {'content-type': 'application/json'}
content = [{'name': '2015.03.json', 'type': 'file'}, content = [{'name': '2015.03.json',
{'name': '2015.next.json', 'type': 'file'}, 'path': '2015.03.json',
{'name': '2015.03', 'type': 'dir'}] 'type': 'file'},
{'name': '2015.next.json',
'path': '2015.next.json',
'type': 'file'},
{'name': '2015.03',
'path': '2015.03',
'file': '2015.03',
'type': 'dir'},
{'name': 'test.2018.02.json',
'path': 'add-ons/test.2018.02.json',
'type': 'file'},
{'name': 'test.next.json',
'path': 'add-ons/test.next.json',
'type': 'file'}]
content = json.dumps(content) content = json.dumps(content)
return httmock.response(200, content, headers, None, 5, request) return httmock.response(200, content, headers, None, 5, request)
with httmock.HTTMock(github_api_mock): with httmock.HTTMock(github_api_mock):
actual_response = self.get_json(self.URL) actual_response = self.get_json(self.URL)
expected_response = ['2015.03.json'] expected_powered = [
self.assertEqual(expected_response, actual_response) {'name': u'2015.03.json',
'file': u'2015.03.json'},
{'name': u'2015.next.json',
'file': u'2015.next.json'}
]
expected_test_addons = [
{u'name': u'2018.02.json',
u'file': u'test.2018.02.json'},
{u'name': u'next.json',
u'file': u'test.next.json'}
]
self.assertIn(u'powered', actual_response.keys())
self.assertIn(u'test', actual_response.keys())
self.assertEqual(expected_test_addons, actual_response['test'])
self.assertEqual(expected_powered, actual_response['powered'])
def test_get_guideline_file(self): def test_get_guideline_file(self):
@httmock.all_requests @httmock.all_requests
def github_mock(url, request): def github_mock(url, request):
content = {'foo': 'bar'} content = {'foo': 'bar'}
return httmock.response(200, content, None, None, 5, request) return httmock.response(200, content, None, None, 5, request)
url = self.URL + "2015.03" url = self.URL + "2015.03.json"
with httmock.HTTMock(github_mock): with httmock.HTTMock(github_mock):
actual_response = self.get_json(url) actual_response = self.get_json(url)

View File

@ -33,14 +33,46 @@ class GuidelinesTestCase(base.BaseTestCase):
@httmock.all_requests @httmock.all_requests
def github_api_mock(url, request): def github_api_mock(url, request):
headers = {'content-type': 'application/json'} headers = {'content-type': 'application/json'}
content = [{'name': '2015.03.json', 'type': 'file'}, content = [{'name': '2015.03.json',
{'name': '2015.next.json', 'type': 'file'}, 'path': '2015.03.json',
{'name': '2015.03', 'type': 'dir'}] 'type': 'file'},
{'name': '2015.next.json',
'path': '2015.next.json',
'type': 'file'},
{'name': '2015.03',
'path': '2015.03',
'type': 'dir'},
{'name': 'test.2018.02.json',
'path': 'add-ons/test.2018.02.json',
'type': 'file'},
{'name': 'test.next.json',
'path': 'add-ons/test.next.json',
'type': 'file'}]
content = json.dumps(content) content = json.dumps(content)
return httmock.response(200, content, headers, None, 5, request) return httmock.response(200, content, headers, None, 5, request)
with httmock.HTTMock(github_api_mock): with httmock.HTTMock(github_api_mock):
result = self.guidelines.get_guideline_list() result = self.guidelines.get_guideline_list()
self.assertEqual(['2015.03.json'], result) print(result)
expected_keys = ['powered', u'test']
expected_powered = [
{'name': u'2015.03.json',
'file': u'2015.03.json'},
{'name': u'2015.next.json',
'file': u'2015.next.json'}
]
expected_test_addons = [
{'name': u'2018.02.json',
'file': u'test.2018.02.json'},
{'name': u'next.json',
'file': u'test.next.json'}
]
self.assertIn('powered', expected_keys)
self.assertIn(u'test', expected_keys)
self.assertEqual(expected_powered,
result['powered'])
self.assertEqual(expected_test_addons,
result[u'test'])
def test_get_guidelines_list_error_code(self): def test_get_guidelines_list_error_code(self):
"""Test when the HTTP status code isn't a 200 OK.""" """Test when the HTTP status code isn't a 200 OK."""
@ -51,24 +83,25 @@ class GuidelinesTestCase(base.BaseTestCase):
with httmock.HTTMock(github_api_mock): with httmock.HTTMock(github_api_mock):
result = self.guidelines.get_guideline_list() result = self.guidelines.get_guideline_list()
self.assertIsNone(result) self.assertEqual(result, {'powered': []})
@mock.patch('requests.get') @mock.patch('requests.get')
def test_get_guidelines_exception(self, mock_requests_get): def test_get_guidelines_exception(self, mock_requests_get):
"""Test when the GET request raises an exception.""" """Test when the GET request raises an exception."""
mock_requests_get.side_effect = requests.exceptions.RequestException() mock_requests_get.side_effect = requests.exceptions.RequestException()
result = self.guidelines.get_guideline_list() result = self.guidelines.get_guideline_list()
self.assertIsNone(result) self.assertEqual(result, {'powered': []})
def test_get_capability_file(self): def test_get_capability_file(self):
"""Test when getting a specific guideline file""" """Test when getting a specific guideline file."""
@httmock.all_requests @httmock.all_requests
def github_mock(url, request): def github_mock(url, request):
content = {'foo': 'bar'} content = {'foo': 'bar'}
return httmock.response(200, content, None, None, 5, request) return httmock.response(200, content, None, None, 5, request)
with httmock.HTTMock(github_mock): with httmock.HTTMock(github_mock):
result = self.guidelines.get_guideline_contents('2010.03.json') gl_file_name = 'dns.2018.02.json'
result = self.guidelines.get_guideline_contents(gl_file_name)
self.assertEqual({'foo': 'bar'}, result) self.assertEqual({'foo': 'bar'}, result)
def test_get_capability_file_error_code(self): def test_get_capability_file_error_code(self):