Browse Source

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
Michael Krotscheck 2 years ago
parent
commit
7a64d24795
6 changed files with 510 additions and 3 deletions
  1. 11
    3
      configure-devstack.js
  2. 109
    0
      src/glance.js
  3. 82
    0
      test/functional/glanceTest.js
  4. 190
    0
      test/unit/glanceTest.js
  5. 114
    0
      test/unit/helpers/data/glance.js
  6. 4
    0
      vagrant.sh

+ 11
- 3
configure-devstack.js View File

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

+ 109
- 0
src/glance.js View File

@@ -0,0 +1,109 @@
1
+/*
2
+ * Copyright (c) 2016 Hewlett Packard Enterprise Development L.P.
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
5
+ * use this file except in compliance with the License. You may obtain a copy
6
+ * of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See
13
+ * the License for the specific language governing permissions and limitations
14
+ * under the License.
15
+ */
16
+import Http from './util/http';
17
+
18
+/**
19
+ * A list of all supported versions. Please keep this array sorted by most recent.
20
+ *
21
+ * @type {Array}
22
+ * @ignore
23
+ */
24
+const supportedGlanceVersions = [
25
+  'v2.3'
26
+];
27
+
28
+export default class Glance {
29
+
30
+  /**
31
+   * This class provides direct, idempotent, low-level access to the Glance API of a specific
32
+   * cloud. The constructor requires that you provide a specific glance interface endpoint
33
+   * descriptor, as received from keystone's catalog list.
34
+   *
35
+   * @example
36
+   * {
37
+   *   region_id: "RegionOne",
38
+   *   url: "http://127.0.0.1:9292",
39
+   *   region: "RegionOne",
40
+   *   interface: "admin",
41
+   *   id: "0b8b5f0f14904136ab5a4f83f27ec49a"
42
+   * }
43
+   * @param {{}} endpointConfig The configuration element for a specific glance endpoint.
44
+   */
45
+  constructor (endpointConfig) {
46
+    // Sanity checks.
47
+    if (!endpointConfig || !endpointConfig.url) {
48
+      throw new Error('An endpoint configuration is required.');
49
+    }
50
+    // Clone the config, so that this instance is immutable
51
+    // at runtime (no modifying the config after the fact).
52
+    this._config = Object.assign({}, endpointConfig);
53
+    this.http = new Http();
54
+  }
55
+
56
+  /**
57
+   * Retrieve all the API versions available.
58
+   *
59
+   * @returns {Promise.<T>} A promise that will resolve with the list of API versions.
60
+   */
61
+  versions () {
62
+    return this.http
63
+      .httpGet(this._config.url)
64
+      .then((response) => response.json())
65
+      .then((body) => body.versions);
66
+  }
67
+
68
+  /**
69
+   * Retrieve the API version declaration that is currently in use by this glance API.
70
+   *
71
+   * @returns {Promise.<T>} A promise that will resolve with the specific API version.
72
+   */
73
+  version () {
74
+    return this
75
+      .versions()
76
+      .then((versions) => {
77
+        const version = versions.find((element) => {
78
+          return supportedGlanceVersions.indexOf(element.id) > -1;
79
+        });
80
+        if (version) {
81
+          return version;
82
+        }
83
+        throw new Error("No supported Glance API version available.");
84
+      });
85
+  }
86
+
87
+  /**
88
+   * Return the root API endpoint for the current supported glance version.
89
+   *
90
+   * @returns {Promise.<T>|*} A promise which will resolve with the endpoint URL string.
91
+   */
92
+  serviceEndpoint () {
93
+    if (!this._endpointPromise) {
94
+      this._endpointPromise = this.version()
95
+        .then((version) => {
96
+          if (version.links) {
97
+            for (let i = 0; i < version.links.length; i++) {
98
+              let link = version.links[i];
99
+              if (link.rel === 'self' && link.href) {
100
+                return link.href;
101
+              }
102
+            }
103
+          }
104
+          throw new Error("No service endpoint discovered.");
105
+        });
106
+    }
107
+    return this._endpointPromise;
108
+  }
109
+}

+ 82
- 0
test/functional/glanceTest.js View File

@@ -0,0 +1,82 @@
1
+/*
2
+ * Copyright (c) 2016 Hewlett Packard Enterprise Development L.P.
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
5
+ * use this file except in compliance with the License. You may obtain a copy
6
+ * of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See
13
+ * the License for the specific language governing permissions and limitations
14
+ * under the License.
15
+ */
16
+
17
+import config from "./helpers/cloudsConfig";
18
+import Version from '../../src/util/version';
19
+import Glance from "../../src/glance";
20
+import Keystone from "../../src/keystone";
21
+import log from 'loglevel';
22
+
23
+log.setLevel("DEBUG");
24
+
25
+describe("Glance", () => {
26
+  // Create a keystone instance and extract the glance API endpoint.
27
+  let devstackConfig = config.clouds.devstack;
28
+  let keystone = new Keystone(devstackConfig);
29
+  let tokenPromise = keystone.tokenIssue();
30
+
31
+  let configPromise = tokenPromise
32
+    .then((token) => keystone.catalogList(token))
33
+    .then((catalog) => catalog.find((entry) => entry.name === 'glance'))
34
+    .then((entry) => entry.endpoints.find((endpoint) => endpoint.interface === 'public'));
35
+
36
+  describe("versions()", () => {
37
+    it("should return a list of all versions available on this clouds' glance", (done) => {
38
+      configPromise
39
+        .then((config) => new Glance(config))
40
+        .then((glance) => glance.versions())
41
+        .then((versions) => {
42
+          // Quick sanity check.
43
+          expect(versions.length > 0).toBeTruthy();
44
+          done();
45
+        })
46
+        .catch((error) => done.fail(error));
47
+    });
48
+  });
49
+
50
+  describe("version()", () => {
51
+
52
+    const supportedApiVersions = [
53
+      new Version('image 2.3')
54
+    ];
55
+
56
+    /**
57
+     * This test acts as a canary, to inform the SDK developers that the Glance API
58
+     * has changed in a significant way.
59
+     */
60
+    it("should return a supported version.", (done) => {
61
+      configPromise
62
+        .then((config) => new Glance(config))
63
+        .then((glance) => glance.version())
64
+        .then((version) => {
65
+
66
+          // Quick sanity check.
67
+          const apiVersion = new Version('image', version.id);
68
+
69
+          for (let i = 0; i < supportedApiVersions.length; i++) {
70
+            let supportedVersion = supportedApiVersions[i];
71
+            if (apiVersion.equals(supportedVersion)) {
72
+              done();
73
+              return;
74
+            }
75
+          }
76
+          fail("Current devstack glance version is not supported.");
77
+          done();
78
+        })
79
+        .catch((error) => done.fail(error));
80
+    });
81
+  });
82
+});

+ 190
- 0
test/unit/glanceTest.js View File

@@ -0,0 +1,190 @@
1
+/*
2
+ * Copyright (c) 2016 Hewlett Packard Enterprise Development L.P.
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
5
+ * use this file except in compliance with the License. You may obtain a copy
6
+ * of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See
13
+ * the License for the specific language governing permissions and limitations
14
+ * under the License.
15
+ */
16
+
17
+import Glance from '../../src/glance.js';
18
+import * as mockData from './helpers/data/glance';
19
+import fetchMock from 'fetch-mock';
20
+
21
+describe('Glance', () => {
22
+
23
+  afterEach(fetchMock.restore);
24
+
25
+  it('should export a class', () => {
26
+    const glance = new Glance(mockData.config);
27
+    expect(glance).toBeDefined();
28
+  });
29
+
30
+  it('should throw an error for an empty config', () => {
31
+    try {
32
+      const glance = new Glance();
33
+      glance.versions();
34
+    } catch (e) {
35
+      expect(e.message).toEqual('An endpoint configuration is required.');
36
+    }
37
+  });
38
+
39
+  describe("versions()", () => {
40
+    it("Should return a list of all versions available on this clouds' glance", (done) => {
41
+      const glance = new Glance(mockData.config);
42
+
43
+      fetchMock.mock(mockData.root());
44
+
45
+      glance.versions()
46
+        .then((versions) => {
47
+          // Quick sanity check.
48
+          expect(versions.length).toBe(6);
49
+          done();
50
+        })
51
+        .catch((error) => done.fail(error));
52
+    });
53
+
54
+    it("Should NOT cache its results", (done) => {
55
+      const glance = new Glance(mockData.config);
56
+      const mockOptions = mockData.root();
57
+
58
+      fetchMock.mock(mockOptions);
59
+
60
+      glance.versions()
61
+        .then(() => {
62
+          // Validate that the mock has only been invoked once
63
+          expect(fetchMock.calls(mockOptions.name).length).toEqual(1);
64
+          return glance.versions();
65
+        })
66
+        .then(() => {
67
+          expect(fetchMock.calls(mockOptions.name).length).toEqual(2);
68
+          done();
69
+        })
70
+        .catch((error) => done.fail(error));
71
+    });
72
+  });
73
+
74
+  describe("version()", () => {
75
+
76
+    it("Should return a supported version of the glance API.", (done) => {
77
+      const glance = new Glance(mockData.config);
78
+
79
+      fetchMock.mock(mockData.root());
80
+
81
+      glance.version()
82
+        .then((version) => {
83
+          expect(version.id).toEqual('v2.3');
84
+          done();
85
+        })
86
+        .catch((error) => done.fail(error));
87
+    });
88
+
89
+    it("Should throw an exception if no supported version is found.", (done) => {
90
+      const glance = new Glance(mockData.config);
91
+
92
+      // Build an invalid mock object.
93
+      const mockOptions = mockData.root();
94
+      mockOptions.response.versions.shift();
95
+
96
+      fetchMock.mock(mockOptions);
97
+
98
+      glance.version()
99
+        .then((response) => done.fail(response))
100
+        .catch((error) => {
101
+          expect(error).not.toBeNull();
102
+          done();
103
+        });
104
+    });
105
+
106
+    it("Should NOT cache its results", (done) => {
107
+      const glance = new Glance(mockData.config);
108
+      const mockOptions = mockData.root();
109
+      fetchMock.mock(mockOptions);
110
+
111
+      glance.version()
112
+        .then(() => {
113
+          // Validate that the mock has only been invoked once
114
+          expect(fetchMock.calls(mockOptions.name).length).toEqual(1);
115
+          return glance.version();
116
+        })
117
+        .then(() => {
118
+          expect(fetchMock.calls(mockOptions.name).length).toEqual(2);
119
+          done();
120
+        })
121
+        .catch((error) => done.fail(error));
122
+    });
123
+  });
124
+
125
+  describe("serviceEndpoint()", () => {
126
+
127
+    it("Should return a valid endpoint to the glance API.", (done) => {
128
+      const glance = new Glance(mockData.config);
129
+
130
+      fetchMock.mock(mockData.root());
131
+
132
+      glance.serviceEndpoint()
133
+        .then((endpoint) => {
134
+          expect(endpoint).toEqual('http://192.168.99.99:9292/v2/');
135
+          done();
136
+        })
137
+        .catch((error) => done.fail(error));
138
+    });
139
+
140
+    it("Should throw an exception if no endpoint is provided.", (done) => {
141
+      const glance = new Glance(mockData.config);
142
+
143
+      // Build an exception payload.
144
+      const mockOptions = mockData.root();
145
+      mockOptions.response.versions[0].links = [];
146
+      fetchMock.mock(mockOptions);
147
+
148
+      glance.serviceEndpoint()
149
+        .then((response) => done.fail(response))
150
+        .catch((error) => {
151
+          expect(error).not.toBeNull();
152
+          done();
153
+        });
154
+    });
155
+
156
+    it("Should throw an exception if no links array exists.", (done) => {
157
+      const glance = new Glance(mockData.config);
158
+
159
+      // Build an exception payload.
160
+      const mockOptions = mockData.root();
161
+      delete mockOptions.response.versions[0].links;
162
+      fetchMock.mock(mockOptions);
163
+
164
+      glance.serviceEndpoint()
165
+        .then((response) => done.fail(response))
166
+        .catch((error) => {
167
+          expect(error).not.toBeNull();
168
+          done();
169
+        });
170
+    });
171
+
172
+    it("Should cache its results", (done) => {
173
+      const glance = new Glance(mockData.config);
174
+      const mockOptions = mockData.root();
175
+      fetchMock.mock(mockOptions);
176
+
177
+      glance.serviceEndpoint()
178
+        .then(() => {
179
+          // Validate that the mock has only been invoked once
180
+          expect(fetchMock.calls(mockOptions.name).length).toEqual(1);
181
+          return glance.serviceEndpoint();
182
+        })
183
+        .then(() => {
184
+          expect(fetchMock.calls(mockOptions.name).length).toEqual(1);
185
+          done();
186
+        })
187
+        .catch((error) => done.fail(error));
188
+    });
189
+  });
190
+});

+ 114
- 0
test/unit/helpers/data/glance.js View File

@@ -0,0 +1,114 @@
1
+/*
2
+ * Copyright (c) 2016 Hewlett Packard Enterprise Development L.P.
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
5
+ * use this file except in compliance with the License. You may obtain a copy
6
+ * of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See
13
+ * the License for the specific language governing permissions and limitations
14
+ * under the License.
15
+ */
16
+
17
+/**
18
+ * This file contains test data for fetchMock, to simplify bootstrapping of unit tests for
19
+ * keystone. Most of these are functions, as FetchMock does not perform a safe clone of the
20
+ * instances, and may accidentally modify them at runtime.
21
+ */
22
+
23
+/**
24
+ * Mock cloud configuration that matches our test data below. This is not a full clouds.yaml
25
+ * format, rather just the subsection pointing to a particular cloud.
26
+ */
27
+const glanceConfig = {
28
+  region_id: "RegionOne",
29
+  url: "http://192.168.99.99:9292/",
30
+  region: "RegionOne",
31
+  interface: "public",
32
+  id: "0b8b5f0f14904136ab5a4f83f27ec49a"
33
+};
34
+
35
+/**
36
+ * Build a new FetchMock configuration for the root endpoint.
37
+ *
38
+ * @returns {{}} A full FetchMock configuration for Glance's Root Resource.
39
+ */
40
+function rootResponse () {
41
+  return {
42
+    method: 'GET',
43
+    matcher: 'http://192.168.99.99:9292/',
44
+    response: {
45
+      versions: [
46
+        {
47
+          status: "CURRENT",
48
+          id: "v2.3",
49
+          links: [
50
+            {
51
+              href: "http://192.168.99.99:9292/v2/",
52
+              rel: "self"
53
+            }
54
+          ]
55
+        },
56
+        {
57
+          status: "SUPPORTED",
58
+          id: "v2.2",
59
+          links: [
60
+            {
61
+              href: "http://192.168.99.99:9292/v2/",
62
+              rel: "self"
63
+            }
64
+          ]
65
+        },
66
+        {
67
+          status: "SUPPORTED",
68
+          id: "v2.1",
69
+          links: [
70
+            {
71
+              href: "http://192.168.99.99:9292/v2/",
72
+              rel: "self"
73
+            }
74
+          ]
75
+        },
76
+        {
77
+          status: "SUPPORTED",
78
+          id: "v2.0",
79
+          links: [
80
+            {
81
+              href: "http://192.168.99.99:9292/v2/",
82
+              rel: "self"
83
+            }
84
+          ]
85
+        },
86
+        {
87
+          status: "SUPPORTED",
88
+          id: "v1.1",
89
+          links: [
90
+            {
91
+              href: "http://192.168.99.99:9292/v1/",
92
+              rel: "self"
93
+            }
94
+          ]
95
+        },
96
+        {
97
+          status: "SUPPORTED",
98
+          id: "v1.0",
99
+          links: [
100
+            {
101
+              href: "http://192.168.99.99:9292/v1/",
102
+              rel: "self"
103
+            }
104
+          ]
105
+        }
106
+      ]
107
+    }
108
+  };
109
+}
110
+
111
+export {
112
+  glanceConfig as config,
113
+  rootResponse as root
114
+};

+ 4
- 0
vagrant.sh View File

@@ -46,6 +46,10 @@ RECLONE=True
46 46
 [[post-config|\$KEYSTONE_CONF]]
47 47
 [cors]
48 48
 allowed_origin=http://localhost:9876
49
+
50
+[[post-config|\$GLANCE_API_CONF]]
51
+[cors]
52
+allowed_origin=http://localhost:9876
49 53
 EOL
50 54
 
51 55
 # Start devstack.

Loading…
Cancel
Save