diff --git a/extensions/arc/package.json b/extensions/arc/package.json index 98e2361faf..c35e709ced 100644 --- a/extensions/arc/package.json +++ b/extensions/arc/package.json @@ -137,7 +137,11 @@ "description": "%resource.type.azure.arc.description%", "platforms": "*", "icon": "./images/data_controller.svg", - "tags": ["Hybrid", "SQL Server", "PostgreSQL"], + "tags": [ + "Hybrid", + "SQL Server", + "PostgreSQL" + ], "providers": [ { "notebookWizard": { @@ -591,7 +595,10 @@ "description": "%resource.type.arc.sql.description%", "platforms": "*", "icon": "./images/miaa.svg", - "tags": ["Hybrid", "SQL Server"], + "tags": [ + "Hybrid", + "SQL Server" + ], "providers": [ { "notebookWizard": { @@ -780,7 +787,10 @@ "description": "%resource.type.arc.postgres.description%", "platforms": "*", "icon": "./images/postgres.svg", - "tags": ["Hybrid", "PostgreSQL"], + "tags": [ + "Hybrid", + "PostgreSQL" + ], "providers": [ { "notebookWizard": { @@ -1024,7 +1034,8 @@ "dependencies": { "request": "^2.88.0", "uuid": "^8.3.0", - "vscode-nls": "^4.1.2" + "vscode-nls": "^4.1.2", + "yamljs": "^0.3.0" }, "devDependencies": { "@types/mocha": "^5.2.5", @@ -1032,6 +1043,7 @@ "@types/request": "^2.48.3", "@types/sinon": "^9.0.4", "@types/uuid": "^8.3.0", + "@types/yamljs": "^0.2.31", "mocha": "^5.2.0", "mocha-junit-reporter": "^1.17.0", "mocha-multi-reporters": "^1.1.7", diff --git a/extensions/arc/src/common/kubeUtils.ts b/extensions/arc/src/common/kubeUtils.ts new file mode 100644 index 0000000000..71d551ce50 --- /dev/null +++ b/extensions/arc/src/common/kubeUtils.ts @@ -0,0 +1,39 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as os from 'os'; +import * as path from 'path'; +import * as yamljs from 'yamljs'; +import * as loc from '../localizedConstants'; +import { throwUnless } from './utils'; +export interface KubeClusterContext { + name: string; + isCurrentContext: boolean; +} + +export function getKubeConfigClusterContexts(configFile: string): Promise { + const config: any = yamljs.load(configFile); + const rawContexts = config['contexts']; + throwUnless(rawContexts && rawContexts.length, loc.noContextFound(configFile)); + const currentContext = config['current-context']; + throwUnless(currentContext, loc.noCurrentContextFound(configFile)); + const contexts: KubeClusterContext[] = []; + rawContexts.forEach(rawContext => { + const name = rawContext['name']; + throwUnless(name, loc.noNameInContext(configFile)); + if (name) { + contexts.push({ + name: name, + isCurrentContext: name === currentContext + }); + } + }); + return Promise.resolve(contexts); +} + +export function getDefaultKubeConfigPath(): string { + return path.join(os.homedir(), '.kube', 'config'); +} + diff --git a/extensions/arc/src/common/utils.ts b/extensions/arc/src/common/utils.ts index 23620c5695..0a765d2983 100644 --- a/extensions/arc/src/common/utils.ts +++ b/extensions/arc/src/common/utils.ts @@ -67,7 +67,7 @@ export function getResourceTypeIcon(resourceType: string | undefined): IconPath /** * Returns the text to display for known connection modes - * @param connectionMode The string repsenting the connection mode + * @param connectionMode The string representing the connection mode */ export function getConnectionModeDisplayText(connectionMode: string | undefined): string { connectionMode = connectionMode ?? ''; @@ -282,8 +282,18 @@ export function convertToGibibyteString(value: string): string { * @param condition * @param message */ -export function throwUnless(condition: boolean, message?: string): asserts condition { +export function throwUnless(condition: any, message?: string): asserts condition { if (!condition) { throw new Error(message); } } + +export async function tryExecuteAction(action: () => T | PromiseLike): Promise<{ result: T | undefined, error: any }> { + let error: any, result: T | undefined; + try { + result = await action(); + } catch (e) { + error = e; + } + return { result, error }; +} diff --git a/extensions/arc/src/localizedConstants.ts b/extensions/arc/src/localizedConstants.ts index 5037b1ef3a..86c65ff253 100644 --- a/extensions/arc/src/localizedConstants.ts +++ b/extensions/arc/src/localizedConstants.ts @@ -202,3 +202,6 @@ export const variableValueFetchForUnsupportedVariable = (variableName: string) = export const isPasswordFetchForUnsupportedVariable = (variableName: string) => localize('getIsPassword.unknownVariableName', "Attempt to get isPassword for unknown variable:{0}", variableName); export const noControllerInfoFound = (name: string) => localize('noControllerInfoFound', "Controller Info could not be found with name: {0}", name); export const noPasswordFound = (controllerName: string) => localize('noPasswordFound', "Password could not be retrieved for controller: {0} and user did not provide a password. Please retry later.", controllerName); +export const noContextFound = (configFile: string) => localize('noContextFound', "No 'contexts' found in the config file: {0}", configFile); +export const noCurrentContextFound = (configFile: string) => localize('noCurrentContextFound', "No context is marked as 'current-context' in the config file: {0}", configFile); +export const noNameInContext = (configFile: string) => localize('noNameInContext', "No name field was found in a cluster context in the config file: {0}", configFile); diff --git a/extensions/arc/src/test/common/kubeUtils.test.ts b/extensions/arc/src/test/common/kubeUtils.test.ts new file mode 100644 index 0000000000..ecd9988f70 --- /dev/null +++ b/extensions/arc/src/test/common/kubeUtils.test.ts @@ -0,0 +1,62 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import 'mocha'; +import * as path from 'path'; +import * as sinon from 'sinon'; +import * as yamljs from 'yamljs'; +import { getDefaultKubeConfigPath, getKubeConfigClusterContexts, KubeClusterContext } from '../../common/kubeUtils'; +import { tryExecuteAction } from '../../common/utils'; + +const kubeConfig = +{ + 'contexts': [ + { + 'context': { + 'cluster': 'docker-desktop', + 'user': 'docker-desktop' + }, + 'name': 'docker-for-desktop' + }, + { + 'context': { + 'cluster': 'kubernetes', + 'user': 'kubernetes-admin' + }, + 'name': 'kubernetes-admin@kubernetes' + } + ], + 'current-context': 'docker-for-desktop' +}; +describe('KubeUtils', function (): void { + const configFile = 'kubeConfig'; + + afterEach('KubeUtils cleanup', () => { + sinon.restore(); + }); + + it('getDefaultKubeConfigPath', async () => { + getDefaultKubeConfigPath().should.endWith(path.join('.kube', 'config')); + }); + + describe('get Kube Config Cluster Contexts', () => { + it('success', async () => { + sinon.stub(yamljs, 'load').returns(kubeConfig); + const verifyContexts = (contexts: KubeClusterContext[], testName: string) => { + contexts.length.should.equal(2, `test: ${testName} failed`); + contexts[0].name.should.equal('docker-for-desktop', `test: ${testName} failed`); + contexts[0].isCurrentContext.should.be.true(`test: ${testName} failed`); + contexts[1].name.should.equal('kubernetes-admin@kubernetes', `test: ${testName} failed`); + contexts[1].isCurrentContext.should.be.false(`test: ${testName} failed`); + }; + verifyContexts(await getKubeConfigClusterContexts(configFile), 'getKubeConfigClusterContexts'); + }); + it('throws error when unable to load config file', async () => { + const error = new Error('unknown error accessing file'); + sinon.stub(yamljs, 'load').throws(error); //erroring config file load + ((await tryExecuteAction(() => getKubeConfigClusterContexts(configFile))).error).should.equal(error, `test: getKubeConfigClusterContexts failed`); + }); + }); +}); diff --git a/extensions/arc/src/test/mocks/fakeRadioButton.ts b/extensions/arc/src/test/mocks/fakeRadioButton.ts new file mode 100644 index 0000000000..a1030f9aab --- /dev/null +++ b/extensions/arc/src/test/mocks/fakeRadioButton.ts @@ -0,0 +1,91 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as azdata from 'azdata'; +import * as vscode from 'vscode'; + +export class FakeRadioButton implements azdata.RadioButtonComponent { + + private _onDidClickEmitter = new vscode.EventEmitter(); + + onDidClick = this._onDidClickEmitter.event; + + constructor(props: azdata.RadioButtonProperties) { + this.label = props.label; + this.value = props.value; + this.checked = props.checked; + this.enabled = props.enabled; + } + + //#region RadioButtonProperties implementation + label?: string; + value?: string; + checked?: boolean; + //#endregion + + click() { + this.checked = true; + this._onDidClickEmitter.fire(this); + } + //#region Component Implementation + id: string = ''; + updateProperties(_properties: { [key: string]: any; }): Thenable { + throw new Error('Method not implemented.'); + } + updateProperty(_key: string, _value: any): Thenable { + throw new Error('Method not implemented.'); + } + updateCssStyles(_cssStyles: { [key: string]: string; }): Thenable { + throw new Error('Method not implemented.'); + } + onValidityChanged: vscode.Event = >{}; + valid: boolean = false; + validate(): Thenable { + throw new Error('Method not implemented.'); + } + focus(): Thenable { + throw new Error('Method not implemented.'); + } + ariaHidden?: boolean | undefined; + //#endregion + + //#region ComponentProperties Implementation + height?: number | string; + width?: number | string; + /** + * The position CSS property. Empty by default. + * This is particularly useful if laying out components inside a FlexContainer and + * the size of the component is meant to be a fixed size. In this case the position must be + * set to 'absolute', with the parent FlexContainer having 'relative' position. + * Without this the component will fail to correctly size itself + */ + position?: azdata.PositionType; + /** + * Whether the component is enabled in the DOM + */ + enabled?: boolean; + /** + * Corresponds to the display CSS property for the element + */ + display?: azdata.DisplayType; + /** + * Corresponds to the aria-label accessibility attribute for this component + */ + ariaLabel?: string; + /** + * Corresponds to the role accessibility attribute for this component + */ + ariaRole?: string; + /** + * Corresponds to the aria-selected accessibility attribute for this component + */ + ariaSelected?: boolean; + /** + * Matches the CSS style key and its available values. + */ + CSSStyles?: { [key: string]: string }; + //#endregion + +} diff --git a/extensions/arc/src/test/stubs.ts b/extensions/arc/src/test/stubs.ts index 8fa717c16a..22a7e49a62 100644 --- a/extensions/arc/src/test/stubs.ts +++ b/extensions/arc/src/test/stubs.ts @@ -3,8 +3,66 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as azdata from 'azdata'; +import * as TypeMoq from 'typemoq'; import * as vscode from 'vscode'; +export function createModelViewMock() { + const mockModelBuilder = TypeMoq.Mock.ofType(); + const mockTextBuilder = setupMockComponentBuilder(); + const mockInputBoxBuilder = setupMockComponentBuilder(); + const mockRadioButtonBuilder = setupMockComponentBuilder(); + const mockDivBuilder = setupMockContainerBuilder(); + const mockLoadingBuilder = setupMockLoadingBuilder(); + mockModelBuilder.setup(b => b.loadingComponent()).returns(() => mockLoadingBuilder.object); + mockModelBuilder.setup(b => b.text()).returns(() => mockTextBuilder.object); + mockModelBuilder.setup(b => b.inputBox()).returns(() => mockInputBoxBuilder.object); + mockModelBuilder.setup(b => b.radioButton()).returns(() => mockRadioButtonBuilder.object); + mockModelBuilder.setup(b => b.divContainer()).returns(() => mockDivBuilder.object); + const mockModelView = TypeMoq.Mock.ofType(); + mockModelView.setup(mv => mv.modelBuilder).returns(() => mockModelBuilder.object); + return { mockModelView, mockModelBuilder, mockTextBuilder, mockInputBoxBuilder, mockRadioButtonBuilder, mockDivBuilder }; +} + +function setupMockLoadingBuilder( + loadingBuilderGetter?: (item: azdata.Component) => azdata.LoadingComponentBuilder, + mockLoadingBuilder?: TypeMoq.IMock +): TypeMoq.IMock { + mockLoadingBuilder = mockLoadingBuilder ?? setupMockComponentBuilder(); + let item: azdata.Component; + mockLoadingBuilder.setup(b => b.withItem(TypeMoq.It.isAny())).callback((_item) => item = _item).returns(() => loadingBuilderGetter ? loadingBuilderGetter(item) : mockLoadingBuilder!.object); + return mockLoadingBuilder; +} + +export function setupMockComponentBuilder = azdata.ComponentBuilder>( + componentGetter?: (props: P) => T, + mockComponentBuilder?: TypeMoq.IMock, +): TypeMoq.IMock { + mockComponentBuilder = mockComponentBuilder ?? TypeMoq.Mock.ofType(); + const returnComponent = TypeMoq.Mock.ofType(); + // Need to setup 'then' for when a mocked object is resolved otherwise the test will hang : https://github.com/florinn/typemoq/issues/66 + returnComponent.setup((x: any) => x.then).returns(() => { }); + let compProps: P; + mockComponentBuilder.setup(b => b.withProperties(TypeMoq.It.isAny())).callback((props: P) => compProps = props).returns(() => mockComponentBuilder!.object); + mockComponentBuilder.setup(b => b.component()).returns(() => { + return componentGetter ? componentGetter(compProps) : Object.assign(Object.assign({}, returnComponent.object), compProps); + }); + + // For now just have these be passthrough - can hook up additional functionality later if needed + mockComponentBuilder.setup(b => b.withValidation(TypeMoq.It.isAny())).returns(() => mockComponentBuilder!.object); + return mockComponentBuilder; +} + +export function setupMockContainerBuilder, P extends azdata.ComponentProperties, B extends azdata.ContainerBuilder = azdata.ContainerBuilder>( + mockContainerBuilder?: TypeMoq.IMock +): TypeMoq.IMock { + mockContainerBuilder = mockContainerBuilder ?? setupMockComponentBuilder(); + // For now just have these be passthrough - can hook up additional functionality later if needed + mockContainerBuilder.setup(b => b.withItems(TypeMoq.It.isAny(), undefined)).returns(() => mockContainerBuilder!.object); + mockContainerBuilder.setup(b => b.withLayout(TypeMoq.It.isAny())).returns(() => mockContainerBuilder!.object); + return mockContainerBuilder; +} + export class MockInputBox implements vscode.InputBox { private _value: string = ''; public get value(): string { diff --git a/extensions/arc/src/test/ui/components/radioOptionsGroup.test.ts b/extensions/arc/src/test/ui/components/radioOptionsGroup.test.ts new file mode 100644 index 0000000000..2db4d4cccc --- /dev/null +++ b/extensions/arc/src/test/ui/components/radioOptionsGroup.test.ts @@ -0,0 +1,88 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as azdata from 'azdata'; +import * as should from 'should'; +import { getErrorMessage } from '../../../common/utils'; +import { RadioOptionsGroup, RadioOptionsInfo } from '../../../ui/components/radioOptionsGroup'; +import { FakeRadioButton } from '../../mocks/fakeRadioButton'; +import { setupMockComponentBuilder, createModelViewMock } from '../../stubs'; + + +const loadingError = new Error('Error loading options'); +const radioOptionsInfo = { + values: [ + 'value1', + 'value2' + ], + defaultValue: 'value2' +}; +const divItems: azdata.Component[] = []; +let radioOptionsGroup: RadioOptionsGroup; + + +describe('radioOptionsGroup', function (): void { + beforeEach(async () => { + const { mockModelView, mockRadioButtonBuilder, mockDivBuilder } = createModelViewMock(); + mockRadioButtonBuilder.reset(); // reset any previous mock so that we can set our own. + setupMockComponentBuilder( + (props) => new FakeRadioButton(props), + mockRadioButtonBuilder, + ); + mockDivBuilder.reset(); // reset previous setups so new setups we are about to create will replace the setups instead creating a recording chain + // create new setups for the DivContainer with custom behavior + setupMockComponentBuilder( + () => { + addItem: (item) => { divItems.push(item); }, + clearItems: () => { divItems.length = 0; }, + get items() { return divItems; }, + }, + mockDivBuilder + ); + radioOptionsGroup = new RadioOptionsGroup(mockModelView.object, (_disposable) => { }); + await radioOptionsGroup.load(async () => radioOptionsInfo); + }); + + it('verify construction and load', async () => { + should(radioOptionsGroup).not.be.undefined(); + should(radioOptionsGroup.value).not.be.undefined(); + radioOptionsGroup.value!.should.equal('value2', 'radio options group should be the default checked value'); + // verify all the radioButtons created in the group + verifyRadioGroup(); + }); + + it('onClick', async () => { + // click the radioButton corresponding to 'value1' + (divItems as FakeRadioButton[]).filter(r => r.value === 'value1').pop()!.click(); + radioOptionsGroup.value!.should.equal('value1', 'radio options group should correspond to the radioButton that we clicked'); + // verify all the radioButtons created in the group + verifyRadioGroup(); + }); + + it('load throws', async () => { + radioOptionsGroup.load(() => { throw loadingError; }); + //in error case radioButtons array wont hold radioButtons but holds a TextComponent with value equal to error string + divItems.length.should.equal(1, 'There is should be only one element in the divContainer when loading error happens'); + const label = divItems[0] as azdata.TextComponent; + should(label.value).not.be.undefined(); + label.value!.should.deepEqual(getErrorMessage(loadingError)); + should(label.CSSStyles).not.be.undefined(); + should(label.CSSStyles!.color).not.be.undefined(); + label.CSSStyles!.color.should.equal('Red'); + }); +}); + +function verifyRadioGroup() { + const radioButtons = divItems as FakeRadioButton[]; + radioButtons.length.should.equal(radioOptionsInfo.values!.length); + radioButtons.forEach(rb => { + should(rb.label).not.be.undefined(); + should(rb.value).not.be.undefined(); + should(rb.enabled).not.be.undefined(); + rb.label!.should.equal(rb.value); + rb.enabled!.should.be.true(); + }); +} + diff --git a/extensions/arc/src/ui/components/radioOptionsGroup.ts b/extensions/arc/src/ui/components/radioOptionsGroup.ts new file mode 100644 index 0000000000..2306cd1d18 --- /dev/null +++ b/extensions/arc/src/ui/components/radioOptionsGroup.ts @@ -0,0 +1,72 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import * as azdata from 'azdata'; +import * as vscode from 'vscode'; +import { getErrorMessage } from '../../common/utils'; + +export interface RadioOptionsInfo { + values?: string[], + defaultValue: string +} + +export class RadioOptionsGroup { + static id: number = 1; + private _divContainer!: azdata.DivContainer; + private _loadingBuilder: azdata.LoadingComponentBuilder; + private _currentRadioOption!: azdata.RadioButtonComponent; + + constructor(private _view: azdata.ModelView, private _onNewDisposableCreated: (disposable: vscode.Disposable) => void, private _groupName: string = `RadioOptionsGroup${RadioOptionsGroup.id++}`) { + const divBuilder = this._view.modelBuilder.divContainer(); + const divBuilderWithProperties = divBuilder.withProperties({ clickable: false }); + this._divContainer = divBuilderWithProperties.component(); + const loadingComponentBuilder = this._view.modelBuilder.loadingComponent(); + this._loadingBuilder = loadingComponentBuilder.withItem(this._divContainer); + } + + public component(): azdata.LoadingComponent { + return this._loadingBuilder.component(); + } + + async load(optionsInfoGetter: () => Promise): Promise { + this.component().loading = true; + this._divContainer.clearItems(); + try { + const optionsInfo = await optionsInfoGetter(); + const options = optionsInfo.values!; + let defaultValue: string = optionsInfo.defaultValue!; + options.forEach((option: string) => { + const radioOption = this._view!.modelBuilder.radioButton().withProperties({ + label: option, + checked: option === defaultValue, + name: this._groupName, + value: option, + enabled: true + }).component(); + if (radioOption.checked) { + this._currentRadioOption = radioOption; + } + this._onNewDisposableCreated(radioOption.onDidClick(() => { + if (this._currentRadioOption !== radioOption) { + // uncheck the previously saved radio option, the ui gets handled correctly even if we did not do this due to the use of the 'groupName', + // however, the checked properties on the radio button do not get updated, so while the stuff works even if we left the previous option checked, + // it is just better to keep things clean. + this._currentRadioOption.checked = false; + this._currentRadioOption = radioOption; + } + })); + this._divContainer.addItem(radioOption); + }); + } + catch (e) { + const errorLabel = this._view!.modelBuilder.text().withProperties({ value: getErrorMessage(e), CSSStyles: { 'color': 'Red' } }).component(); + this._divContainer.addItem(errorLabel); + } + this.component().loading = false; + } + + get value(): string | undefined { + return this._currentRadioOption?.value; + } +} diff --git a/extensions/arc/yarn.lock b/extensions/arc/yarn.lock index f87395b60d..36f59e1832 100644 --- a/extensions/arc/yarn.lock +++ b/extensions/arc/yarn.lock @@ -270,6 +270,11 @@ resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.0.tgz#215c231dff736d5ba92410e6d602050cce7e273f" integrity sha512-eQ9qFW/fhfGJF8WKHGEHZEyVWfZxrT+6CLIJGBcZPfxUh/+BnEj+UCGYMlr9qZuX/2AltsvwrGqp0LhEW8D0zQ== +"@types/yamljs@^0.2.31": + version "0.2.31" + resolved "https://registry.yarnpkg.com/@types/yamljs/-/yamljs-0.2.31.tgz#b1a620b115c96db7b3bfdf0cf54aee0c57139245" + integrity sha512-QcJ5ZczaXAqbVD3o8mw/mEBhRvO5UAdTtbvgwL/OgoWubvNBh6/MxLBAigtcgIFaq3shon9m3POIxQaLQt4fxQ== + ajv@^6.5.5: version "6.12.0" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.0.tgz#06d60b96d87b8454a5adaba86e7854da629db4b7" @@ -299,6 +304,13 @@ append-transform@^2.0.0: dependencies: default-require-extensions "^3.0.0" +argparse@^1.0.7: + version "1.0.10" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" + integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== + dependencies: + sprintf-js "~1.0.2" + asn1@~0.2.3: version "0.2.4" resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136" @@ -580,7 +592,7 @@ glob@7.1.2: once "^1.3.0" path-is-absolute "^1.0.0" -glob@^7.1.2, glob@^7.1.3: +glob@^7.0.5, glob@^7.1.2, glob@^7.1.3: version "7.1.6" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== @@ -1109,6 +1121,11 @@ source-map@^0.6.1: resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== +sprintf-js@~1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" + integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= + sshpk@^1.7.0: version "1.16.1" resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.16.1.tgz#fb661c0bef29b39db40769ee39fa70093d6f6877" @@ -1251,3 +1268,11 @@ xml@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/xml/-/xml-1.0.1.tgz#78ba72020029c5bc87b8a81a3cfcd74b4a2fc1e5" integrity sha1-eLpyAgApxbyHuKgaPPzXS0ovweU= + +yamljs@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/yamljs/-/yamljs-0.3.0.tgz#dc060bf267447b39f7304e9b2bfbe8b5a7ddb03b" + integrity sha512-C/FsVVhht4iPQYXOInoxUM/1ELSf9EsgKH34FofQOp6hwCPrW4vG4w5++TED3xRUo8gD7l0P1J1dLlDYzODsTQ== + dependencies: + argparse "^1.0.7" + glob "^7.0.5"