diff --git a/README.md b/README.md index e02ec61..c6836ba 100644 --- a/README.md +++ b/README.md @@ -19,23 +19,23 @@ const isAvailable = await gcpMetadata.isAvailable(); #### Access all metadata ```js -const res = await gcpMetadata.instance(); -console.log(res.data); // ... All metadata properties +const data = await gcpMetadata.instance(); +console.log(data); // ... All metadata properties ``` #### Access specific properties ```js -const res = await gcpMetadata.instance('hostname'); -console.log(res.data) // ...All metadata properties +const data = await gcpMetadata.instance('hostname'); +console.log(data) // ...Instance hostname ``` #### Access specific properties with query parameters ```js -const res = await gcpMetadata.instance({ +const data = await gcpMetadata.instance({ property: 'tags', params: { alt: 'text' } }); -console.log(res.data) // ...Tags as newline-delimited list +console.log(data) // ...Tags as newline-delimited list ``` [circle]: https://circleci.com/gh/stephenplusplus/gcp-metadata diff --git a/package.json b/package.json index 5ea0a51..0126b74 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gcp-metadata", - "version": "0.6.5", + "version": "0.7.0", "description": "Get the metadata from a Google Cloud Platform environment", "repository": "stephenplusplus/gcp-metadata", "main": "./build/src/index.js", @@ -33,11 +33,9 @@ "license": "MIT", "dependencies": { "axios": "^0.18.0", - "extend": "^3.0.1", "retry-axios": "0.3.2" }, "devDependencies": { - "@types/extend": "^3.0.0", "@types/mocha": "^5.2.4", "@types/ncp": "^2.0.1", "@types/nock": "^9.1.3", diff --git a/src/index.ts b/src/index.ts index ff45daf..6d17e89 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,4 @@ -import axios, {AxiosError, AxiosRequestConfig, AxiosResponse} from 'axios'; -import extend from 'extend'; +import axios from 'axios'; import * as rax from 'retry-axios'; export const HOST_ADDRESS = 'http://metadata.google.internal'; @@ -9,30 +8,33 @@ export const HEADER_NAME = 'Metadata-Flavor'; export const HEADER_VALUE = 'Google'; export const HEADERS = Object.freeze({[HEADER_NAME]: HEADER_VALUE}); -export type Options = AxiosRequestConfig& - {[index: string]: {} | string | undefined, property?: string, uri?: string}; +export interface Options { + params?: {[index: string]: string}; + property?: string; +} -// Accepts an options object passed from the user to the API. In the -// previous version of the API, it referred to a `Request` options object. -// Now it refers to an Axios Request Config object. This is here to help -// ensure users don't pass invalid options when they upgrade from 0.4 to 0.5. +// Accepts an options object passed from the user to the API. In previous +// versions of the API, it referred to a `Request` or an `Axios` request +// options object. Now it refers to an object with very limited property +// names. This is here to help ensure users don't pass invalid options when +// they upgrade from 0.4 to 0.5 to 0.8. function validate(options: Options) { - const vpairs = [ - {invalid: 'uri', expected: 'url'}, {invalid: 'json', expected: 'data'}, - {invalid: 'qs', expected: 'params'} - ]; - for (const pair of vpairs) { - if (options[pair.invalid]) { - const e = `'${ - pair.invalid}' is not a valid configuration option. Please use '${ - pair.expected}' instead. This library is using Axios for requests. Please see https://github.com/axios/axios to learn more about the valid request options.`; - throw new Error(e); + Object.keys(options).forEach(key => { + switch (key) { + case 'params': + case 'property': + break; + case 'qs': + throw new Error( + `'qs' is not a valid configuration option. Please use 'params' instead.`); + default: + throw new Error(`'${key}' is not a valid configuration option.`); } - } + }); } -async function metadataAccessor( - type: string, options?: string|Options, noResponseRetries = 3) { +async function metadataAccessor( + type: string, options?: string|Options, noResponseRetries = 3): Promise { options = options || {}; if (typeof options === 'string') { options = {property: options}; @@ -44,38 +46,38 @@ async function metadataAccessor( validate(options); const ax = axios.create(); rax.attach(ax); - const baseOpts = { + const reqOpts = { url: `${BASE_URL}/${type}${property}`, headers: Object.assign({}, HEADERS), - raxConfig: {noResponseRetries, instance: ax} + raxConfig: {noResponseRetries, instance: ax}, + params: options.params }; - const reqOpts = extend(true, baseOpts, options); - delete (reqOpts as {property: string}).property; - return ax.request(reqOpts) - .then(res => { - // NOTE: node.js converts all incoming headers to lower case. - if (res.headers[HEADER_NAME.toLowerCase()] !== HEADER_VALUE) { - throw new Error(`Invalid response from metadata service: incorrect ${ - HEADER_NAME} header.`); - } else if (!res.data) { - throw new Error('Invalid response from the metadata service'); - } - return res; - }) - .catch((err: AxiosError) => { - if (err.response && err.response.status !== 200) { - err.message = 'Unsuccessful response status code. ' + err.message; - } - throw err; - }); + try { + const res = await ax.request(reqOpts); + // NOTE: node.js converts all incoming headers to lower case. + if (res.headers[HEADER_NAME.toLowerCase()] !== HEADER_VALUE) { + throw new Error(`Invalid response from metadata service: incorrect ${ + HEADER_NAME} header.`); + } else if (!res.data) { + throw new Error('Invalid response from the metadata service'); + } + return res.data; + } catch (e) { + if (e.response && e.response.status !== 200) { + e.message = `Unsuccessful response status code. ${e.message}`; + } + throw e; + } } -export function instance(options?: string|Options) { - return metadataAccessor('instance', options); +// tslint:disable-next-line no-any +export function instance(options?: string|Options) { + return metadataAccessor('instance', options); } -export function project(options?: string|Options) { - return metadataAccessor('project', options); +// tslint:disable-next-line no-any +export function project(options?: string|Options) { + return metadataAccessor('project', options); } /** diff --git a/test/fixtures/kitchen/src/index.ts b/test/fixtures/kitchen/src/index.ts index 7715258..cbe22b1 100644 --- a/test/fixtures/kitchen/src/index.ts +++ b/test/fixtures/kitchen/src/index.ts @@ -9,6 +9,6 @@ async function main() { const v = await gcp.instance('/somepath'); } -gcp.project('something').then(x => console.log); +gcp.project('something').then(console.log); main().catch(console.error); \ No newline at end of file diff --git a/test/index.test.ts b/test/index.test.ts index c159e6a..c07bfc3 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -1,7 +1,5 @@ import assert from 'assert'; -import extend from 'extend'; import nock from 'nock'; - import * as gcp from '../src'; assert.rejects = require('assert-rejects'); @@ -30,20 +28,18 @@ it('should create the correct accessors', async () => { }); it('should access all the metadata properly', async () => { - const scope = nock(HOST).get(`${PATH}/${TYPE}`).reply(200, {}, HEADERS); - const res = await gcp.instance(); + const scope = nock(HOST) + .get(`${PATH}/${TYPE}`, undefined, HEADERS) + .reply(200, {}, HEADERS); + await gcp.instance(); scope.done(); - assert(res.config.url, `${BASE_URL}/${TYPE}`); - assert(res.config.headers[HEADER_NAME], gcp.HEADER_VALUE); - assert(res.headers[HEADER_NAME.toLowerCase()], gcp.HEADER_VALUE); }); it('should access a specific metadata property', async () => { const scope = nock(HOST).get(`${PATH}/${TYPE}/${PROPERTY}`).reply(200, {}, HEADERS); - const res = await gcp.instance(PROPERTY); + await gcp.instance(PROPERTY); scope.done(); - assert(res.config.url, `${BASE_URL}/${TYPE}/${PROPERTY}`); }); it('should accept an object with property and query fields', async () => { @@ -52,22 +48,8 @@ it('should accept an object with property and query fields', async () => { .get(`${PATH}/project/${PROPERTY}`) .query(QUERY) .reply(200, {}, HEADERS); - const res = await gcp.project({property: PROPERTY, params: QUERY}); - scope.done(); - assert(JSON.stringify(res.config.params), JSON.stringify(QUERY)); - assert(res.config.url, `${BASE_URL}/${TYPE}/${PROPERTY}`); -}); - -it('should extend the request options', async () => { - const options = {property: PROPERTY, headers: {'Custom-Header': 'Custom'}}; - const originalOptions = extend(true, {}, options); - const scope = - nock(HOST).get(`${PATH}/${TYPE}/${PROPERTY}`).reply(200, {}, HEADERS); - const res = await gcp.instance(options); + await gcp.project({property: PROPERTY, params: QUERY}); scope.done(); - assert(res.config.url, `${BASE_URL}/${TYPE}/${PROPERTY}`); - assert(res.config.headers['Custom-Header'], 'Custom'); - assert.deepStrictEqual(options, originalOptions); // wasn't modified }); it('should return the request error', async () => { @@ -107,15 +89,21 @@ it('should retry if the initial request fails', async () => { .reply(500) .get(`${PATH}/${TYPE}`) .reply(200, {}, HEADERS); - const res = await gcp.instance(); + await gcp.instance(); scope.done(); - assert(res.config.url, `${BASE_URL}/${TYPE}`); }); it('should throw if request options are passed', async () => { await assert.rejects( // tslint:disable-next-line no-any - (gcp as any).instance({qs: {one: 'two'}}), /\'qs\' is not a valid/); + gcp.instance({qs: {one: 'two'}} as any), + /\'qs\' is not a valid configuration option. Please use \'params\' instead\./); +}); + +it('should throw if invalid options are passed', async () => { + await assert.rejects( + // tslint:disable-next-line no-any + gcp.instance({fake: 'news'} as any), /\'fake\' is not a valid/); }); it('should retry on DNS errors', async () => { @@ -124,9 +112,9 @@ it('should retry on DNS errors', async () => { .replyWithError({code: 'ETIMEDOUT'}) .get(`${PATH}/${TYPE}`) .reply(200, {}, HEADERS); - const res = await gcp.instance(); + const data = await gcp.instance(); scope.done(); - assert(res.data); + assert(data); }); it('should report isGCE if the server returns a 500 first', async () => { @@ -138,7 +126,7 @@ it('should report isGCE if the server returns a 500 first', async () => { .reply(200, {}, HEADERS); const isGCE = await gcp.isAvailable(); scope.done(); - assert(isGCE); + assert.equal(isGCE, true); }); it('should fail fast on isAvailable if ENOTFOUND is returned', async () => { @@ -146,7 +134,7 @@ it('should fail fast on isAvailable if ENOTFOUND is returned', async () => { nock(HOST).get(`${PATH}/${TYPE}`).replyWithError({code: 'ENOTFOUND'}); const isGCE = await gcp.isAvailable(); scope.done(); - assert.equal(isGCE, false); + assert.equal(false, isGCE); }); it('should fail fast on isAvailable if ENOENT is returned', async () => { @@ -154,7 +142,7 @@ it('should fail fast on isAvailable if ENOENT is returned', async () => { nock(HOST).get(`${PATH}/${TYPE}`).replyWithError({code: 'ENOENT'}); const isGCE = await gcp.isAvailable(); scope.done(); - assert.equal(isGCE, false); + assert.equal(false, isGCE); }); it('should throw on unexpected errors', async () => {