Added Glance Service

This glance service follows the same pattern as the keystone service,
and provides both version negotiation and endpoint discovery. Unlike
the keystone service, however, it is configured using an endpoint
configuration object as discovered from the Keystone service catalog.

Change-Id: I0fe2bc9690022688e2cb80b9ca1b10bcea86c13d
This commit is contained in:
Michael Krotscheck 2016-08-26 11:33:00 -07:00
parent 3bce487421
commit 7a64d24795
6 changed files with 510 additions and 3 deletions

View File

@ -5,9 +5,17 @@ import path from 'path';
function getDevstackConfig() {
const karmaConfig = karma.parseConfig(path.resolve('./karma.conf.js'));
return "[[post-config|$KEYSTONE_CONF]]\n" +
"[cors]\n" +
"allowed_origin=http://localhost:" + karmaConfig.port + "\n";
return getCorsConfig('$KEYSTONE_CONF', karmaConfig) +
getCorsConfig('$GLANCE_API_CONF', karmaConfig);
}
function getCorsConfig(service, karmaConfig) {
return `[[post-config|${service}]]
[cors]
allowed_origin=http://localhost:${karmaConfig.port}
`;
}
fs.appendFile(process.env.BASE + '/new/devstack/local.conf', getDevstackConfig(), (err) => {

109
src/glance.js Normal file
View File

@ -0,0 +1,109 @@
/*
* Copyright (c) 2016 Hewlett Packard Enterprise Development L.P.
*
* 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
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See
* the License for the specific language governing permissions and limitations
* under the License.
*/
import Http from './util/http';
/**
* A list of all supported versions. Please keep this array sorted by most recent.
*
* @type {Array}
* @ignore
*/
const supportedGlanceVersions = [
'v2.3'
];
export default class Glance {
/**
* This class provides direct, idempotent, low-level access to the Glance API of a specific
* cloud. The constructor requires that you provide a specific glance interface endpoint
* descriptor, as received from keystone's catalog list.
*
* @example
* {
* region_id: "RegionOne",
* url: "http://127.0.0.1:9292",
* region: "RegionOne",
* interface: "admin",
* id: "0b8b5f0f14904136ab5a4f83f27ec49a"
* }
* @param {{}} endpointConfig The configuration element for a specific glance endpoint.
*/
constructor (endpointConfig) {
// Sanity checks.
if (!endpointConfig || !endpointConfig.url) {
throw new Error('An endpoint configuration is required.');
}
// Clone the config, so that this instance is immutable
// at runtime (no modifying the config after the fact).
this._config = Object.assign({}, endpointConfig);
this.http = new Http();
}
/**
* Retrieve all the API versions available.
*
* @returns {Promise.<T>} A promise that will resolve with the list of API versions.
*/
versions () {
return this.http
.httpGet(this._config.url)
.then((response) => response.json())
.then((body) => body.versions);
}
/**
* Retrieve the API version declaration that is currently in use by this glance API.
*
* @returns {Promise.<T>} A promise that will resolve with the specific API version.
*/
version () {
return this
.versions()
.then((versions) => {
const version = versions.find((element) => {
return supportedGlanceVersions.indexOf(element.id) > -1;
});
if (version) {
return version;
}
throw new Error("No supported Glance API version available.");
});
}
/**
* Return the root API endpoint for the current supported glance version.
*
* @returns {Promise.<T>|*} A promise which will resolve with the endpoint URL string.
*/
serviceEndpoint () {
if (!this._endpointPromise) {
this._endpointPromise = this.version()
.then((version) => {
if (version.links) {
for (let i = 0; i < version.links.length; i++) {
let link = version.links[i];
if (link.rel === 'self' && link.href) {
return link.href;
}
}
}
throw new Error("No service endpoint discovered.");
});
}
return this._endpointPromise;
}
}

View File

@ -0,0 +1,82 @@
/*
* Copyright (c) 2016 Hewlett Packard Enterprise Development L.P.
*
* 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
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See
* the License for the specific language governing permissions and limitations
* under the License.
*/
import config from "./helpers/cloudsConfig";
import Version from '../../src/util/version';
import Glance from "../../src/glance";
import Keystone from "../../src/keystone";
import log from 'loglevel';
log.setLevel("DEBUG");
describe("Glance", () => {
// Create a keystone instance and extract the glance API endpoint.
let devstackConfig = config.clouds.devstack;
let keystone = new Keystone(devstackConfig);
let tokenPromise = keystone.tokenIssue();
let configPromise = tokenPromise
.then((token) => keystone.catalogList(token))
.then((catalog) => catalog.find((entry) => entry.name === 'glance'))
.then((entry) => entry.endpoints.find((endpoint) => endpoint.interface === 'public'));
describe("versions()", () => {
it("should return a list of all versions available on this clouds' glance", (done) => {
configPromise
.then((config) => new Glance(config))
.then((glance) => glance.versions())
.then((versions) => {
// Quick sanity check.
expect(versions.length > 0).toBeTruthy();
done();
})
.catch((error) => done.fail(error));
});
});
describe("version()", () => {
const supportedApiVersions = [
new Version('image 2.3')
];
/**
* This test acts as a canary, to inform the SDK developers that the Glance API
* has changed in a significant way.
*/
it("should return a supported version.", (done) => {
configPromise
.then((config) => new Glance(config))
.then((glance) => glance.version())
.then((version) => {
// Quick sanity check.
const apiVersion = new Version('image', version.id);
for (let i = 0; i < supportedApiVersions.length; i++) {
let supportedVersion = supportedApiVersions[i];
if (apiVersion.equals(supportedVersion)) {
done();
return;
}
}
fail("Current devstack glance version is not supported.");
done();
})
.catch((error) => done.fail(error));
});
});
});

190
test/unit/glanceTest.js Normal file
View File

@ -0,0 +1,190 @@
/*
* Copyright (c) 2016 Hewlett Packard Enterprise Development L.P.
*
* 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
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See
* the License for the specific language governing permissions and limitations
* under the License.
*/
import Glance from '../../src/glance.js';
import * as mockData from './helpers/data/glance';
import fetchMock from 'fetch-mock';
describe('Glance', () => {
afterEach(fetchMock.restore);
it('should export a class', () => {
const glance = new Glance(mockData.config);
expect(glance).toBeDefined();
});
it('should throw an error for an empty config', () => {
try {
const glance = new Glance();
glance.versions();
} catch (e) {
expect(e.message).toEqual('An endpoint configuration is required.');
}
});
describe("versions()", () => {
it("Should return a list of all versions available on this clouds' glance", (done) => {
const glance = new Glance(mockData.config);
fetchMock.mock(mockData.root());
glance.versions()
.then((versions) => {
// Quick sanity check.
expect(versions.length).toBe(6);
done();
})
.catch((error) => done.fail(error));
});
it("Should NOT cache its results", (done) => {
const glance = new Glance(mockData.config);
const mockOptions = mockData.root();
fetchMock.mock(mockOptions);
glance.versions()
.then(() => {
// Validate that the mock has only been invoked once
expect(fetchMock.calls(mockOptions.name).length).toEqual(1);
return glance.versions();
})
.then(() => {
expect(fetchMock.calls(mockOptions.name).length).toEqual(2);
done();
})
.catch((error) => done.fail(error));
});
});
describe("version()", () => {
it("Should return a supported version of the glance API.", (done) => {
const glance = new Glance(mockData.config);
fetchMock.mock(mockData.root());
glance.version()
.then((version) => {
expect(version.id).toEqual('v2.3');
done();
})
.catch((error) => done.fail(error));
});
it("Should throw an exception if no supported version is found.", (done) => {
const glance = new Glance(mockData.config);
// Build an invalid mock object.
const mockOptions = mockData.root();
mockOptions.response.versions.shift();
fetchMock.mock(mockOptions);
glance.version()
.then((response) => done.fail(response))
.catch((error) => {
expect(error).not.toBeNull();
done();
});
});
it("Should NOT cache its results", (done) => {
const glance = new Glance(mockData.config);
const mockOptions = mockData.root();
fetchMock.mock(mockOptions);
glance.version()
.then(() => {
// Validate that the mock has only been invoked once
expect(fetchMock.calls(mockOptions.name).length).toEqual(1);
return glance.version();
})
.then(() => {
expect(fetchMock.calls(mockOptions.name).length).toEqual(2);
done();
})
.catch((error) => done.fail(error));
});
});
describe("serviceEndpoint()", () => {
it("Should return a valid endpoint to the glance API.", (done) => {
const glance = new Glance(mockData.config);
fetchMock.mock(mockData.root());
glance.serviceEndpoint()
.then((endpoint) => {
expect(endpoint).toEqual('http://192.168.99.99:9292/v2/');
done();
})
.catch((error) => done.fail(error));
});
it("Should throw an exception if no endpoint is provided.", (done) => {
const glance = new Glance(mockData.config);
// Build an exception payload.
const mockOptions = mockData.root();
mockOptions.response.versions[0].links = [];
fetchMock.mock(mockOptions);
glance.serviceEndpoint()
.then((response) => done.fail(response))
.catch((error) => {
expect(error).not.toBeNull();
done();
});
});
it("Should throw an exception if no links array exists.", (done) => {
const glance = new Glance(mockData.config);
// Build an exception payload.
const mockOptions = mockData.root();
delete mockOptions.response.versions[0].links;
fetchMock.mock(mockOptions);
glance.serviceEndpoint()
.then((response) => done.fail(response))
.catch((error) => {
expect(error).not.toBeNull();
done();
});
});
it("Should cache its results", (done) => {
const glance = new Glance(mockData.config);
const mockOptions = mockData.root();
fetchMock.mock(mockOptions);
glance.serviceEndpoint()
.then(() => {
// Validate that the mock has only been invoked once
expect(fetchMock.calls(mockOptions.name).length).toEqual(1);
return glance.serviceEndpoint();
})
.then(() => {
expect(fetchMock.calls(mockOptions.name).length).toEqual(1);
done();
})
.catch((error) => done.fail(error));
});
});
});

View File

@ -0,0 +1,114 @@
/*
* Copyright (c) 2016 Hewlett Packard Enterprise Development L.P.
*
* 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
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See
* the License for the specific language governing permissions and limitations
* under the License.
*/
/**
* This file contains test data for fetchMock, to simplify bootstrapping of unit tests for
* keystone. Most of these are functions, as FetchMock does not perform a safe clone of the
* instances, and may accidentally modify them at runtime.
*/
/**
* Mock cloud configuration that matches our test data below. This is not a full clouds.yaml
* format, rather just the subsection pointing to a particular cloud.
*/
const glanceConfig = {
region_id: "RegionOne",
url: "http://192.168.99.99:9292/",
region: "RegionOne",
interface: "public",
id: "0b8b5f0f14904136ab5a4f83f27ec49a"
};
/**
* Build a new FetchMock configuration for the root endpoint.
*
* @returns {{}} A full FetchMock configuration for Glance's Root Resource.
*/
function rootResponse () {
return {
method: 'GET',
matcher: 'http://192.168.99.99:9292/',
response: {
versions: [
{
status: "CURRENT",
id: "v2.3",
links: [
{
href: "http://192.168.99.99:9292/v2/",
rel: "self"
}
]
},
{
status: "SUPPORTED",
id: "v2.2",
links: [
{
href: "http://192.168.99.99:9292/v2/",
rel: "self"
}
]
},
{
status: "SUPPORTED",
id: "v2.1",
links: [
{
href: "http://192.168.99.99:9292/v2/",
rel: "self"
}
]
},
{
status: "SUPPORTED",
id: "v2.0",
links: [
{
href: "http://192.168.99.99:9292/v2/",
rel: "self"
}
]
},
{
status: "SUPPORTED",
id: "v1.1",
links: [
{
href: "http://192.168.99.99:9292/v1/",
rel: "self"
}
]
},
{
status: "SUPPORTED",
id: "v1.0",
links: [
{
href: "http://192.168.99.99:9292/v1/",
rel: "self"
}
]
}
]
}
};
}
export {
glanceConfig as config,
rootResponse as root
};

View File

@ -46,6 +46,10 @@ RECLONE=True
[[post-config|\$KEYSTONE_CONF]]
[cors]
allowed_origin=http://localhost:9876
[[post-config|\$GLANCE_API_CONF]]
[cors]
allowed_origin=http://localhost:9876
EOL
# Start devstack.