Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: quarantine axios config #62

Merged
merged 7 commits into from
Jul 11, 2018
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) // ...All metadata properties

This comment was marked as spam.

```

#### 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
Expand Down
2 changes: 0 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
85 changes: 43 additions & 42 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import axios, {AxiosError, AxiosRequestConfig, AxiosResponse} from 'axios';
import extend from 'extend';
import axios, {AxiosError, AxiosResponse} from 'axios';
import * as rax from 'retry-axios';

export const HOST_ADDRESS = 'http://metadata.google.internal';
Expand All @@ -9,30 +8,32 @@ 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.
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 => {

This comment was marked as spam.

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<T>(
type: string, options?: string|Options, noResponseRetries = 3): Promise<T> {
options = options || {};
if (typeof options === 'string') {
options = {property: options};
Expand All @@ -44,38 +45,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<T>(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<T = any>(options?: string|Options) {
return metadataAccessor<T>('instance', options);
}

export function project(options?: string|Options) {
return metadataAccessor('project', options);
// tslint:disable-next-line no-any
export function project<T = any>(options?: string|Options) {
return metadataAccessor<T>('project', options);
}

/**
Expand Down
2 changes: 1 addition & 1 deletion test/fixtures/kitchen/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
52 changes: 20 additions & 32 deletions test/index.test.ts
Original file line number Diff line number Diff line change
@@ -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');
Expand Down Expand Up @@ -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 () => {
Expand All @@ -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 () => {
Expand Down Expand Up @@ -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 () => {
Expand All @@ -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 () => {
Expand All @@ -138,23 +126,23 @@ 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 () => {
const scope =
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 () => {
const scope =
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 () => {
Expand Down