mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[Usage collection] refactor cloud detector collector (#110439)
This commit is contained in:
parent
7da17a5e67
commit
13560c01fc
11 changed files with 652 additions and 567 deletions
|
@ -83,7 +83,6 @@
|
|||
"**/pdfkit/crypto-js": "4.0.0",
|
||||
"**/react-syntax-highlighter": "^15.3.1",
|
||||
"**/react-syntax-highlighter/**/highlight.js": "^10.4.1",
|
||||
"**/request": "^2.88.2",
|
||||
"**/trim": "1.0.1",
|
||||
"**/typescript": "4.1.3",
|
||||
"**/underscore": "^1.13.1"
|
||||
|
@ -368,7 +367,6 @@
|
|||
"regenerator-runtime": "^0.13.3",
|
||||
"remark-parse": "^8.0.3",
|
||||
"remark-stringify": "^9.0.0",
|
||||
"request": "^2.88.0",
|
||||
"require-in-the-middle": "^5.0.2",
|
||||
"reselect": "^4.0.0",
|
||||
"resize-observer-polyfill": "^1.5.0",
|
||||
|
@ -606,7 +604,6 @@
|
|||
"@types/recompose": "^0.30.6",
|
||||
"@types/reduce-reducers": "^1.0.0",
|
||||
"@types/redux-actions": "^2.6.1",
|
||||
"@types/request": "^2.48.2",
|
||||
"@types/seedrandom": ">=2.0.0 <4.0.0",
|
||||
"@types/selenium-webdriver": "^4.0.9",
|
||||
"@types/semver": "^7",
|
||||
|
|
|
@ -6,120 +6,120 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import fs from 'fs';
|
||||
import type { Request, RequestOptions } from './cloud_service';
|
||||
/* eslint-disable dot-notation */
|
||||
jest.mock('node-fetch');
|
||||
jest.mock('fs/promises');
|
||||
import { AWSCloudService, AWSResponse } from './aws';
|
||||
|
||||
type Callback = (err: unknown, res: unknown) => void;
|
||||
|
||||
const AWS = new AWSCloudService();
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const fetchMock = require('node-fetch') as jest.Mock;
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const { readFile } = require('fs/promises') as { readFile: jest.Mock };
|
||||
|
||||
describe('AWS', () => {
|
||||
const expectedFilenames = ['/sys/hypervisor/uuid', '/sys/devices/virtual/dmi/id/product_uuid'];
|
||||
const expectedEncoding = 'utf8';
|
||||
// mixed case to ensure we check for ec2 after lowercasing
|
||||
const ec2Uuid = 'eC2abcdef-ghijk\n';
|
||||
const ec2FileSystem = {
|
||||
readFile: (filename: string, encoding: string, callback: Callback) => {
|
||||
expect(expectedFilenames).toContain(filename);
|
||||
expect(encoding).toEqual(expectedEncoding);
|
||||
|
||||
callback(null, ec2Uuid);
|
||||
},
|
||||
} as typeof fs;
|
||||
const mockIsWindows = jest.fn();
|
||||
const awsService = new AWSCloudService();
|
||||
awsService['_isWindows'] = mockIsWindows.mockReturnValue(false);
|
||||
readFile.mockResolvedValue('eC2abcdef-ghijk\n');
|
||||
|
||||
beforeEach(() => jest.clearAllMocks());
|
||||
it('is named "aws"', () => {
|
||||
expect(AWS.getName()).toEqual('aws');
|
||||
expect(awsService.getName()).toEqual('aws');
|
||||
});
|
||||
|
||||
describe('_checkIfService', () => {
|
||||
it('handles expected response', async () => {
|
||||
const id = 'abcdef';
|
||||
const request = ((req: RequestOptions, callback: Callback) => {
|
||||
expect(req.method).toEqual('GET');
|
||||
expect(req.uri).toEqual(
|
||||
'http://169.254.169.254/2016-09-02/dynamic/instance-identity/document'
|
||||
);
|
||||
expect(req.json).toEqual(true);
|
||||
|
||||
const body = `{"instanceId": "${id}","availabilityZone":"us-fake-2c", "imageId" : "ami-6df1e514"}`;
|
||||
|
||||
callback(null, { statusCode: 200, body });
|
||||
}) as Request;
|
||||
// ensure it does not use the fs to trump the body
|
||||
const awsCheckedFileSystem = new AWSCloudService({
|
||||
_fs: ec2FileSystem,
|
||||
_isWindows: false,
|
||||
fetchMock.mockResolvedValue({
|
||||
json: () =>
|
||||
`{"instanceId": "${id}","availabilityZone":"us-fake-2c", "imageId" : "ami-6df1e514"}`,
|
||||
status: 200,
|
||||
ok: true,
|
||||
});
|
||||
|
||||
const response = await awsCheckedFileSystem._checkIfService(request);
|
||||
const response = await awsService['_checkIfService']();
|
||||
expect(readFile).toBeCalledTimes(0);
|
||||
expect(fetchMock).toBeCalledTimes(1);
|
||||
expect(fetchMock).toBeCalledWith(
|
||||
'http://169.254.169.254/2016-09-02/dynamic/instance-identity/document',
|
||||
{
|
||||
method: 'GET',
|
||||
}
|
||||
);
|
||||
|
||||
expect(response.isConfirmed()).toEqual(true);
|
||||
expect(response.toJSON()).toEqual({
|
||||
name: AWS.getName(),
|
||||
id,
|
||||
region: undefined,
|
||||
vm_type: undefined,
|
||||
zone: 'us-fake-2c',
|
||||
metadata: {
|
||||
imageId: 'ami-6df1e514',
|
||||
},
|
||||
});
|
||||
expect(response.toJSON()).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"id": "abcdef",
|
||||
"metadata": Object {
|
||||
"imageId": "ami-6df1e514",
|
||||
},
|
||||
"name": "aws",
|
||||
"region": undefined,
|
||||
"vm_type": undefined,
|
||||
"zone": "us-fake-2c",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('handles request without a usable body by downgrading to UUID detection', async () => {
|
||||
const request = ((_req: RequestOptions, callback: Callback) =>
|
||||
callback(null, { statusCode: 404 })) as Request;
|
||||
const awsCheckedFileSystem = new AWSCloudService({
|
||||
_fs: ec2FileSystem,
|
||||
_isWindows: false,
|
||||
fetchMock.mockResolvedValue({
|
||||
json: () => null,
|
||||
status: 200,
|
||||
ok: true,
|
||||
});
|
||||
|
||||
const response = await awsCheckedFileSystem._checkIfService(request);
|
||||
const response = await awsService['_checkIfService']();
|
||||
|
||||
expect(response.isConfirmed()).toBe(true);
|
||||
expect(response.toJSON()).toEqual({
|
||||
name: AWS.getName(),
|
||||
id: ec2Uuid.trim().toLowerCase(),
|
||||
region: undefined,
|
||||
vm_type: undefined,
|
||||
zone: undefined,
|
||||
metadata: undefined,
|
||||
});
|
||||
expect(response.toJSON()).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"id": "ec2abcdef-ghijk",
|
||||
"metadata": undefined,
|
||||
"name": "aws",
|
||||
"region": undefined,
|
||||
"vm_type": undefined,
|
||||
"zone": undefined,
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('handles request failure by downgrading to UUID detection', async () => {
|
||||
const failedRequest = ((_req: RequestOptions, callback: Callback) =>
|
||||
callback(new Error('expected: request failed'), null)) as Request;
|
||||
const awsCheckedFileSystem = new AWSCloudService({
|
||||
_fs: ec2FileSystem,
|
||||
_isWindows: false,
|
||||
fetchMock.mockResolvedValue({
|
||||
status: 404,
|
||||
ok: false,
|
||||
});
|
||||
|
||||
const response = await awsCheckedFileSystem._checkIfService(failedRequest);
|
||||
const response = await awsService['_checkIfService']();
|
||||
|
||||
expect(response.isConfirmed()).toBe(true);
|
||||
expect(response.toJSON()).toEqual({
|
||||
name: AWS.getName(),
|
||||
id: ec2Uuid.trim().toLowerCase(),
|
||||
region: undefined,
|
||||
vm_type: undefined,
|
||||
zone: undefined,
|
||||
metadata: undefined,
|
||||
});
|
||||
expect(response.toJSON()).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"id": "ec2abcdef-ghijk",
|
||||
"metadata": undefined,
|
||||
"name": "aws",
|
||||
"region": undefined,
|
||||
"vm_type": undefined,
|
||||
"zone": undefined,
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('handles not running on AWS', async () => {
|
||||
const failedRequest = ((_req: RequestOptions, callback: Callback) =>
|
||||
callback(null, null)) as Request;
|
||||
const awsIgnoredFileSystem = new AWSCloudService({
|
||||
_fs: ec2FileSystem,
|
||||
_isWindows: true,
|
||||
fetchMock.mockResolvedValue({
|
||||
json: () => null,
|
||||
status: 404,
|
||||
ok: false,
|
||||
});
|
||||
|
||||
const response = await awsIgnoredFileSystem._checkIfService(failedRequest);
|
||||
mockIsWindows.mockReturnValue(true);
|
||||
|
||||
expect(response.getName()).toEqual(AWS.getName());
|
||||
const response = await awsService['_checkIfService']();
|
||||
expect(mockIsWindows).toBeCalledTimes(1);
|
||||
expect(readFile).toBeCalledTimes(0);
|
||||
|
||||
expect(response.getName()).toEqual('aws');
|
||||
expect(response.isConfirmed()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
@ -144,10 +144,10 @@ describe('AWS', () => {
|
|||
marketplaceProductCodes: null,
|
||||
};
|
||||
|
||||
const response = AWSCloudService.parseBody(AWS.getName(), body)!;
|
||||
const response = awsService.parseBody(body)!;
|
||||
expect(response).not.toBeNull();
|
||||
|
||||
expect(response.getName()).toEqual(AWS.getName());
|
||||
expect(response.getName()).toEqual('aws');
|
||||
expect(response.isConfirmed()).toEqual(true);
|
||||
expect(response.toJSON()).toEqual({
|
||||
name: 'aws',
|
||||
|
@ -169,141 +169,84 @@ describe('AWS', () => {
|
|||
|
||||
it('ignores unexpected response body', () => {
|
||||
// @ts-expect-error
|
||||
expect(AWSCloudService.parseBody(AWS.getName(), undefined)).toBe(null);
|
||||
expect(awsService.parseBody(undefined)).toBe(null);
|
||||
// @ts-expect-error
|
||||
expect(AWSCloudService.parseBody(AWS.getName(), null)).toBe(null);
|
||||
expect(awsService.parseBody(null)).toBe(null);
|
||||
// @ts-expect-error
|
||||
expect(AWSCloudService.parseBody(AWS.getName(), {})).toBe(null);
|
||||
expect(awsService.parseBody({})).toBe(null);
|
||||
// @ts-expect-error
|
||||
expect(AWSCloudService.parseBody(AWS.getName(), { privateIp: 'a.b.c.d' })).toBe(null);
|
||||
expect(awsService.parseBody({ privateIp: 'a.b.c.d' })).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('_tryToDetectUuid', () => {
|
||||
describe('tryToDetectUuid', () => {
|
||||
describe('checks the file system for UUID if not Windows', () => {
|
||||
it('checks /sys/hypervisor/uuid', async () => {
|
||||
const awsCheckedFileSystem = new AWSCloudService({
|
||||
_fs: {
|
||||
readFile: (filename: string, encoding: string, callback: Callback) => {
|
||||
expect(expectedFilenames).toContain(filename);
|
||||
expect(encoding).toEqual(expectedEncoding);
|
||||
beforeAll(() => mockIsWindows.mockReturnValue(false));
|
||||
|
||||
callback(null, ec2Uuid);
|
||||
},
|
||||
} as typeof fs,
|
||||
_isWindows: false,
|
||||
it('checks /sys/hypervisor/uuid and /sys/devices/virtual/dmi/id/product_uuid', async () => {
|
||||
const response = await awsService['tryToDetectUuid']();
|
||||
|
||||
readFile.mockImplementation(async (filename: string, encoding: string) => {
|
||||
expect(['/sys/hypervisor/uuid', '/sys/devices/virtual/dmi/id/product_uuid']).toContain(
|
||||
filename
|
||||
);
|
||||
expect(encoding).toEqual('utf8');
|
||||
|
||||
return 'eC2abcdef-ghijk\n';
|
||||
});
|
||||
|
||||
const response = await awsCheckedFileSystem._tryToDetectUuid();
|
||||
|
||||
expect(readFile).toBeCalledTimes(2);
|
||||
expect(response.isConfirmed()).toEqual(true);
|
||||
expect(response.toJSON()).toEqual({
|
||||
name: AWS.getName(),
|
||||
id: ec2Uuid.trim().toLowerCase(),
|
||||
region: undefined,
|
||||
zone: undefined,
|
||||
vm_type: undefined,
|
||||
metadata: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('checks /sys/devices/virtual/dmi/id/product_uuid', async () => {
|
||||
const awsCheckedFileSystem = new AWSCloudService({
|
||||
_fs: {
|
||||
readFile: (filename: string, encoding: string, callback: Callback) => {
|
||||
expect(expectedFilenames).toContain(filename);
|
||||
expect(encoding).toEqual(expectedEncoding);
|
||||
|
||||
callback(null, ec2Uuid);
|
||||
},
|
||||
} as typeof fs,
|
||||
_isWindows: false,
|
||||
});
|
||||
|
||||
const response = await awsCheckedFileSystem._tryToDetectUuid();
|
||||
|
||||
expect(response.isConfirmed()).toEqual(true);
|
||||
expect(response.toJSON()).toEqual({
|
||||
name: AWS.getName(),
|
||||
id: ec2Uuid.trim().toLowerCase(),
|
||||
region: undefined,
|
||||
zone: undefined,
|
||||
vm_type: undefined,
|
||||
metadata: undefined,
|
||||
});
|
||||
expect(response.toJSON()).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"id": "ec2abcdef-ghijk",
|
||||
"metadata": undefined,
|
||||
"name": "aws",
|
||||
"region": undefined,
|
||||
"vm_type": undefined,
|
||||
"zone": undefined,
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('returns confirmed if only one file exists', async () => {
|
||||
let callCount = 0;
|
||||
const awsCheckedFileSystem = new AWSCloudService({
|
||||
_fs: {
|
||||
readFile: (filename: string, encoding: string, callback: Callback) => {
|
||||
if (callCount === 0) {
|
||||
callCount++;
|
||||
throw new Error('oops');
|
||||
}
|
||||
callback(null, ec2Uuid);
|
||||
},
|
||||
} as typeof fs,
|
||||
_isWindows: false,
|
||||
});
|
||||
readFile.mockRejectedValueOnce(new Error('oops'));
|
||||
readFile.mockResolvedValueOnce('ec2Uuid');
|
||||
|
||||
const response = await awsCheckedFileSystem._tryToDetectUuid();
|
||||
const response = await awsService['tryToDetectUuid']();
|
||||
expect(readFile).toBeCalledTimes(2);
|
||||
|
||||
expect(response.isConfirmed()).toEqual(true);
|
||||
expect(response.toJSON()).toEqual({
|
||||
name: AWS.getName(),
|
||||
id: ec2Uuid.trim().toLowerCase(),
|
||||
region: undefined,
|
||||
zone: undefined,
|
||||
vm_type: undefined,
|
||||
metadata: undefined,
|
||||
});
|
||||
expect(response.toJSON()).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"id": "ec2uuid",
|
||||
"metadata": undefined,
|
||||
"name": "aws",
|
||||
"region": undefined,
|
||||
"vm_type": undefined,
|
||||
"zone": undefined,
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('returns unconfirmed if all files return errors', async () => {
|
||||
const awsFailedFileSystem = new AWSCloudService({
|
||||
_fs: ({
|
||||
readFile: () => {
|
||||
throw new Error('oops');
|
||||
},
|
||||
} as unknown) as typeof fs,
|
||||
_isWindows: false,
|
||||
});
|
||||
readFile.mockRejectedValue(new Error('oops'));
|
||||
|
||||
const response = await awsFailedFileSystem._tryToDetectUuid();
|
||||
const response = await awsService['tryToDetectUuid']();
|
||||
expect(response.isConfirmed()).toEqual(false);
|
||||
});
|
||||
|
||||
it('ignores UUID if it does not start with ec2', async () => {
|
||||
readFile.mockResolvedValue('notEC2');
|
||||
|
||||
const response = await awsService['tryToDetectUuid']();
|
||||
expect(response.isConfirmed()).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
it('ignores UUID if it does not start with ec2', async () => {
|
||||
const notEC2FileSystem = {
|
||||
readFile: (filename: string, encoding: string, callback: Callback) => {
|
||||
expect(expectedFilenames).toContain(filename);
|
||||
expect(encoding).toEqual(expectedEncoding);
|
||||
|
||||
callback(null, 'notEC2');
|
||||
},
|
||||
} as typeof fs;
|
||||
|
||||
const awsCheckedFileSystem = new AWSCloudService({
|
||||
_fs: notEC2FileSystem,
|
||||
_isWindows: false,
|
||||
});
|
||||
|
||||
const response = await awsCheckedFileSystem._tryToDetectUuid();
|
||||
|
||||
expect(response.isConfirmed()).toEqual(false);
|
||||
});
|
||||
|
||||
it('does NOT check the file system for UUID on Windows', async () => {
|
||||
const awsUncheckedFileSystem = new AWSCloudService({
|
||||
_fs: ec2FileSystem,
|
||||
_isWindows: true,
|
||||
});
|
||||
|
||||
const response = await awsUncheckedFileSystem._tryToDetectUuid();
|
||||
mockIsWindows.mockReturnValue(true);
|
||||
const response = await awsService['tryToDetectUuid']();
|
||||
|
||||
expect(response.isConfirmed()).toEqual(false);
|
||||
});
|
||||
|
|
|
@ -6,10 +6,10 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import fs from 'fs';
|
||||
import { get, isString, omit } from 'lodash';
|
||||
import { promisify } from 'util';
|
||||
import { CloudService, CloudServiceOptions, Request, RequestOptions } from './cloud_service';
|
||||
import { readFile } from 'fs/promises';
|
||||
import { get, omit } from 'lodash';
|
||||
import fetch from 'node-fetch';
|
||||
import { CloudService } from './cloud_service';
|
||||
import { CloudServiceResponse } from './cloud_response';
|
||||
|
||||
// We explicitly call out the version, 2016-09-02, rather than 'latest' to avoid unexpected changes
|
||||
|
@ -40,9 +40,9 @@ export interface AWSResponse {
|
|||
* @internal
|
||||
*/
|
||||
export class AWSCloudService extends CloudService {
|
||||
private readonly _isWindows: boolean;
|
||||
private readonly _fs: typeof fs;
|
||||
|
||||
constructor() {
|
||||
super('aws');
|
||||
}
|
||||
/**
|
||||
* Parse the AWS response, if possible.
|
||||
*
|
||||
|
@ -64,7 +64,8 @@ export class AWSCloudService extends CloudService {
|
|||
* "version" : "2010-08-31",
|
||||
* }
|
||||
*/
|
||||
static parseBody(name: string, body: AWSResponse): CloudServiceResponse | null {
|
||||
parseBody = (body: AWSResponse): CloudServiceResponse | null => {
|
||||
const name = this.getName();
|
||||
const id: string | undefined = get(body, 'instanceId');
|
||||
const vmType: string | undefined = get(body, 'instanceType');
|
||||
const region: string | undefined = get(body, 'region');
|
||||
|
@ -88,64 +89,60 @@ export class AWSCloudService extends CloudService {
|
|||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
constructor(options: CloudServiceOptions = {}) {
|
||||
super('aws', options);
|
||||
private _isWindows = (): boolean => {
|
||||
return process.platform.startsWith('win');
|
||||
};
|
||||
|
||||
// Allow the file system handler to be swapped out for tests
|
||||
const { _fs = fs, _isWindows = process.platform.startsWith('win') } = options;
|
||||
protected _checkIfService = async () => {
|
||||
try {
|
||||
const response = await fetch(SERVICE_ENDPOINT, {
|
||||
method: 'GET',
|
||||
});
|
||||
|
||||
this._fs = _fs;
|
||||
this._isWindows = _isWindows;
|
||||
}
|
||||
if (!response.ok || response.status === 404) {
|
||||
throw new Error('AWS request failed');
|
||||
}
|
||||
|
||||
async _checkIfService(request: Request) {
|
||||
const req: RequestOptions = {
|
||||
method: 'GET',
|
||||
uri: SERVICE_ENDPOINT,
|
||||
json: true,
|
||||
};
|
||||
|
||||
return promisify(request)(req)
|
||||
.then((response) =>
|
||||
this._parseResponse(response.body, (body) =>
|
||||
AWSCloudService.parseBody(this.getName(), body)
|
||||
)
|
||||
)
|
||||
.catch(() => this._tryToDetectUuid());
|
||||
}
|
||||
const jsonBody: AWSResponse = await response.json();
|
||||
return this._parseResponse(jsonBody, this.parseBody);
|
||||
} catch (_) {
|
||||
return this.tryToDetectUuid();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Attempt to load the UUID by checking `/sys/hypervisor/uuid`.
|
||||
*
|
||||
* This is a fallback option if the metadata service is unavailable for some reason.
|
||||
*/
|
||||
_tryToDetectUuid() {
|
||||
private tryToDetectUuid = async () => {
|
||||
const isWindows = this._isWindows();
|
||||
// Windows does not have an easy way to check
|
||||
if (!this._isWindows) {
|
||||
if (!isWindows) {
|
||||
const pathsToCheck = ['/sys/hypervisor/uuid', '/sys/devices/virtual/dmi/id/product_uuid'];
|
||||
const promises = pathsToCheck.map((path) => promisify(this._fs.readFile)(path, 'utf8'));
|
||||
const responses = await Promise.allSettled(
|
||||
pathsToCheck.map((path) => readFile(path, 'utf8'))
|
||||
);
|
||||
|
||||
return Promise.allSettled(promises).then((responses) => {
|
||||
for (const response of responses) {
|
||||
let uuid;
|
||||
if (response.status === 'fulfilled' && isString(response.value)) {
|
||||
// Some AWS APIs return it lowercase (like the file did in testing), while others return it uppercase
|
||||
uuid = response.value.trim().toLowerCase();
|
||||
for (const response of responses) {
|
||||
let uuid;
|
||||
if (response.status === 'fulfilled' && typeof response.value === 'string') {
|
||||
// Some AWS APIs return it lowercase (like the file did in testing), while others return it uppercase
|
||||
uuid = response.value.trim().toLowerCase();
|
||||
|
||||
// There is a small chance of a false positive here in the unlikely event that a uuid which doesn't
|
||||
// belong to ec2 happens to be generated with `ec2` as the first three characters.
|
||||
if (uuid.startsWith('ec2')) {
|
||||
return new CloudServiceResponse(this._name, true, { id: uuid });
|
||||
}
|
||||
// There is a small chance of a false positive here in the unlikely event that a uuid which doesn't
|
||||
// belong to ec2 happens to be generated with `ec2` as the first three characters.
|
||||
if (uuid.startsWith('ec2')) {
|
||||
return new CloudServiceResponse(this._name, true, { id: uuid });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return this._createUnconfirmedResponse();
|
||||
});
|
||||
return this._createUnconfirmedResponse();
|
||||
}
|
||||
|
||||
return Promise.resolve(this._createUnconfirmedResponse());
|
||||
}
|
||||
return this._createUnconfirmedResponse();
|
||||
};
|
||||
}
|
||||
|
|
|
@ -6,36 +6,47 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import type { Request, RequestOptions } from './cloud_service';
|
||||
/* eslint-disable dot-notation */
|
||||
jest.mock('node-fetch');
|
||||
import { AzureCloudService } from './azure';
|
||||
|
||||
type Callback = (err: unknown, res: unknown) => void;
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const fetchMock = require('node-fetch') as jest.Mock;
|
||||
|
||||
const AZURE = new AzureCloudService();
|
||||
|
||||
describe('Azure', () => {
|
||||
describe('AzureCloudService', () => {
|
||||
const azureCloudService = new AzureCloudService();
|
||||
it('is named "azure"', () => {
|
||||
expect(AZURE.getName()).toEqual('azure');
|
||||
expect(azureCloudService.getName()).toEqual('azure');
|
||||
});
|
||||
|
||||
describe('_checkIfService', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it('handles expected response', async () => {
|
||||
const id = 'abcdef';
|
||||
const request = ((req: RequestOptions, callback: Callback) => {
|
||||
expect(req.method).toEqual('GET');
|
||||
expect(req.uri).toEqual('http://169.254.169.254/metadata/instance?api-version=2017-04-02');
|
||||
expect(req.headers?.Metadata).toEqual('true');
|
||||
expect(req.json).toEqual(true);
|
||||
fetchMock.mockResolvedValue({
|
||||
json: () =>
|
||||
`{"compute":{"vmId": "${id}","location":"fakeus","availabilityZone":"fakeus-2"}}`,
|
||||
status: 200,
|
||||
ok: true,
|
||||
});
|
||||
|
||||
const body = `{"compute":{"vmId": "${id}","location":"fakeus","availabilityZone":"fakeus-2"}}`;
|
||||
const response = await azureCloudService['_checkIfService']();
|
||||
|
||||
callback(null, { statusCode: 200, body });
|
||||
}) as Request;
|
||||
const response = await AZURE._checkIfService(request);
|
||||
expect(fetchMock).toBeCalledTimes(1);
|
||||
expect(fetchMock).toBeCalledWith(
|
||||
'http://169.254.169.254/metadata/instance?api-version=2017-04-02',
|
||||
{
|
||||
method: 'GET',
|
||||
headers: { Metadata: 'true' },
|
||||
}
|
||||
);
|
||||
|
||||
expect(response.isConfirmed()).toEqual(true);
|
||||
expect(response.toJSON()).toEqual({
|
||||
name: AZURE.getName(),
|
||||
name: azureCloudService.getName(),
|
||||
id,
|
||||
region: 'fakeus',
|
||||
vm_type: undefined,
|
||||
|
@ -49,34 +60,30 @@ describe('Azure', () => {
|
|||
// NOTE: the CloudService method, checkIfService, catches the errors that follow
|
||||
it('handles not running on Azure with error by rethrowing it', async () => {
|
||||
const someError = new Error('expected: request failed');
|
||||
const failedRequest = ((_req: RequestOptions, callback: Callback) =>
|
||||
callback(someError, null)) as Request;
|
||||
fetchMock.mockRejectedValue(someError);
|
||||
|
||||
expect(async () => {
|
||||
await AZURE._checkIfService(failedRequest);
|
||||
}).rejects.toThrowError(someError.message);
|
||||
await expect(() => azureCloudService['_checkIfService']()).rejects.toThrowError(
|
||||
someError.message
|
||||
);
|
||||
});
|
||||
|
||||
it('handles not running on Azure with 404 response by throwing error', async () => {
|
||||
const failedRequest = ((_req: RequestOptions, callback: Callback) =>
|
||||
callback(null, { statusCode: 404 })) as Request;
|
||||
fetchMock.mockResolvedValue({ status: 404 });
|
||||
|
||||
expect(async () => {
|
||||
await AZURE._checkIfService(failedRequest);
|
||||
}).rejects.toThrowErrorMatchingInlineSnapshot(`"Azure request failed"`);
|
||||
await expect(() =>
|
||||
azureCloudService['_checkIfService']()
|
||||
).rejects.toThrowErrorMatchingInlineSnapshot(`"Azure request failed"`);
|
||||
});
|
||||
|
||||
it('handles not running on Azure with unexpected response by throwing error', async () => {
|
||||
const failedRequest = ((_req: RequestOptions, callback: Callback) =>
|
||||
callback(null, null)) as Request;
|
||||
|
||||
expect(async () => {
|
||||
await AZURE._checkIfService(failedRequest);
|
||||
}).rejects.toThrowErrorMatchingInlineSnapshot(`"Azure request failed"`);
|
||||
fetchMock.mockResolvedValue({ ok: false });
|
||||
await expect(() =>
|
||||
azureCloudService['_checkIfService']()
|
||||
).rejects.toThrowErrorMatchingInlineSnapshot(`"Azure request failed"`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('_parseBody', () => {
|
||||
describe('parseBody', () => {
|
||||
// it's expected that most users use the resource manager UI (which has been out for years)
|
||||
it('parses object in expected format', () => {
|
||||
const body = {
|
||||
|
@ -119,10 +126,10 @@ describe('Azure', () => {
|
|||
},
|
||||
};
|
||||
|
||||
const response = AzureCloudService.parseBody(AZURE.getName(), body)!;
|
||||
const response = azureCloudService['parseBody'](body)!;
|
||||
expect(response).not.toBeNull();
|
||||
|
||||
expect(response.getName()).toEqual(AZURE.getName());
|
||||
expect(response.getName()).toEqual(azureCloudService.getName());
|
||||
expect(response.isConfirmed()).toEqual(true);
|
||||
expect(response.toJSON()).toEqual({
|
||||
name: 'azure',
|
||||
|
@ -172,10 +179,10 @@ describe('Azure', () => {
|
|||
},
|
||||
};
|
||||
|
||||
const response = AzureCloudService.parseBody(AZURE.getName(), body)!;
|
||||
const response = azureCloudService['parseBody'](body)!;
|
||||
expect(response).not.toBeNull();
|
||||
|
||||
expect(response.getName()).toEqual(AZURE.getName());
|
||||
expect(response.getName()).toEqual(azureCloudService.getName());
|
||||
expect(response.isConfirmed()).toEqual(true);
|
||||
expect(response.toJSON()).toEqual({
|
||||
name: 'azure',
|
||||
|
@ -191,13 +198,13 @@ describe('Azure', () => {
|
|||
|
||||
it('ignores unexpected response body', () => {
|
||||
// @ts-expect-error
|
||||
expect(AzureCloudService.parseBody(AZURE.getName(), undefined)).toBe(null);
|
||||
expect(azureCloudService['parseBody'](undefined)).toBe(null);
|
||||
// @ts-expect-error
|
||||
expect(AzureCloudService.parseBody(AZURE.getName(), null)).toBe(null);
|
||||
expect(azureCloudService['parseBody'](null)).toBe(null);
|
||||
// @ts-expect-error
|
||||
expect(AzureCloudService.parseBody(AZURE.getName(), {})).toBe(null);
|
||||
expect(azureCloudService['parseBody']({})).toBe(null);
|
||||
// @ts-expect-error
|
||||
expect(AzureCloudService.parseBody(AZURE.getName(), { privateIp: 'a.b.c.d' })).toBe(null);
|
||||
expect(azureCloudService['parseBody']({ privateIp: 'a.b.c.d' })).toBe(null);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -7,8 +7,8 @@
|
|||
*/
|
||||
|
||||
import { get, omit } from 'lodash';
|
||||
import { promisify } from 'util';
|
||||
import { CloudService, Request } from './cloud_service';
|
||||
import fetch from 'node-fetch';
|
||||
import { CloudService } from './cloud_service';
|
||||
import { CloudServiceResponse } from './cloud_response';
|
||||
|
||||
// 2017-04-02 is the first GA release of this API
|
||||
|
@ -25,6 +25,9 @@ interface AzureResponse {
|
|||
* @internal
|
||||
*/
|
||||
export class AzureCloudService extends CloudService {
|
||||
constructor() {
|
||||
super('azure');
|
||||
}
|
||||
/**
|
||||
* Parse the Azure response, if possible.
|
||||
*
|
||||
|
@ -51,7 +54,8 @@ export class AzureCloudService extends CloudService {
|
|||
* }
|
||||
* }
|
||||
*/
|
||||
static parseBody(name: string, body: AzureResponse): CloudServiceResponse | null {
|
||||
private parseBody = (body: AzureResponse): CloudServiceResponse | null => {
|
||||
const name = this.getName();
|
||||
const compute: Record<string, string> | undefined = get(body, 'compute');
|
||||
const id = get<Record<string, string>, string>(compute, 'vmId');
|
||||
const vmType = get<Record<string, string>, string>(compute, 'vmSize');
|
||||
|
@ -72,32 +76,22 @@ export class AzureCloudService extends CloudService {
|
|||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
constructor(options = {}) {
|
||||
super('azure', options);
|
||||
}
|
||||
|
||||
async _checkIfService(request: Request) {
|
||||
const req = {
|
||||
protected _checkIfService = async () => {
|
||||
const response = await fetch(SERVICE_ENDPOINT, {
|
||||
method: 'GET',
|
||||
uri: SERVICE_ENDPOINT,
|
||||
headers: {
|
||||
// Azure requires this header
|
||||
Metadata: 'true',
|
||||
},
|
||||
json: true,
|
||||
};
|
||||
});
|
||||
|
||||
const response = await promisify(request)(req);
|
||||
|
||||
// Note: there is no fallback option for Azure
|
||||
if (!response || response.statusCode === 404) {
|
||||
if (!response.ok || response.status === 404) {
|
||||
throw new Error('Azure request failed');
|
||||
}
|
||||
|
||||
return this._parseResponse(response.body, (body) =>
|
||||
AzureCloudService.parseBody(this.getName(), body)
|
||||
);
|
||||
}
|
||||
const jsonBody: AzureResponse = await response.json();
|
||||
return this._parseResponse(jsonBody, this.parseBody);
|
||||
};
|
||||
}
|
||||
|
|
|
@ -37,9 +37,9 @@ export class CloudDetector {
|
|||
/**
|
||||
* Get any cloud details that we have detected.
|
||||
*/
|
||||
getCloudDetails() {
|
||||
public getCloudDetails = () => {
|
||||
return this.cloudDetails;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Asynchronously detect the cloud service.
|
||||
|
@ -48,9 +48,9 @@ export class CloudDetector {
|
|||
* caller to trigger the lookup and then simply use it whenever we
|
||||
* determine it.
|
||||
*/
|
||||
async detectCloudService() {
|
||||
public detectCloudService = async () => {
|
||||
this.cloudDetails = await this.getCloudService();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Check every cloud service until the first one reports success from detection.
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { CloudService, Response } from './cloud_service';
|
||||
import { CloudService } from './cloud_service';
|
||||
import { CloudServiceResponse } from './cloud_response';
|
||||
|
||||
describe('CloudService', () => {
|
||||
|
@ -30,9 +30,9 @@ describe('CloudService', () => {
|
|||
|
||||
describe('_checkIfService', () => {
|
||||
it('throws an exception unless overridden', async () => {
|
||||
expect(async () => {
|
||||
await service._checkIfService(undefined);
|
||||
}).rejects.toThrowErrorMatchingInlineSnapshot(`"not implemented"`);
|
||||
await expect(() =>
|
||||
service._checkIfService(undefined)
|
||||
).rejects.toThrowErrorMatchingInlineSnapshot(`"not implemented"`);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -88,52 +88,59 @@ describe('CloudService', () => {
|
|||
describe('_parseResponse', () => {
|
||||
const body = { some: { body: {} } };
|
||||
|
||||
it('throws error upon failure to parse body as object', async () => {
|
||||
expect(async () => {
|
||||
await service._parseResponse();
|
||||
}).rejects.toMatchInlineSnapshot(`undefined`);
|
||||
expect(async () => {
|
||||
await service._parseResponse(null);
|
||||
}).rejects.toMatchInlineSnapshot(`undefined`);
|
||||
expect(async () => {
|
||||
await service._parseResponse({});
|
||||
}).rejects.toMatchInlineSnapshot(`undefined`);
|
||||
expect(async () => {
|
||||
await service._parseResponse(123);
|
||||
}).rejects.toMatchInlineSnapshot(`undefined`);
|
||||
expect(async () => {
|
||||
await service._parseResponse('raw string');
|
||||
}).rejects.toMatchInlineSnapshot(`[Error: 'raw string' is not a JSON object]`);
|
||||
expect(async () => {
|
||||
await service._parseResponse('{{}');
|
||||
}).rejects.toMatchInlineSnapshot(`[Error: '{{}' is not a JSON object]`);
|
||||
it('throws error upon failure to parse body as object', () => {
|
||||
expect(() => service._parseResponse()).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Unable to handle body"`
|
||||
);
|
||||
expect(() => service._parseResponse(null)).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Unable to handle body"`
|
||||
);
|
||||
expect(() => service._parseResponse({})).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Unable to handle body"`
|
||||
);
|
||||
expect(() => service._parseResponse(123)).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Unable to handle body"`
|
||||
);
|
||||
expect(() => service._parseResponse('raw string')).toThrowErrorMatchingInlineSnapshot(
|
||||
`"'raw string' is not a JSON object"`
|
||||
);
|
||||
expect(() => service._parseResponse('{{}')).toThrowErrorMatchingInlineSnapshot(
|
||||
`"'{{}' is not a JSON object"`
|
||||
);
|
||||
});
|
||||
|
||||
it('expects unusable bodies', async () => {
|
||||
const parseBody = (parsedBody: Response['body']) => {
|
||||
expect(parsedBody).toEqual(body);
|
||||
it('expects unusable bodies', () => {
|
||||
const parseBody = jest.fn().mockReturnValue(null);
|
||||
|
||||
return null;
|
||||
};
|
||||
expect(() =>
|
||||
service._parseResponse(JSON.stringify(body), parseBody)
|
||||
).toThrowErrorMatchingInlineSnapshot(`"Unable to handle body"`);
|
||||
expect(parseBody).toBeCalledTimes(1);
|
||||
expect(parseBody).toBeCalledWith(body);
|
||||
parseBody.mockClear();
|
||||
|
||||
expect(async () => {
|
||||
await service._parseResponse(JSON.stringify(body), parseBody);
|
||||
}).rejects.toMatchInlineSnapshot(`undefined`);
|
||||
expect(async () => {
|
||||
await service._parseResponse(body, parseBody);
|
||||
}).rejects.toMatchInlineSnapshot(`undefined`);
|
||||
expect(() => service._parseResponse(body, parseBody)).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Unable to handle body"`
|
||||
);
|
||||
expect(parseBody).toBeCalledTimes(1);
|
||||
expect(parseBody).toBeCalledWith(body);
|
||||
});
|
||||
|
||||
it('uses parsed object to create response', async () => {
|
||||
const serviceResponse = new CloudServiceResponse('a123', true, { id: 'xyz' });
|
||||
const parseBody = (parsedBody: Response['body']) => {
|
||||
expect(parsedBody).toEqual(body);
|
||||
|
||||
return serviceResponse;
|
||||
};
|
||||
const parseBody = jest.fn().mockReturnValue(serviceResponse);
|
||||
|
||||
const response = await service._parseResponse(body, parseBody);
|
||||
expect(parseBody).toBeCalledWith(body);
|
||||
expect(response).toBe(serviceResponse);
|
||||
});
|
||||
|
||||
it('parses object before passing it to parseBody to create response', async () => {
|
||||
const serviceResponse = new CloudServiceResponse('a123', true, { id: 'xyz' });
|
||||
const parseBody = jest.fn().mockReturnValue(serviceResponse);
|
||||
|
||||
const response = await service._parseResponse(JSON.stringify(body), parseBody);
|
||||
expect(parseBody).toBeCalledWith(body);
|
||||
expect(response).toBe(serviceResponse);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,81 +6,56 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import fs from 'fs';
|
||||
import { isObject, isString, isPlainObject } from 'lodash';
|
||||
import defaultRequest from 'request';
|
||||
import type { OptionsWithUri, Response as DefaultResponse } from 'request';
|
||||
import { isObject, isPlainObject } from 'lodash';
|
||||
import { CloudServiceResponse } from './cloud_response';
|
||||
|
||||
/** @internal */
|
||||
export type Request = typeof defaultRequest;
|
||||
|
||||
/** @internal */
|
||||
export type RequestOptions = OptionsWithUri;
|
||||
|
||||
/** @internal */
|
||||
export type Response = DefaultResponse;
|
||||
|
||||
/** @internal */
|
||||
export interface CloudServiceOptions {
|
||||
_request?: Request;
|
||||
_fs?: typeof fs;
|
||||
_isWindows?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* CloudService provides a mechanism for cloud services to be checked for
|
||||
* metadata that may help to determine the best defaults and priorities.
|
||||
*/
|
||||
export abstract class CloudService {
|
||||
private readonly _request: Request;
|
||||
protected readonly _name: string;
|
||||
|
||||
constructor(name: string, options: CloudServiceOptions = {}) {
|
||||
constructor(name: string) {
|
||||
this._name = name.toLowerCase();
|
||||
|
||||
// Allow the HTTP handler to be swapped out for tests
|
||||
const { _request = defaultRequest } = options;
|
||||
|
||||
this._request = _request;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the search-friendly name of the Cloud Service.
|
||||
*/
|
||||
getName() {
|
||||
public getName = () => {
|
||||
return this._name;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Using whatever mechanism is required by the current Cloud Service,
|
||||
* determine if Kibana is running in it and return relevant metadata.
|
||||
*/
|
||||
async checkIfService() {
|
||||
public checkIfService = async () => {
|
||||
try {
|
||||
return await this._checkIfService(this._request);
|
||||
return await this._checkIfService();
|
||||
} catch (e) {
|
||||
return this._createUnconfirmedResponse();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
_checkIfService(request: Request): Promise<CloudServiceResponse> {
|
||||
protected _checkIfService = async (): Promise<CloudServiceResponse> => {
|
||||
// should always be overridden by a subclass
|
||||
return Promise.reject(new Error('not implemented'));
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a new CloudServiceResponse that denotes that this cloud service
|
||||
* is not being used by the current machine / VM.
|
||||
*/
|
||||
_createUnconfirmedResponse() {
|
||||
protected _createUnconfirmedResponse = () => {
|
||||
return CloudServiceResponse.unconfirmed(this._name);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Strictly parse JSON.
|
||||
*/
|
||||
_stringToJson(value: string) {
|
||||
protected _stringToJson = (value: string) => {
|
||||
// note: this will throw an error if this is not a string
|
||||
value = value.trim();
|
||||
|
||||
|
@ -94,7 +69,7 @@ export abstract class CloudService {
|
|||
} catch (e) {
|
||||
throw new Error(`'${value}' is not a JSON object`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert the response to a JSON object and attempt to parse it using the
|
||||
|
@ -103,28 +78,21 @@ export abstract class CloudService {
|
|||
* If the response cannot be parsed as a JSON object, or if it fails to be
|
||||
* useful, then parseBody should return null.
|
||||
*/
|
||||
_parseResponse(
|
||||
body: Response['body'],
|
||||
parseBody?: (body: Response['body']) => CloudServiceResponse | null
|
||||
): Promise<CloudServiceResponse> {
|
||||
protected _parseResponse = <Body>(
|
||||
body: string | Body,
|
||||
parseBodyFn: (body: Body) => CloudServiceResponse | null
|
||||
): CloudServiceResponse => {
|
||||
// parse it if necessary
|
||||
if (isString(body)) {
|
||||
try {
|
||||
body = this._stringToJson(body);
|
||||
} catch (err) {
|
||||
return Promise.reject(err);
|
||||
}
|
||||
}
|
||||
|
||||
if (isObject(body) && parseBody) {
|
||||
const response = parseBody(body);
|
||||
const jsonBody: Body = typeof body === 'string' ? this._stringToJson(body) : body;
|
||||
|
||||
if (isObject(jsonBody) && typeof parseBodyFn !== 'undefined') {
|
||||
const response = parseBodyFn(jsonBody);
|
||||
if (response) {
|
||||
return Promise.resolve(response);
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
||||
// use default handling
|
||||
return Promise.reject();
|
||||
}
|
||||
throw new Error('Unable to handle body');
|
||||
};
|
||||
}
|
||||
|
|
|
@ -5,136 +5,185 @@
|
|||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import type { Request, RequestOptions } from './cloud_service';
|
||||
/* eslint-disable dot-notation */
|
||||
jest.mock('node-fetch');
|
||||
import { GCPCloudService } from './gcp';
|
||||
|
||||
type Callback = (err: unknown, res: unknown) => void;
|
||||
|
||||
const GCP = new GCPCloudService();
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const fetchMock = require('node-fetch') as jest.Mock;
|
||||
|
||||
describe('GCP', () => {
|
||||
const gcpService = new GCPCloudService();
|
||||
beforeEach(() => jest.clearAllMocks());
|
||||
|
||||
it('is named "gcp"', () => {
|
||||
expect(GCP.getName()).toEqual('gcp');
|
||||
expect(gcpService.getName()).toEqual('gcp');
|
||||
});
|
||||
|
||||
describe('_checkIfService', () => {
|
||||
// GCP responds with the header that they expect (and request lowercases the header's name)
|
||||
const headers = { 'metadata-flavor': 'Google' };
|
||||
const headers = new Map();
|
||||
headers.set('metadata-flavor', 'Google');
|
||||
|
||||
it('handles expected responses', async () => {
|
||||
const basePath = 'http://169.254.169.254/computeMetadata/v1/instance/';
|
||||
const metadata: Record<string, string> = {
|
||||
id: 'abcdef',
|
||||
'machine-type': 'projects/441331612345/machineTypes/f1-micro',
|
||||
zone: 'projects/441331612345/zones/us-fake4-c',
|
||||
};
|
||||
const request = ((req: RequestOptions, callback: Callback) => {
|
||||
const basePath = 'http://169.254.169.254/computeMetadata/v1/instance/';
|
||||
|
||||
expect(req.method).toEqual('GET');
|
||||
expect((req.uri as string).startsWith(basePath)).toBe(true);
|
||||
expect(req.headers!['Metadata-Flavor']).toEqual('Google');
|
||||
expect(req.json).toEqual(false);
|
||||
|
||||
const requestKey = (req.uri as string).substring(basePath.length);
|
||||
let body = null;
|
||||
fetchMock.mockImplementation((url: string) => {
|
||||
const requestKey = url.substring(basePath.length);
|
||||
let body: string | null = null;
|
||||
|
||||
if (metadata[requestKey]) {
|
||||
body = metadata[requestKey];
|
||||
}
|
||||
return {
|
||||
status: 200,
|
||||
ok: true,
|
||||
text: () => body,
|
||||
headers,
|
||||
};
|
||||
});
|
||||
|
||||
callback(null, { statusCode: 200, body, headers });
|
||||
}) as Request;
|
||||
const response = await GCP._checkIfService(request);
|
||||
const response = await gcpService['_checkIfService']();
|
||||
const fetchParams = {
|
||||
headers: { 'Metadata-Flavor': 'Google' },
|
||||
method: 'GET',
|
||||
};
|
||||
expect(fetchMock).toBeCalledTimes(3);
|
||||
expect(fetchMock).toHaveBeenNthCalledWith(1, `${basePath}id`, fetchParams);
|
||||
expect(fetchMock).toHaveBeenNthCalledWith(2, `${basePath}machine-type`, fetchParams);
|
||||
expect(fetchMock).toHaveBeenNthCalledWith(3, `${basePath}zone`, fetchParams);
|
||||
|
||||
expect(response.isConfirmed()).toEqual(true);
|
||||
expect(response.toJSON()).toEqual({
|
||||
name: GCP.getName(),
|
||||
id: metadata.id,
|
||||
region: 'us-fake4',
|
||||
vm_type: 'f1-micro',
|
||||
zone: 'us-fake4-c',
|
||||
metadata: undefined,
|
||||
});
|
||||
expect(response.toJSON()).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"id": "abcdef",
|
||||
"metadata": undefined,
|
||||
"name": "gcp",
|
||||
"region": "us-fake4",
|
||||
"vm_type": "f1-micro",
|
||||
"zone": "us-fake4-c",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
// NOTE: the CloudService method, checkIfService, catches the errors that follow
|
||||
it('handles unexpected responses', async () => {
|
||||
const request = ((_req: RequestOptions, callback: Callback) =>
|
||||
callback(null, { statusCode: 200, headers })) as Request;
|
||||
fetchMock.mockResolvedValue({
|
||||
status: 200,
|
||||
ok: true,
|
||||
headers,
|
||||
text: () => undefined,
|
||||
});
|
||||
|
||||
expect(async () => {
|
||||
await GCP._checkIfService(request);
|
||||
}).rejects.toThrowErrorMatchingInlineSnapshot(`"unrecognized responses"`);
|
||||
await expect(() =>
|
||||
gcpService['_checkIfService']()
|
||||
).rejects.toThrowErrorMatchingInlineSnapshot(`"unrecognized responses"`);
|
||||
});
|
||||
|
||||
it('handles unexpected responses without response header', async () => {
|
||||
const body = 'xyz';
|
||||
const failedRequest = ((_req: RequestOptions, callback: Callback) =>
|
||||
callback(null, { statusCode: 200, body })) as Request;
|
||||
fetchMock.mockResolvedValue({
|
||||
status: 200,
|
||||
ok: true,
|
||||
headers: new Map(),
|
||||
text: () => 'xyz',
|
||||
});
|
||||
|
||||
expect(async () => {
|
||||
await GCP._checkIfService(failedRequest);
|
||||
}).rejects.toThrowErrorMatchingInlineSnapshot(`"unrecognized responses"`);
|
||||
await expect(() =>
|
||||
gcpService['_checkIfService']()
|
||||
).rejects.toThrowErrorMatchingInlineSnapshot(`"GCP request failed"`);
|
||||
});
|
||||
|
||||
it('handles not running on GCP with error by rethrowing it', async () => {
|
||||
it('handles not running on GCP', async () => {
|
||||
const someError = new Error('expected: request failed');
|
||||
const failedRequest = ((_req: RequestOptions, callback: Callback) =>
|
||||
callback(someError, null)) as Request;
|
||||
fetchMock.mockRejectedValue(someError);
|
||||
|
||||
expect(async () => {
|
||||
await GCP._checkIfService(failedRequest);
|
||||
}).rejects.toThrowError(someError);
|
||||
await expect(() =>
|
||||
gcpService['_checkIfService']()
|
||||
).rejects.toThrowErrorMatchingInlineSnapshot(`"GCP request failed"`);
|
||||
});
|
||||
|
||||
it('handles not running on GCP with 404 response by throwing error', async () => {
|
||||
const body = 'This is some random error text';
|
||||
const failedRequest = ((_req: RequestOptions, callback: Callback) =>
|
||||
callback(null, { statusCode: 404, headers, body })) as Request;
|
||||
fetchMock.mockResolvedValue({
|
||||
status: 404,
|
||||
ok: false,
|
||||
headers,
|
||||
text: () => 'This is some random error text',
|
||||
});
|
||||
|
||||
expect(async () => {
|
||||
await GCP._checkIfService(failedRequest);
|
||||
}).rejects.toThrowErrorMatchingInlineSnapshot(`"GCP request failed"`);
|
||||
await expect(() =>
|
||||
gcpService['_checkIfService']()
|
||||
).rejects.toThrowErrorMatchingInlineSnapshot(`"GCP request failed"`);
|
||||
});
|
||||
|
||||
it('handles not running on GCP with unexpected response by throwing error', async () => {
|
||||
const failedRequest = ((_req: RequestOptions, callback: Callback) =>
|
||||
callback(null, null)) as Request;
|
||||
it('handles GCP response even if some requests fail', async () => {
|
||||
fetchMock
|
||||
.mockResolvedValueOnce({
|
||||
status: 200,
|
||||
ok: true,
|
||||
headers,
|
||||
text: () => 'some_id',
|
||||
})
|
||||
.mockRejectedValueOnce({
|
||||
status: 500,
|
||||
ok: false,
|
||||
headers,
|
||||
text: () => 'This is some random error text',
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
status: 404,
|
||||
ok: false,
|
||||
headers,
|
||||
text: () => 'URI Not found',
|
||||
});
|
||||
const response = await gcpService['_checkIfService']();
|
||||
|
||||
expect(async () => {
|
||||
await GCP._checkIfService(failedRequest);
|
||||
}).rejects.toThrowErrorMatchingInlineSnapshot(`"GCP request failed"`);
|
||||
expect(fetchMock).toBeCalledTimes(3);
|
||||
|
||||
expect(response.isConfirmed()).toEqual(true);
|
||||
expect(response.toJSON()).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"id": "some_id",
|
||||
"metadata": undefined,
|
||||
"name": "gcp",
|
||||
"region": undefined,
|
||||
"vm_type": undefined,
|
||||
"zone": undefined,
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('_extractValue', () => {
|
||||
describe('extractValue', () => {
|
||||
it('only handles strings', () => {
|
||||
// @ts-expect-error
|
||||
expect(GCP._extractValue()).toBe(undefined);
|
||||
expect(gcpService['extractValue']()).toBe(undefined);
|
||||
// @ts-expect-error
|
||||
expect(GCP._extractValue(null, null)).toBe(undefined);
|
||||
expect(gcpService['extractValue'](null, null)).toBe(undefined);
|
||||
// @ts-expect-error
|
||||
expect(GCP._extractValue('abc', { field: 'abcxyz' })).toBe(undefined);
|
||||
expect(gcpService['extractValue']('abc', { field: 'abcxyz' })).toBe(undefined);
|
||||
// @ts-expect-error
|
||||
expect(GCP._extractValue('abc', 1234)).toBe(undefined);
|
||||
expect(GCP._extractValue('abc/', 'abc/xyz')).toEqual('xyz');
|
||||
expect(gcpService['extractValue']('abc', 1234)).toBe(undefined);
|
||||
expect(gcpService['extractValue']('abc/', 'abc/xyz')).toEqual('xyz');
|
||||
});
|
||||
|
||||
it('uses the last index of the prefix to truncate', () => {
|
||||
expect(GCP._extractValue('abc/', ' \n 123/abc/xyz\t \n')).toEqual('xyz');
|
||||
expect(gcpService['extractValue']('abc/', ' \n 123/abc/xyz\t \n')).toEqual('xyz');
|
||||
});
|
||||
});
|
||||
|
||||
describe('_combineResponses', () => {
|
||||
describe('combineResponses', () => {
|
||||
it('parses in expected format', () => {
|
||||
const id = '5702733457649812345';
|
||||
const machineType = 'projects/441331612345/machineTypes/f1-micro';
|
||||
const zone = 'projects/441331612345/zones/us-fake4-c';
|
||||
|
||||
const response = GCP._combineResponses(id, machineType, zone);
|
||||
const response = gcpService['combineResponses'](id, machineType, zone);
|
||||
|
||||
expect(response.getName()).toEqual(GCP.getName());
|
||||
expect(response.getName()).toEqual('gcp');
|
||||
expect(response.isConfirmed()).toEqual(true);
|
||||
expect(response.toJSON()).toEqual({
|
||||
name: 'gcp',
|
||||
|
@ -152,9 +201,9 @@ describe('GCP', () => {
|
|||
const machineType = 'f1-micro';
|
||||
const zone = 'us-fake4-c';
|
||||
|
||||
const response = GCP._combineResponses(id, machineType, zone);
|
||||
const response = gcpService['combineResponses'](id, machineType, zone);
|
||||
|
||||
expect(response.getName()).toEqual(GCP.getName());
|
||||
expect(response.getName()).toEqual('gcp');
|
||||
expect(response.isConfirmed()).toEqual(true);
|
||||
expect(response.toJSON()).toEqual({
|
||||
name: 'gcp',
|
||||
|
@ -167,18 +216,16 @@ describe('GCP', () => {
|
|||
});
|
||||
|
||||
it('ignores unexpected response body', () => {
|
||||
expect(() => gcpService['combineResponses']()).toThrow();
|
||||
expect(() => gcpService['combineResponses'](undefined, undefined, undefined)).toThrow();
|
||||
// @ts-expect-error
|
||||
expect(() => GCP._combineResponses()).toThrow();
|
||||
// @ts-expect-error
|
||||
expect(() => GCP._combineResponses(undefined, undefined, undefined)).toThrow();
|
||||
// @ts-expect-error
|
||||
expect(() => GCP._combineResponses(null, null, null)).toThrow();
|
||||
expect(() => gcpService['combineResponses'](null, null, null)).toThrow();
|
||||
expect(() =>
|
||||
// @ts-expect-error
|
||||
GCP._combineResponses({ id: 'x' }, { machineType: 'a' }, { zone: 'b' })
|
||||
gcpService['combineResponses']({ id: 'x' }, { machineType: 'a' }, { zone: 'b' })
|
||||
).toThrow();
|
||||
// @ts-expect-error
|
||||
expect(() => GCP._combineResponses({ privateIp: 'a.b.c.d' })).toThrow();
|
||||
expect(() => gcpService['combineResponses']({ privateIp: 'a.b.c.d' })).toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,14 +6,15 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { isString } from 'lodash';
|
||||
import { promisify } from 'util';
|
||||
import { CloudService, CloudServiceOptions, Request, Response } from './cloud_service';
|
||||
import fetch, { Response } from 'node-fetch';
|
||||
import { CloudService } from './cloud_service';
|
||||
import { CloudServiceResponse } from './cloud_response';
|
||||
|
||||
// GCP documentation shows both 'metadata.google.internal' (mostly) and '169.254.169.254' (sometimes)
|
||||
// To bypass potential DNS changes, the IP was used because it's shared with other cloud services
|
||||
const SERVICE_ENDPOINT = 'http://169.254.169.254/computeMetadata/v1/instance';
|
||||
// GCP required headers
|
||||
const SERVICE_HEADERS = { 'Metadata-Flavor': 'Google' };
|
||||
|
||||
/**
|
||||
* Checks and loads the service metadata for an Google Cloud Platform VM if it is available.
|
||||
|
@ -21,61 +22,54 @@ const SERVICE_ENDPOINT = 'http://169.254.169.254/computeMetadata/v1/instance';
|
|||
* @internal
|
||||
*/
|
||||
export class GCPCloudService extends CloudService {
|
||||
constructor(options: CloudServiceOptions = {}) {
|
||||
super('gcp', options);
|
||||
constructor() {
|
||||
super('gcp');
|
||||
}
|
||||
|
||||
_checkIfService(request: Request) {
|
||||
protected _checkIfService = async () => {
|
||||
// we need to call GCP individually for each field we want metadata for
|
||||
const fields = ['id', 'machine-type', 'zone'];
|
||||
|
||||
const create = this._createRequestForField;
|
||||
const allRequests = fields.map((field) => promisify(request)(create(field)));
|
||||
return (
|
||||
Promise.all(allRequests)
|
||||
// Note: there is no fallback option for GCP;
|
||||
// responses are arrays containing [fullResponse, body];
|
||||
// because GCP returns plaintext, we have no way of validating
|
||||
// without using the response code.
|
||||
.then((responses) => {
|
||||
return responses.map((response) => {
|
||||
if (!response || response.statusCode === 404) {
|
||||
throw new Error('GCP request failed');
|
||||
}
|
||||
return this._extractBody(response, response.body);
|
||||
});
|
||||
})
|
||||
.then(([id, machineType, zone]) => this._combineResponses(id, machineType, zone))
|
||||
const settledResponses = await Promise.allSettled(
|
||||
fields.map(async (field) => {
|
||||
return await fetch(`${SERVICE_ENDPOINT}/${field}`, {
|
||||
method: 'GET',
|
||||
headers: { ...SERVICE_HEADERS },
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
_createRequestForField(field: string) {
|
||||
return {
|
||||
method: 'GET',
|
||||
uri: `${SERVICE_ENDPOINT}/${field}`,
|
||||
headers: {
|
||||
// GCP requires this header
|
||||
'Metadata-Flavor': 'Google',
|
||||
},
|
||||
// GCP does _not_ return JSON
|
||||
json: false,
|
||||
};
|
||||
}
|
||||
const hasValidResponses = settledResponses.some(this.isValidResponse);
|
||||
|
||||
/**
|
||||
* Extract the body if the response is valid and it came from GCP.
|
||||
*/
|
||||
_extractBody(response: Response, body?: Response['body']) {
|
||||
if (
|
||||
response?.statusCode === 200 &&
|
||||
response.headers &&
|
||||
response.headers['metadata-flavor'] === 'Google'
|
||||
) {
|
||||
return body;
|
||||
if (!hasValidResponses) {
|
||||
throw new Error('GCP request failed');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
// Note: there is no fallback option for GCP;
|
||||
// responses are arrays containing [fullResponse, body];
|
||||
// because GCP returns plaintext, we have no way of validating
|
||||
// without using the response code.
|
||||
const [id, machineType, zone] = await Promise.all(
|
||||
settledResponses.map(async (settledResponse) => {
|
||||
if (this.isValidResponse(settledResponse)) {
|
||||
// GCP does _not_ return JSON
|
||||
return await settledResponse.value.text();
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
return this.combineResponses(id, machineType, zone);
|
||||
};
|
||||
|
||||
private isValidResponse = (
|
||||
settledResponse: PromiseSettledResult<Response>
|
||||
): settledResponse is PromiseFulfilledResult<Response> => {
|
||||
if (settledResponse.status === 'rejected') {
|
||||
return false;
|
||||
}
|
||||
const { value } = settledResponse;
|
||||
return value.ok && value.status !== 404 && value.headers.get('metadata-flavor') === 'Google';
|
||||
};
|
||||
|
||||
/**
|
||||
* Parse the GCP responses, if possible.
|
||||
|
@ -86,17 +80,11 @@ export class GCPCloudService extends CloudService {
|
|||
* machineType: 'projects/441331612345/machineTypes/f1-micro'
|
||||
* zone: 'projects/441331612345/zones/us-east4-c'
|
||||
*/
|
||||
_combineResponses(id: string, machineType: string, zone: string) {
|
||||
const vmId = isString(id) ? id.trim() : undefined;
|
||||
const vmType = this._extractValue('machineTypes/', machineType);
|
||||
const vmZone = this._extractValue('zones/', zone);
|
||||
|
||||
let region;
|
||||
|
||||
if (vmZone) {
|
||||
// converts 'us-east4-c' into 'us-east4'
|
||||
region = vmZone.substring(0, vmZone.lastIndexOf('-'));
|
||||
}
|
||||
private combineResponses = (id?: string, machineType?: string, zone?: string) => {
|
||||
const vmId = typeof id === 'string' ? id.trim() : undefined;
|
||||
const vmType = this.extractValue('machineTypes/', machineType);
|
||||
const vmZone = this.extractValue('zones/', zone);
|
||||
const region = vmZone ? vmZone.substring(0, vmZone.lastIndexOf('-')) : undefined;
|
||||
|
||||
// ensure we actually have some data
|
||||
if (vmId || vmType || region || vmZone) {
|
||||
|
@ -104,7 +92,7 @@ export class GCPCloudService extends CloudService {
|
|||
}
|
||||
|
||||
throw new Error('unrecognized responses');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Extract the useful information returned from GCP while discarding
|
||||
|
@ -113,15 +101,15 @@ export class GCPCloudService extends CloudService {
|
|||
* For example, this turns something like
|
||||
* 'projects/441331612345/machineTypes/f1-micro' into 'f1-micro'.
|
||||
*/
|
||||
_extractValue(fieldPrefix: string, value: string) {
|
||||
if (isString(value)) {
|
||||
const index = value.lastIndexOf(fieldPrefix);
|
||||
|
||||
if (index !== -1) {
|
||||
return value.substring(index + fieldPrefix.length).trim();
|
||||
}
|
||||
private extractValue = (fieldPrefix: string, value?: string) => {
|
||||
if (typeof value !== 'string') {
|
||||
return;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
const index = value.lastIndexOf(fieldPrefix);
|
||||
|
||||
if (index !== -1) {
|
||||
return value.substring(index + fieldPrefix.length).trim();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
181
yarn.lock
181
yarn.lock
|
@ -4913,11 +4913,6 @@
|
|||
"@types/node" "*"
|
||||
"@types/responselike" "*"
|
||||
|
||||
"@types/caseless@*":
|
||||
version "0.12.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/caseless/-/caseless-0.12.2.tgz#f65d3d6389e01eeb458bd54dc8f52b95a9463bc8"
|
||||
integrity sha512-6ckxMjBBD8URvjB6J3NcnuAn5Pkl7t3TizAg+xdlzzQGSPSmBcXf8KoIH0ua/i+tio+ZRUHEXp0HEmvaR4kt0w==
|
||||
|
||||
"@types/chance@^1.0.0":
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/chance/-/chance-1.0.1.tgz#c10703020369602c40dd9428cc6e1437027116df"
|
||||
|
@ -6024,16 +6019,6 @@
|
|||
dependencies:
|
||||
"@types/prismjs" "*"
|
||||
|
||||
"@types/request@^2.48.2":
|
||||
version "2.48.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/request/-/request-2.48.2.tgz#936374cbe1179d7ed529fc02543deb4597450fed"
|
||||
integrity sha512-gP+PSFXAXMrd5PcD7SqHeUjdGshAI8vKQ3+AvpQr3ht9iQea+59LOKvKITcQI+Lg+1EIkDP6AFSBUJPWG8GDyA==
|
||||
dependencies:
|
||||
"@types/caseless" "*"
|
||||
"@types/node" "*"
|
||||
"@types/tough-cookie" "*"
|
||||
form-data "^2.5.0"
|
||||
|
||||
"@types/resize-observer-browser@^0.1.5":
|
||||
version "0.1.5"
|
||||
resolved "https://registry.yarnpkg.com/@types/resize-observer-browser/-/resize-observer-browser-0.1.5.tgz#36d897708172ac2380cd486da7a3daf1161c1e23"
|
||||
|
@ -6921,6 +6906,14 @@ ajv-keywords@^3.5.2:
|
|||
resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d"
|
||||
integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==
|
||||
|
||||
ajv@^4.9.1:
|
||||
version "4.11.8"
|
||||
resolved "https://registry.yarnpkg.com/ajv/-/ajv-4.11.8.tgz#82ffb02b29e662ae53bdc20af15947706739c536"
|
||||
integrity sha1-gv+wKynmYq5TvcIK8VlHcGc5xTY=
|
||||
dependencies:
|
||||
co "^4.6.0"
|
||||
json-stable-stringify "^1.0.1"
|
||||
|
||||
ajv@^6.1.0, ajv@^6.10.0, ajv@^6.10.2, ajv@^6.12.2, ajv@^6.12.4, ajv@^6.5.5, ajv@^6.9.1:
|
||||
version "6.12.4"
|
||||
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.4.tgz#0614facc4522127fa713445c6bfd3ebd376e2234"
|
||||
|
@ -7505,6 +7498,11 @@ assert-plus@1.0.0, assert-plus@^1.0.0:
|
|||
resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525"
|
||||
integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=
|
||||
|
||||
assert-plus@^0.2.0:
|
||||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-0.2.0.tgz#d74e1b87e7affc0db8aadb7021f3fe48101ab234"
|
||||
integrity sha1-104bh+ev/A24qttwIfP+SBAasjQ=
|
||||
|
||||
assert@^1.1.1:
|
||||
version "1.4.1"
|
||||
resolved "https://registry.yarnpkg.com/assert/-/assert-1.4.1.tgz#99912d591836b5a6f5b345c0f07eefc08fc65d91"
|
||||
|
@ -7735,11 +7733,21 @@ available-typed-arrays@^1.0.0, available-typed-arrays@^1.0.2:
|
|||
dependencies:
|
||||
array-filter "^1.0.0"
|
||||
|
||||
aws-sign2@~0.6.0:
|
||||
version "0.6.0"
|
||||
resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.6.0.tgz#14342dd38dbcc94d0e5b87d763cd63612c0e794f"
|
||||
integrity sha1-FDQt0428yU0OW4fXY81jYSwOeU8=
|
||||
|
||||
aws-sign2@~0.7.0:
|
||||
version "0.7.0"
|
||||
resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8"
|
||||
integrity sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=
|
||||
|
||||
aws4@^1.2.1:
|
||||
version "1.11.0"
|
||||
resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.11.0.tgz#d61f46d83b2519250e2784daf5b09479a8b41c59"
|
||||
integrity sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==
|
||||
|
||||
aws4@^1.8.0:
|
||||
version "1.8.0"
|
||||
resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.8.0.tgz#f0e003d9ca9e7f59c7a508945d7b2ef9a04a542f"
|
||||
|
@ -8495,6 +8503,13 @@ boolbase@^1.0.0, boolbase@~1.0.0:
|
|||
resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e"
|
||||
integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24=
|
||||
|
||||
boom@2.x.x:
|
||||
version "2.10.1"
|
||||
resolved "https://registry.yarnpkg.com/boom/-/boom-2.10.1.tgz#39c8918ceff5799f83f9492a848f625add0c766f"
|
||||
integrity sha1-OciRjO/1eZ+D+UkqhI9iWt0Mdm8=
|
||||
dependencies:
|
||||
hoek "2.x.x"
|
||||
|
||||
bottleneck@^2.15.3:
|
||||
version "2.18.0"
|
||||
resolved "https://registry.yarnpkg.com/bottleneck/-/bottleneck-2.18.0.tgz#41fa63ae185b65435d789d1700334bc48222dacf"
|
||||
|
@ -9897,9 +9912,9 @@ combine-source-map@^0.8.0, combine-source-map@~0.8.0:
|
|||
lodash.memoize "~3.0.3"
|
||||
source-map "~0.5.3"
|
||||
|
||||
combined-stream@^1.0.6, combined-stream@^1.0.8, combined-stream@~1.0.6:
|
||||
combined-stream@^1.0.5, combined-stream@^1.0.6, combined-stream@^1.0.8, combined-stream@~1.0.5, combined-stream@~1.0.6:
|
||||
version "1.0.8"
|
||||
resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
|
||||
resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz"
|
||||
integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==
|
||||
dependencies:
|
||||
delayed-stream "~1.0.0"
|
||||
|
@ -10474,6 +10489,13 @@ crypt@~0.0.1:
|
|||
resolved "https://registry.yarnpkg.com/crypt/-/crypt-0.0.2.tgz#88d7ff7ec0dfb86f713dc87bbb42d044d3e6c41b"
|
||||
integrity sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs=
|
||||
|
||||
cryptiles@2.x.x:
|
||||
version "2.0.5"
|
||||
resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-2.0.5.tgz#3bdfecdc608147c1c67202fa291e7dca59eaa3b8"
|
||||
integrity sha1-O9/s3GCBR8HGcgL6KR59ylnqo7g=
|
||||
dependencies:
|
||||
boom "2.x.x"
|
||||
|
||||
crypto-browserify@^3.0.0, crypto-browserify@^3.11.0:
|
||||
version "3.12.0"
|
||||
resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.12.0.tgz#396cf9f3137f03e4b8e532c58f698254e00f80ec"
|
||||
|
@ -13422,7 +13444,7 @@ extend-shallow@^3.0.0, extend-shallow@^3.0.2:
|
|||
assign-symbols "^1.0.0"
|
||||
is-extendable "^1.0.1"
|
||||
|
||||
extend@^3.0.0, extend@~3.0.2:
|
||||
extend@^3.0.0, extend@~3.0.0, extend@~3.0.2:
|
||||
version "3.0.2"
|
||||
resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa"
|
||||
integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==
|
||||
|
@ -14088,7 +14110,7 @@ fork-ts-checker-webpack-plugin@4.1.6, fork-ts-checker-webpack-plugin@^4.1.4:
|
|||
tapable "^1.0.0"
|
||||
worker-rpc "^0.1.0"
|
||||
|
||||
form-data@^2.3.1, form-data@^2.5.0:
|
||||
form-data@^2.3.1:
|
||||
version "2.5.0"
|
||||
resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.5.0.tgz#094ec359dc4b55e7d62e0db4acd76e89fe874d37"
|
||||
integrity sha512-WXieX3G/8side6VIqx44ablyULoGruSde5PNTxoUyo5CeyAMX6nVWUd0rgist/EuX655cjhUhTo1Fo3tRYqbcA==
|
||||
|
@ -14115,6 +14137,15 @@ form-data@^4.0.0:
|
|||
combined-stream "^1.0.8"
|
||||
mime-types "^2.1.12"
|
||||
|
||||
form-data@~2.1.1:
|
||||
version "2.1.4"
|
||||
resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.1.4.tgz#33c183acf193276ecaa98143a69e94bfee1750d1"
|
||||
integrity sha1-M8GDrPGTJ27KqYFDpp6Uv+4XUNE=
|
||||
dependencies:
|
||||
asynckit "^0.4.0"
|
||||
combined-stream "^1.0.5"
|
||||
mime-types "^2.1.12"
|
||||
|
||||
form-data@~2.3.2:
|
||||
version "2.3.3"
|
||||
resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6"
|
||||
|
@ -15139,11 +15170,24 @@ handlebars@4.7.7, handlebars@^4.7.7:
|
|||
optionalDependencies:
|
||||
uglify-js "^3.1.4"
|
||||
|
||||
har-schema@^1.0.5:
|
||||
version "1.0.5"
|
||||
resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-1.0.5.tgz#d263135f43307c02c602afc8fe95970c0151369e"
|
||||
integrity sha1-0mMTX0MwfALGAq/I/pWXDAFRNp4=
|
||||
|
||||
har-schema@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92"
|
||||
integrity sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=
|
||||
|
||||
har-validator@~4.2.1:
|
||||
version "4.2.1"
|
||||
resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-4.2.1.tgz#33481d0f1bbff600dd203d75812a6a5fba002e2a"
|
||||
integrity sha1-M0gdDxu/9gDdID11gSpqX7oALio=
|
||||
dependencies:
|
||||
ajv "^4.9.1"
|
||||
har-schema "^1.0.5"
|
||||
|
||||
har-validator@~5.1.3:
|
||||
version "5.1.3"
|
||||
resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.3.tgz#1ef89ebd3e4996557675eed9893110dc350fa080"
|
||||
|
@ -15403,6 +15447,16 @@ hat@0.0.3:
|
|||
resolved "https://registry.yarnpkg.com/hat/-/hat-0.0.3.tgz#bb014a9e64b3788aed8005917413d4ff3d502d8a"
|
||||
integrity sha1-uwFKnmSzeIrtgAWRdBPU/z1QLYo=
|
||||
|
||||
hawk@~3.1.3:
|
||||
version "3.1.3"
|
||||
resolved "https://registry.yarnpkg.com/hawk/-/hawk-3.1.3.tgz#078444bd7c1640b0fe540d2c9b73d59678e8e1c4"
|
||||
integrity sha1-B4REvXwWQLD+VA0sm3PVlnjo4cQ=
|
||||
dependencies:
|
||||
boom "2.x.x"
|
||||
cryptiles "2.x.x"
|
||||
hoek "2.x.x"
|
||||
sntp "1.x.x"
|
||||
|
||||
hdr-histogram-js@^1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/hdr-histogram-js/-/hdr-histogram-js-1.2.0.tgz#1213c0b317f39b9c05bc4f208cb7931dbbc192ae"
|
||||
|
@ -15462,6 +15516,11 @@ hmac-drbg@^1.0.1:
|
|||
minimalistic-assert "^1.0.0"
|
||||
minimalistic-crypto-utils "^1.0.1"
|
||||
|
||||
hoek@2.x.x:
|
||||
version "2.16.3"
|
||||
resolved "https://registry.yarnpkg.com/hoek/-/hoek-2.16.3.tgz#20bb7403d3cea398e91dc4710a8ff1b8274a25ed"
|
||||
integrity sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0=
|
||||
|
||||
hoist-non-react-statics@^2.3.1, hoist-non-react-statics@^2.5.5, hoist-non-react-statics@^3.0.0, hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.1, hoist-non-react-statics@^3.3.2:
|
||||
version "3.3.2"
|
||||
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
|
||||
|
@ -15745,6 +15804,15 @@ http-proxy@^1.17.0, http-proxy@^1.18.1:
|
|||
follow-redirects "^1.0.0"
|
||||
requires-port "^1.0.0"
|
||||
|
||||
http-signature@~1.1.0:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.1.1.tgz#df72e267066cd0ac67fb76adf8e134a8fbcf91bf"
|
||||
integrity sha1-33LiZwZs0Kxn+3at+OE0qPvPkb8=
|
||||
dependencies:
|
||||
assert-plus "^0.2.0"
|
||||
jsprim "^1.2.2"
|
||||
sshpk "^1.7.0"
|
||||
|
||||
http-signature@~1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1"
|
||||
|
@ -19362,6 +19430,11 @@ mime-db@1.44.0, mime-db@1.x.x, "mime-db@>= 1.40.0 < 2":
|
|||
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.44.0.tgz#fa11c5eb0aca1334b4233cb4d52f10c5a6272f92"
|
||||
integrity sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==
|
||||
|
||||
mime-db@1.45.0:
|
||||
version "1.45.0"
|
||||
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.45.0.tgz#cceeda21ccd7c3a745eba2decd55d4b73e7879ea"
|
||||
integrity sha512-CkqLUxUk15hofLoLyljJSrukZi8mAtgd+yE5uO4tqRZsdsAJKv0O+rFMhVDRJgozy+yG6md5KwuXhD4ocIoP+w==
|
||||
|
||||
mime-types@^2.0.1, mime-types@^2.1.12, mime-types@^2.1.26, mime-types@^2.1.27, mime-types@~2.1.17, mime-types@~2.1.19, mime-types@~2.1.24:
|
||||
version "2.1.27"
|
||||
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.27.tgz#47949f98e279ea53119f5722e0f34e529bec009f"
|
||||
|
@ -19369,6 +19442,13 @@ mime-types@^2.0.1, mime-types@^2.1.12, mime-types@^2.1.26, mime-types@^2.1.27, m
|
|||
dependencies:
|
||||
mime-db "1.44.0"
|
||||
|
||||
mime-types@~2.1.7:
|
||||
version "2.1.28"
|
||||
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.28.tgz#1160c4757eab2c5363888e005273ecf79d2a0ecd"
|
||||
integrity sha512-0TO2yJ5YHYr7M2zzT7gDU1tbwHxEUWBCLt0lscSNpcdAfFyJOVEpRYNS7EXVcTLNj/25QO8gulHC5JtTzSE2UQ==
|
||||
dependencies:
|
||||
mime-db "1.45.0"
|
||||
|
||||
mime@1.6.0, mime@^1.2.11, mime@^1.3.4, mime@^1.4.1:
|
||||
version "1.6.0"
|
||||
resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
|
||||
|
@ -20486,6 +20566,11 @@ nyc@^15.0.1:
|
|||
test-exclude "^6.0.0"
|
||||
yargs "^15.0.2"
|
||||
|
||||
oauth-sign@~0.8.1:
|
||||
version "0.8.2"
|
||||
resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43"
|
||||
integrity sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=
|
||||
|
||||
oauth-sign@~0.9.0:
|
||||
version "0.9.0"
|
||||
resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455"
|
||||
|
@ -22443,7 +22528,7 @@ punycode@1.3.2:
|
|||
resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d"
|
||||
integrity sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=
|
||||
|
||||
punycode@^1.2.4, punycode@^1.3.2:
|
||||
punycode@^1.2.4, punycode@^1.3.2, punycode@^1.4.1:
|
||||
version "1.4.1"
|
||||
resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e"
|
||||
integrity sha1-wNWmOycYgArY4esPpSachN1BhF4=
|
||||
|
@ -22525,6 +22610,11 @@ qs@^6.7.0:
|
|||
resolved "https://registry.yarnpkg.com/qs/-/qs-6.9.4.tgz#9090b290d1f91728d3c22e54843ca44aea5ab687"
|
||||
integrity sha512-A1kFqHekCTM7cz0udomYUoYNWjBebHm/5wzU/XqrBRBNWectVH0QIiN+NEcZ0Dte5hvzHwbr8+XQmguPhJ6WdQ==
|
||||
|
||||
qs@~6.4.0:
|
||||
version "6.4.0"
|
||||
resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233"
|
||||
integrity sha1-E+JtKK1rD/qpExLNO/cI7TUecjM=
|
||||
|
||||
qs@~6.5.2:
|
||||
version "6.5.2"
|
||||
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"
|
||||
|
@ -24184,7 +24274,35 @@ request-promise@^4.2.2:
|
|||
stealthy-require "^1.1.1"
|
||||
tough-cookie "^2.3.3"
|
||||
|
||||
request@2.81.0, request@^2.44.0, request@^2.87.0, request@^2.88.0, request@^2.88.2:
|
||||
request@2.81.0:
|
||||
version "2.81.0"
|
||||
resolved "https://registry.yarnpkg.com/request/-/request-2.81.0.tgz#c6928946a0e06c5f8d6f8a9333469ffda46298a0"
|
||||
integrity sha1-xpKJRqDgbF+Nb4qTM0af/aRimKA=
|
||||
dependencies:
|
||||
aws-sign2 "~0.6.0"
|
||||
aws4 "^1.2.1"
|
||||
caseless "~0.12.0"
|
||||
combined-stream "~1.0.5"
|
||||
extend "~3.0.0"
|
||||
forever-agent "~0.6.1"
|
||||
form-data "~2.1.1"
|
||||
har-validator "~4.2.1"
|
||||
hawk "~3.1.3"
|
||||
http-signature "~1.1.0"
|
||||
is-typedarray "~1.0.0"
|
||||
isstream "~0.1.2"
|
||||
json-stringify-safe "~5.0.1"
|
||||
mime-types "~2.1.7"
|
||||
oauth-sign "~0.8.1"
|
||||
performance-now "^0.2.0"
|
||||
qs "~6.4.0"
|
||||
safe-buffer "^5.0.1"
|
||||
stringstream "~0.0.4"
|
||||
tough-cookie "~2.3.0"
|
||||
tunnel-agent "^0.6.0"
|
||||
uuid "^3.0.0"
|
||||
|
||||
request@^2.44.0, request@^2.87.0, request@^2.88.0, request@^2.88.2:
|
||||
version "2.88.2"
|
||||
resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3"
|
||||
integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==
|
||||
|
@ -25261,6 +25379,13 @@ snapdragon@^0.8.1:
|
|||
source-map-resolve "^0.5.0"
|
||||
use "^2.0.0"
|
||||
|
||||
sntp@1.x.x:
|
||||
version "1.0.9"
|
||||
resolved "https://registry.yarnpkg.com/sntp/-/sntp-1.0.9.tgz#6541184cc90aeea6c6e7b35e2659082443c66198"
|
||||
integrity sha1-ZUEYTMkK7qbG57NeJlkIJEPGYZg=
|
||||
dependencies:
|
||||
hoek "2.x.x"
|
||||
|
||||
sockjs-client@1.4.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/sockjs-client/-/sockjs-client-1.4.0.tgz#c9f2568e19c8fd8173b4997ea3420e0bb306c7d5"
|
||||
|
@ -26007,6 +26132,11 @@ stringify-entities@^3.0.1:
|
|||
is-decimal "^1.0.2"
|
||||
is-hexadecimal "^1.0.0"
|
||||
|
||||
stringstream@~0.0.4:
|
||||
version "0.0.6"
|
||||
resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.6.tgz#7880225b0d4ad10e30927d167a1d6f2fd3b33a72"
|
||||
integrity sha512-87GEBAkegbBcweToUrdzf3eLhWNg06FJTebl4BVJz/JgWy8CvEr9dRtX5qWphiynMSQlxxi+QqN0z5T32SLlhA==
|
||||
|
||||
strip-ansi@*, strip-ansi@5.2.0, strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0:
|
||||
version "5.2.0"
|
||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae"
|
||||
|
@ -27082,6 +27212,13 @@ tough-cookie@^4.0.0:
|
|||
punycode "^2.1.1"
|
||||
universalify "^0.1.2"
|
||||
|
||||
tough-cookie@~2.3.0:
|
||||
version "2.3.4"
|
||||
resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.4.tgz#ec60cee38ac675063ffc97a5c18970578ee83655"
|
||||
integrity sha512-TZ6TTfI5NtZnuyy/Kecv+CnoROnyXn2DN97LontgQpCwsX2XyLYCC0ENhYkehSOwAp8rTQKc/NUIF7BkQ5rKLA==
|
||||
dependencies:
|
||||
punycode "^1.4.1"
|
||||
|
||||
tr46@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/tr46/-/tr46-1.0.1.tgz#a8b13fd6bfd2489519674ccde55ba3693b706d09"
|
||||
|
@ -28133,7 +28270,7 @@ uuid@^2.0.1:
|
|||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-2.0.3.tgz#67e2e863797215530dff318e5bf9dcebfd47b21a"
|
||||
integrity sha1-Z+LoY3lyFVMN/zGOW/nc6/1Hsho=
|
||||
|
||||
uuid@^3.3.2, uuid@^3.3.3, uuid@^3.4.0:
|
||||
uuid@^3.0.0, uuid@^3.3.2, uuid@^3.3.3, uuid@^3.4.0:
|
||||
version "3.4.0"
|
||||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"
|
||||
integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue