feature: allow overwriting existing Azure config (#42)
* fix: letter casing * feature: allow to overwrite existing azure config * test: add ng-add unit tests
This commit is contained in:
Родитель
7b361ea22d
Коммит
c74dedddf4
|
@ -0,0 +1,4 @@
|
|||
*.spec.ts
|
||||
*.test.json
|
||||
__mocks__
|
||||
__tests__
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"$schema": "node_modules/@angular-devkit/schematics/collection-schema.json",
|
||||
"schematics": {
|
||||
"ng-add": {
|
||||
"description": "Adds Angular Deploy Azure to the application without affecting any templates",
|
||||
"factory": "./src/ng-add/index#ngAdd",
|
||||
"schema": "./src/ng-add/schema.json",
|
||||
"aliases": ["install"]
|
||||
}
|
||||
}
|
||||
}
|
|
@ -75,6 +75,7 @@
|
|||
"typescript": "~3.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@schematics/angular": "^8.2.0",
|
||||
"@types/chai": "^4.0.4",
|
||||
"@types/conf": "^2.1.0",
|
||||
"@types/configstore": "^4.0.0",
|
||||
|
|
|
@ -0,0 +1,139 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import { Tree } from '@angular-devkit/schematics';
|
||||
import { SchematicTestRunner, UnitTestTree } from '@angular-devkit/schematics/testing';
|
||||
import { Schema as WorkspaceOptions } from '@schematics/angular/workspace/schema';
|
||||
import { Schema as ApplicationOptions } from '@schematics/angular/application/schema';
|
||||
import { confirm } from '../util/prompt/confirm';
|
||||
|
||||
jest.mock('../util/azure/auth');
|
||||
jest.mock('../util/azure/subscription');
|
||||
jest.mock('../util/azure/resource-group');
|
||||
jest.mock('../util/azure/account');
|
||||
jest.mock('../util/prompt/confirm');
|
||||
|
||||
const collectionPath = require.resolve('../../collection.test.json');
|
||||
|
||||
const workspaceOptions: WorkspaceOptions = {
|
||||
name: 'workspace',
|
||||
newProjectRoot: 'tests',
|
||||
version: '8.0.0'
|
||||
};
|
||||
|
||||
const appOptions: ApplicationOptions = { name: 'test-app' };
|
||||
|
||||
describe('ng add @azure/ng-deploy', () => {
|
||||
const testRunner = new SchematicTestRunner('schematics', collectionPath);
|
||||
|
||||
async function initAngularProject(): Promise<UnitTestTree> {
|
||||
const appTree = await testRunner.runExternalSchematicAsync('@schematics/angular', 'workspace', workspaceOptions).toPromise();
|
||||
return await testRunner.runExternalSchematicAsync('@schematics/angular', 'application', appOptions, appTree).toPromise();
|
||||
}
|
||||
|
||||
it('fails with a missing tree', async () => {
|
||||
await expect(testRunner.runSchematicAsync('ng-add', {}, Tree.empty()).toPromise()).rejects.toThrow();
|
||||
});
|
||||
|
||||
it('adds azure deploy to an existing project', async () => {
|
||||
let appTree = await initAngularProject();
|
||||
appTree = await testRunner.runSchematicAsync('ng-add', {}, appTree).toPromise()
|
||||
const angularJson = JSON.parse(appTree.readContent('/angular.json'));
|
||||
|
||||
expect(angularJson.projects[appOptions.name].architect.deploy).toBeDefined();
|
||||
expect(angularJson.projects[appOptions.name].architect.azureLogout).toBeDefined();
|
||||
expect(appTree.files).toContain('/azure.json');
|
||||
|
||||
const azureJson = JSON.parse(appTree.readContent('/azure.json'));
|
||||
expect(azureJson).toEqual({
|
||||
hosting: [
|
||||
{
|
||||
app: {
|
||||
configuration: "production",
|
||||
path: "dist/test-app",
|
||||
project: "test-app",
|
||||
target: "build",
|
||||
},
|
||||
azureHosting: {
|
||||
account: "fakeStorageAccount",
|
||||
resourceGroupName: "fake-resource-group",
|
||||
subscription: "fake-subscription-1234",
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
it('should overwrite existing hosting config', async () => {
|
||||
// Simulate existing app setup
|
||||
let appTree = await initAngularProject();
|
||||
appTree = await testRunner.runSchematicAsync('ng-add', {}, appTree).toPromise()
|
||||
appTree.overwrite('/azure.json', appTree.readContent('azure.json').replace(/fake/g, 'existing'));
|
||||
|
||||
const confirmMock = confirm as jest.Mock;
|
||||
confirmMock.mockClear();
|
||||
confirmMock.mockImplementationOnce(() => Promise.resolve(true));
|
||||
|
||||
// Run ng add @azure/deploy on existing project
|
||||
appTree = await testRunner.runSchematicAsync('ng-add', {}, appTree).toPromise()
|
||||
|
||||
expect(confirm).toHaveBeenCalledTimes(1);
|
||||
expect(appTree.files).toContain('/azure.json');
|
||||
|
||||
const azureJson = JSON.parse(appTree.readContent('/azure.json'));
|
||||
expect(azureJson).toEqual({
|
||||
hosting: [
|
||||
{
|
||||
app: {
|
||||
configuration: "production",
|
||||
path: "dist/test-app",
|
||||
project: "test-app",
|
||||
target: "build",
|
||||
},
|
||||
azureHosting: {
|
||||
account: "fakeStorageAccount",
|
||||
resourceGroupName: "fake-resource-group",
|
||||
subscription: "fake-subscription-1234",
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
it('should keep existing hosting config', async () => {
|
||||
// Simulate existing app setup
|
||||
let appTree = await initAngularProject();
|
||||
appTree = await testRunner.runSchematicAsync('ng-add', {}, appTree).toPromise()
|
||||
appTree.overwrite('/azure.json', appTree.readContent('azure.json').replace(/fake/g, 'existing'));
|
||||
|
||||
const confirmMock = confirm as jest.Mock;
|
||||
confirmMock.mockClear();
|
||||
confirmMock.mockImplementationOnce(() => Promise.resolve(false));
|
||||
|
||||
// Run ng add @azure/deploy on existing project
|
||||
appTree = await testRunner.runSchematicAsync('ng-add', {}, appTree).toPromise()
|
||||
|
||||
expect(confirm).toHaveBeenCalledTimes(1);
|
||||
expect(appTree.files).toContain('/azure.json');
|
||||
|
||||
const azureJson = JSON.parse(appTree.readContent('/azure.json'));
|
||||
expect(azureJson).toEqual({
|
||||
hosting: [
|
||||
{
|
||||
app: {
|
||||
configuration: "production",
|
||||
path: "dist/test-app",
|
||||
project: "test-app",
|
||||
target: "build",
|
||||
},
|
||||
azureHosting: {
|
||||
account: "existingStorageAccount",
|
||||
resourceGroupName: "existing-resource-group",
|
||||
subscription: "existing-subscription-1234",
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
});
|
||||
});
|
|
@ -3,13 +3,14 @@
|
|||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import { chain, Rule, SchematicContext, Tree } from '@angular-devkit/schematics';
|
||||
import { confirm } from '../util/prompt/confirm';
|
||||
import { loginToAzure } from '../util/azure/auth';
|
||||
import { DeviceTokenCredentials } from '@azure/ms-rest-nodeauth';
|
||||
import { selectSubscription } from '../util/azure/subscription';
|
||||
import { getResourceGroup } from '../util/azure/resource-group';
|
||||
import { getAccount, getAzureStorageClient } from '../util/azure/account';
|
||||
import { AngularWorkspace } from '../util/workspace/angular-json';
|
||||
import { generateAzureJson } from '../util/workspace/azure-json';
|
||||
import { generateAzureJson, readAzureJson, getAzureHostingConfig } from '../util/workspace/azure-json';
|
||||
import { AddOptions } from '../util/shared/types';
|
||||
|
||||
export function ngAdd(_options: AddOptions): Rule {
|
||||
|
@ -22,38 +23,38 @@ export function ngAdd(_options: AddOptions): Rule {
|
|||
|
||||
export function addDeployAzure(_options: AddOptions): Rule {
|
||||
return async (tree: Tree, _context: SchematicContext) => {
|
||||
// TODO: if azure.json already exists: get data / delete / error
|
||||
|
||||
const project = new AngularWorkspace(tree, _options);
|
||||
const azureJson = readAzureJson(tree);
|
||||
const hostingConfig = azureJson ? getAzureHostingConfig(azureJson, project.projectName) : null;
|
||||
|
||||
if (!hostingConfig || await confirm(`Overwrite existing Azure config for ${ project.projectName }?`)) {
|
||||
|
||||
const auth = await loginToAzure(_context.logger);
|
||||
const credentials = auth.credentials as DeviceTokenCredentials;
|
||||
const subscription = await selectSubscription(auth.subscriptions, _options, _context.logger);
|
||||
const resourceGroup = await getResourceGroup(credentials, subscription, _options, _context.logger);
|
||||
const client = getAzureStorageClient(credentials, subscription);
|
||||
const account = await getAccount(client, resourceGroup, _options, _context.logger);
|
||||
|
||||
const appDeployConfig = {
|
||||
project: project.projectName,
|
||||
target: project.target,
|
||||
configuration: project.configuration,
|
||||
path: project.path
|
||||
};
|
||||
|
||||
const azureDeployConfig = {
|
||||
subscription,
|
||||
resourceGroupName: resourceGroup.name,
|
||||
account
|
||||
};
|
||||
|
||||
// TODO: log url for account at Azure portal
|
||||
generateAzureJson(tree, appDeployConfig, azureDeployConfig);
|
||||
|
||||
}
|
||||
|
||||
const auth = await loginToAzure(_context.logger);
|
||||
const credentials = auth.credentials as DeviceTokenCredentials;
|
||||
project.addLogoutArchitect();
|
||||
|
||||
const subscription = await selectSubscription(auth.subscriptions, _options, _context.logger);
|
||||
|
||||
const resourceGroup = await getResourceGroup(credentials, subscription, _options, _context.logger);
|
||||
|
||||
const client = getAzureStorageClient(credentials, subscription);
|
||||
|
||||
const account = await getAccount(client, resourceGroup, _options, _context.logger);
|
||||
|
||||
const appDeployConfig = {
|
||||
project: project.projectName,
|
||||
target: project.target,
|
||||
configuration: project.configuration,
|
||||
path: project.path
|
||||
};
|
||||
|
||||
const azureDeployConfig = {
|
||||
subscription,
|
||||
resourceGroupName: resourceGroup.name,
|
||||
account
|
||||
};
|
||||
|
||||
// TODO: log url for account at Azure portal
|
||||
generateAzureJson(tree, appDeployConfig, azureDeployConfig);
|
||||
|
||||
project.addDeployArchitect();
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,20 +0,0 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import { Tree } from '@angular-devkit/schematics';
|
||||
import { SchematicTestRunner } from '@angular-devkit/schematics/testing';
|
||||
import * as path from 'path';
|
||||
|
||||
|
||||
const collectionPath = path.join(__dirname, '../collection.json');
|
||||
|
||||
|
||||
describe('ng-deploy-azure', () => {
|
||||
it('works', () => {
|
||||
const runner = new SchematicTestRunner('schematics', collectionPath);
|
||||
const tree = runner.runSchematic('ng-deploy-azure', {}, Tree.empty());
|
||||
|
||||
expect(tree.files).toEqual([]);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,8 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
export const getAccount = () => 'fakeStorageAccount';
|
||||
|
||||
export const getAzureStorageClient = () => null;
|
|
@ -0,0 +1,9 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
export const loginToAzure = () => Promise.resolve({
|
||||
credentials: null,
|
||||
subscriptions: []
|
||||
});
|
|
@ -0,0 +1,10 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
export const getResourceGroup = () => Promise.resolve({
|
||||
id: '4321',
|
||||
name: 'fake-resource-group',
|
||||
location: 'westus'
|
||||
});
|
|
@ -0,0 +1,7 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
export const selectSubscription = () => Promise.resolve('fake-subscription-1234');
|
||||
|
|
@ -181,7 +181,7 @@ export async function createAccount(
|
|||
`https://${ account }.blob.core.windows.net`,
|
||||
pipeline
|
||||
);
|
||||
spinner.start('setting container to be publicly available static site');
|
||||
spinner.start('Setting container to be publicly available static site');
|
||||
await setStaticSiteToPublic(serviceURL);
|
||||
spinner.succeed();
|
||||
}
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import { prompt } from 'inquirer';
|
||||
|
||||
export async function confirm(message: string, confirmByDefault: boolean = false): Promise<boolean> {
|
||||
const { ok } = await prompt<{ ok: any }>([
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'ok',
|
||||
default: confirmByDefault,
|
||||
message
|
||||
}
|
||||
]);
|
||||
return ok;
|
||||
}
|
|
@ -4,6 +4,8 @@
|
|||
*--------------------------------------------------------------------------------------------*/
|
||||
import { SchematicsException, Tree } from '@angular-devkit/schematics';
|
||||
|
||||
const azureJsonFile = 'azure.json';
|
||||
|
||||
export interface AzureDeployConfig {
|
||||
subscription: string;
|
||||
resourceGroupName: string;
|
||||
|
@ -26,18 +28,30 @@ export interface AzureJSON {
|
|||
hosting: AzureHostingConfig[];
|
||||
}
|
||||
|
||||
export function generateAzureJson(tree: Tree, appDeployConfig: AppDeployConfig, azureDeployConfig: AzureDeployConfig) {
|
||||
const path = 'azure.json';
|
||||
const azureJson: AzureJSON = tree.exists(path) ? safeReadJSON(path, tree) : emptyAzureJson();
|
||||
export function readAzureJson(tree: Tree): AzureJSON | null {
|
||||
return tree.exists(azureJsonFile) ? safeReadJSON(azureJsonFile, tree) : null;
|
||||
}
|
||||
|
||||
if (azureJson.hosting.find(config => config.app.project === appDeployConfig.project)) {
|
||||
// TODO: if exists - update?
|
||||
throw new SchematicsException(`Target ${ appDeployConfig.project } already exists in ${ path }`);
|
||||
export function generateAzureJson(tree: Tree, appDeployConfig: AppDeployConfig, azureDeployConfig: AzureDeployConfig) {
|
||||
const azureJson: AzureJSON = readAzureJson(tree) || emptyAzureJson();
|
||||
const existingHostingConfigIndex = getAzureHostingConfigIndex(azureJson, appDeployConfig.project);
|
||||
const hostingConfig = generateHostingConfig(appDeployConfig, azureDeployConfig);
|
||||
|
||||
if (existingHostingConfigIndex >= 0) {
|
||||
azureJson.hosting[existingHostingConfigIndex] = hostingConfig;
|
||||
} else {
|
||||
azureJson.hosting.push(hostingConfig);
|
||||
}
|
||||
|
||||
azureJson.hosting.push(generateHostingConfig(appDeployConfig, azureDeployConfig));
|
||||
overwriteIfExists(tree, azureJsonFile, stringifyFormatted(azureJson));
|
||||
}
|
||||
|
||||
overwriteIfExists(tree, path, stringifyFormatted(azureJson));
|
||||
export function getAzureHostingConfig(azureJson: AzureJSON, projectName: string): AzureHostingConfig | undefined {
|
||||
return azureJson.hosting.find(config => config.app.project === projectName);
|
||||
}
|
||||
|
||||
function getAzureHostingConfigIndex(azureJson: AzureJSON, project: string): number {
|
||||
return azureJson.hosting.findIndex(config => config.app.project === project);
|
||||
}
|
||||
|
||||
const overwriteIfExists = (tree: Tree, path: string, content: string) => {
|
||||
|
@ -68,7 +82,6 @@ function safeReadJSON(path: string, tree: Tree) {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
function generateHostingConfig(appDeployConfig: AppDeployConfig, azureDeployConfig: AzureDeployConfig) {
|
||||
return {
|
||||
app: appDeployConfig,
|
||||
|
|
Загрузка…
Ссылка в новой задаче