sleeveforarm
Version:
Making Azure Easy
298 lines (270 loc) • 12.2 kB
text/typescript
import * as fs from "fs-extra";
import * as Path from "path";
import * as ReplaceInFile from "replace-in-file";
import * as Winston from "winston";
import ApplicationInsights from "./applicationInsights";
// tslint:disable-next-line:max-line-length
import * as ApplicationInsightsInfrastructure from "./applicationInsightsInfrastructure";
import * as CommonUtilities from "./common-utilities";
import * as Data from "./data";
import * as IInfrastructure from "./IInfrastructure";
import KeyVault from "./keyvault";
import * as KeyVaultInfrastructure from "./keyvaultInfrastructure";
import MySqlAzure from "./mysql-azure";
import * as MySqlAzureInfrastructure from "./mysql-azureInfrastructure";
import * as Resource from "./resource";
import ResourceGroup from "./resourcegroup";
import * as ResourceGroupInfrastructure from "./resourcegroupInfrastructure";
import WebappNodeAzure from "./webapp-node-azure";
// tslint:disable-next-line:max-line-length
import * as WebappNodeAzureInfrastructure from "./webapp-node-azureInfrastructure";
export type InfraResourceType = IInfrastructure.IInfrastructure<any> &
Resource.Resource;
function createInfraResource(resource: Resource.Resource,
targetDirectoryPath: string)
: InfraResourceType {
let infraResource: InfraResourceType | null = null;
if (CommonUtilities.isClass(resource, ResourceGroup)) {
infraResource =
new ResourceGroupInfrastructure.ResourceGroupInfrastructure();
}
if (CommonUtilities.isClass(resource, KeyVault)) {
infraResource = new KeyVaultInfrastructure.KeyVaultInfrastructure();
}
if (CommonUtilities.isClass(resource, WebappNodeAzure)) {
infraResource =
new WebappNodeAzureInfrastructure.WebappNodeAzureInfrastructure();
}
if (CommonUtilities.isClass(resource, MySqlAzure)) {
infraResource = new MySqlAzureInfrastructure.MySqlAzureInfrastructure();
}
if (CommonUtilities.isClass(resource, ApplicationInsights)) {
infraResource =
new ApplicationInsightsInfrastructure.ApplicationInsightsInfrastructure();
}
if (infraResource === null) {
throw new Error("Unrecognized resource type!");
}
infraResource.initialize(resource, targetDirectoryPath);
return infraResource;
}
export async function init(currentWorkingDirectory: string) {
const assetPath =
Path.join(__dirname,
"..",
"assets",
"cliInit");
if (!(await CommonUtilities.validateResource(Path.basename(process.cwd()),
Data.data.ProjectNameLength))) {
throw new Error(`Project name should be less than \
${Data.data.ProjectNameLength} characters, contains only \
alphanumeric characters and start with a letter\n`);
}
await fs.copy(assetPath, currentWorkingDirectory);
const aiInfrastructure =
new ApplicationInsightsInfrastructure.ApplicationInsightsInfrastructure();
const aiSleevePath = Path.join(currentWorkingDirectory, "AppInsights");
await aiInfrastructure
.initialize(null, aiSleevePath)
.setup();
const aiReplaceOptions = {
files: Path.join(aiSleevePath, "sleeve.js"),
from: /new applicationInsights\(\);/,
to: "new applicationInsights().setGlobalDefault(true);"
};
await ReplaceInFile(aiReplaceOptions);
// NPM publish turns all .gitignore into .npmignore. For awhile it
// seemed that you could put in both a .gitignore and a .npmignore
// and the .gitignore would be ignored but no longer. So now we
// ship a .npmignore and then rename it after install.
await fs.move(Path.join(currentWorkingDirectory, ".npmignore"),
Path.join(currentWorkingDirectory, ".gitignore"));
const locations = await CommonUtilities.azAppServiceListLocations();
const dataCenterEnum: string = locations[0].name.replace(/ /g, "");
const replaceInFileOptions = {
files: Path.join(currentWorkingDirectory, "sleeve.js"),
from: /XXXX/,
to: `DataCenterNames.${dataCenterEnum}`
};
await ReplaceInFile(replaceInFileOptions);
await CommonUtilities.npmSetup(currentWorkingDirectory);
await CommonUtilities.executeOnSleeveResources(currentWorkingDirectory,
(path) => {
return CommonUtilities.npmSetup(path);
});
if (await fs.pathExists(Path.join(currentWorkingDirectory, ".git"))
=== false) {
CommonUtilities.exec("git init", currentWorkingDirectory);
}
}
export async function setup(currentWorkingDirectory: string,
serviceName: string,
serviceType: string): Promise<string> {
const rootPath: string =
await CommonUtilities.findGitRootDir(currentWorkingDirectory);
const targetPath = Path.join(rootPath, serviceName);
let infraResource: IInfrastructure.IInfrastructure<any>;
switch (serviceType) {
case Resource.ResourcesWeSupportSettingUp.MySqlAzure: {
infraResource = new MySqlAzureInfrastructure.MySqlAzureInfrastructure();
break;
}
case Resource.ResourcesWeSupportSettingUp.WebAppNode: {
infraResource =
new WebappNodeAzureInfrastructure.WebappNodeAzureInfrastructure();
break;
}
default: {
throw new Error(`Unsupported resource type ${serviceType}`);
}
}
infraResource.initialize(null, targetPath);
await infraResource.setup();
return targetPath;
}
async function hydrateEnvironment(currentWorkingDirectory: string,
deploymentType: Resource.DeployType):
Promise<InfraResourceType[]> {
const resourcesInEnvironment: InfraResourceType[] = [];
const rootOfDeploymentPath: string =
await CommonUtilities.findGitRootDir(currentWorkingDirectory);
const rootSleevePath = Path.join(rootOfDeploymentPath, "sleeve.js");
if (!(fs.existsSync(rootSleevePath))) {
// tslint:disable-next-line:no-console
console.log("There is no sleeve.js in the root, \
this is not a properly configured project");
process.exit(-1);
}
await CommonUtilities.npmSetup(rootOfDeploymentPath);
const rootResourceGroup: ResourceGroup = require(rootSleevePath);
// tslint:disable-next-line:max-line-length
const rootResourceGroupInfra =
createInfraResource(rootResourceGroup, rootOfDeploymentPath) as ResourceGroupInfrastructure.ResourceGroupInfrastructure;
await rootResourceGroupInfra.hydrate(resourcesInEnvironment,
deploymentType);
resourcesInEnvironment.push(rootResourceGroupInfra);
await CommonUtilities.executeOnSleeveResources(rootOfDeploymentPath,
async (candidatePath) => {
await CommonUtilities.npmSetup(candidatePath);
const sleevePath = Path.join(candidatePath, "sleeve.js");
const resource = require(sleevePath);
const infraResource = createInfraResource(resource, candidatePath);
resourcesInEnvironment.push(
await infraResource.hydrate(resourcesInEnvironment, deploymentType));
});
return resourcesInEnvironment;
}
// TODO: developmentDeploy is really just intended for Node WebAPPs so that
// we deploy code in Azure that will properly set up the environment
// to take a version of Sleeve from the local machine and not look to
// NPM. But we really need to shove this into some kind of property bag
// as the name and usage is confusing.
export async function deployResources(
currentWorkingDirectory: string,
deploymentType: Resource.DeployType,
developmentDeploy = false)
: Promise<InfraResourceType[]> {
const resourcesInEnvironment: InfraResourceType[] =
await hydrateEnvironment(currentWorkingDirectory, deploymentType);
if (resourcesInEnvironment.length === 0) {
// tslint:disable-next-line:no-console
console.log("There are no resources to deploy");
process.exit(-1);
}
const promisesToWaitFor = [];
for (const resource of resourcesInEnvironment) {
promisesToWaitFor.push(resource.deployResource(developmentDeploy));
}
await Promise.all(promisesToWaitFor);
if (deploymentType === Resource.DeployType.Production) {
for (const resource of resourcesInEnvironment) {
if (resource instanceof
WebappNodeAzureInfrastructure.WebappNodeAzureInfrastructure) {
const baseDeployWebApp =
// tslint:disable-next-line:max-line-length
await (resource as WebappNodeAzureInfrastructure.WebappNodeAzureInfrastructure)
.getBaseDeployClassInstance();
console.log(
`Web app ${resource.baseName} is available at \
${await baseDeployWebApp.getDeployedURL()}`);
}
}
}
return resourcesInEnvironment;
}
function getAppInsightsURL(aiName: string, resourceGroupName: string):
Promise<string> {
return CommonUtilities.runAzCommand(`az resource show --name ${aiName} \
--resource-group ${resourceGroupName} \
--resource-type Microsoft.Insights/components`)
.then((result) => {
if (result && result.properties) {
return `https://ms.portal.azure.com/#resource/subscriptions/\
${result.properties.TenantId}/resourceGroups/${resourceGroupName}\
/providers/Microsoft.Insights/components/${aiName}/overview`;
}
return "";
});
}
async function createEnvironmentToGetAiLocation(
currentWorkingDirectory: string,
deploymentType: Resource.DeployType):
Promise<string> {
const resourcesInEnvironment: InfraResourceType[] =
await hydrateEnvironment(currentWorkingDirectory,
deploymentType);
const rootResourceGroupInfra =
CommonUtilities
.findGlobalDefaultResourceByType(resourcesInEnvironment,
// tslint:disable-next-line:max-line-length
ResourceGroupInfrastructure.ResourceGroupInfrastructure) as ResourceGroupInfrastructure.ResourceGroupInfrastructure;
const rootAiInfra =
CommonUtilities
.findGlobalDefaultResourceByType(resourcesInEnvironment,
// tslint:disable-next-line:max-line-length
ApplicationInsightsInfrastructure.ApplicationInsightsInfrastructure) as ApplicationInsightsInfrastructure.ApplicationInsightsInfrastructure;
return getAppInsightsURL(rootAiInfra.appInsightsFullName,
rootResourceGroupInfra.resourceGroupName);
}
export async function getManageResourceData(currentWorkingDirectory: string):
Promise<{[deploymentType: string]: string}> {
const results: {[deploymentType: string]: string} = {};
results[Resource.DeployType.LocalDevelopment] =
await createEnvironmentToGetAiLocation(currentWorkingDirectory,
Resource.DeployType.LocalDevelopment);
results[Resource.DeployType.Production] =
await createEnvironmentToGetAiLocation(currentWorkingDirectory,
Resource.DeployType.Production);
return results;
}
function printManageResults(deployType: Resource.DeployType,
deployResult: string) {
const printPrefix = `${deployType} deployment:`;
if (deployResult === "") {
console.log(`${printPrefix} NO DEPLOYMENT`);
return;
}
console.log(`${printPrefix} ${deployResult}`);
}
export async function manageResource(currentWorkingDirectory: string) {
const manageResults = await getManageResourceData(currentWorkingDirectory);
const devDeployResults =
manageResults[Resource.DeployType.LocalDevelopment];
const productionDeployResults =
manageResults[Resource.DeployType.Production];
printManageResults(Resource.DeployType.LocalDevelopment, devDeployResults);
printManageResults(Resource.DeployType.Production, productionDeployResults);
}
export function setLoggingIfNeeded(argv: any) {
if (argv.verbose) {
Winston.add(Winston.transports.File, {
filename: "log.txt",
level: "silly"
});
Winston.remove(Winston.transports.Console);
Winston.add(Winston.transports.Console, {
level: "silly",
stderrLevels: []
});
}
}