UNPKG

sleeveforarm

Version:
317 lines (263 loc) 13 kB
import * as FS from "fs-extra"; import * as Path from "path"; // tslint:disable-next-line:max-line-length import * as ApplicationInsightsInfrastructure from "./applicationInsightsInfrastructure"; import BaseDeployStorageResource from "./BaseDeployStorageResource"; import * as CommonUtilities from "./common-utilities"; import * as data from "./data"; import * as IInfrastructure from "./IInfrastructure"; import IStorageResource from "./IStorageResource"; import PromiseGate from "./promiseGate"; import * as Resource from "./resource"; import * as ServiceEnvironment from "./serviceEnvironmentUtilities"; import WebappNodeAzure from "./webapp-node-azure"; interface IPublishingProfile { publishMethod: string; userName: string; userPWD: string; } export class BaseDeployWebappNodeAzureInfrastructure { constructor(private nodeInfra: WebappNodeAzureInfrastructure) {} public async getDeployedURL() { // tslint:disable-next-line:max-line-length const azResult = await CommonUtilities.runAzCommand(`az webapp show \ --resource-group ${this.nodeInfra.resourceGroup.resourceGroupName} \ --name ${this.nodeInfra.webAppDNSName}`); return "http://" + azResult.defaultHostName; } } export class WebappNodeAzureInfrastructure extends WebappNodeAzure // tslint:disable-next-line:max-line-length implements IInfrastructure.IInfrastructure<BaseDeployWebappNodeAzureInfrastructure> { public webAppServicePlanName: string; public webAppDNSName: string; private readonly promiseGate = new PromiseGate(); public initialize(resource: WebappNodeAzure | null, targetDirectoryPath: string): this { super.initialize(resource, targetDirectoryPath); if (resource !== null) { Object.assign(this, resource); } return this; } public async setup(): Promise<void> { return await WebappNodeAzure.internalSetup(__filename, this.targetDirectoryPath, data.data.WebAppNameLength); } public async hydrate(resourcesInEnvironment: Resource.Resource[], deploymentType: Resource.DeployType) : Promise<this> { await super.hydrate(resourcesInEnvironment, deploymentType); if (this.webAppDNSName === undefined) { this.webAppDNSName = this.resourceGroup.resourceGroupName + this.baseName; } if (this.webAppServicePlanName === undefined) { this.webAppServicePlanName = this.resourceGroup.resourceGroupName + this.baseName + "webAppPlan"; } return this; } public async deployResource(developmentDeploy = false): Promise<this> { await this.resourceGroup.getBaseDeployClassInstance(); const aiResource = CommonUtilities.findGlobalDefaultResourceByType( this.resourcesInEnvironment, ApplicationInsightsInfrastructure // tslint:disable-next-line:max-line-length .ApplicationInsightsInfrastructure) as ApplicationInsightsInfrastructure.ApplicationInsightsInfrastructure; const aiKeyID = (await aiResource.getBaseDeployClassInstance()).getInstrumentationKey(); const storageResources = CommonUtilities.findInfraResourcesByInterface<IStorageResource>( this.resourcesInEnvironment, CommonUtilities.isIStorageResource); const storagePromisesToWaitFor : Array<Promise<BaseDeployStorageResource>> = []; for (const storageResource of storageResources) { storagePromisesToWaitFor .push(storageResource.getBaseDeployClassInstance()); } await (this.deploymentType === Resource.DeployType.Production ? this.deployToProduction(developmentDeploy, storagePromisesToWaitFor, aiKeyID) : this.deployToDev(storagePromisesToWaitFor, aiKeyID)); this.promiseGate .openGateSuccess(new BaseDeployWebappNodeAzureInfrastructure(this)); return this; } public getBaseDeployClassInstance() : Promise<BaseDeployWebappNodeAzureInfrastructure> { return this.promiseGate.promise.then( function(baseClass: BaseDeployWebappNodeAzureInfrastructure) { return baseClass; }); } private async deployToDev(storagePromisesToWaitFor : Array<Promise<BaseDeployStorageResource>>, aiKeyId: string) { const baseStorageResources: BaseDeployStorageResource[] = await Promise.all(storagePromisesToWaitFor); let environmentVariablesArray: Array<[string, string]> = []; for (const baseStorageResource of baseStorageResources) { environmentVariablesArray = [...environmentVariablesArray, ...baseStorageResource.getEnvironmentVariables()]; } environmentVariablesArray.push( [ServiceEnvironment.aiEnvironmentVariableName, aiKeyId]); const sleevePath = CommonUtilities.localScratchDirectory(this.targetDirectoryPath); await FS.ensureDir(sleevePath); const variablePath = Path.join(CommonUtilities .localScratchDirectory(this.targetDirectoryPath), ServiceEnvironment.environmentFileName); FS.removeSync(variablePath); for (const nameValuePair of environmentVariablesArray) { FS.appendFileSync(variablePath, `${nameValuePair[0]} ${nameValuePair[1]}\n`); } } private async deployToProduction( developmentDeploy: boolean, storagePromisesToWaitFor : Array<Promise<BaseDeployStorageResource>>, aiKeyId: string) { const resourceGroupName = this.resourceGroup.resourceGroupName; const webPromise = CommonUtilities.runAzCommand( `az appservice plan create \ --name ${this.webAppServicePlanName} \ --resource-group ${resourceGroupName} --sku FREE`) .then(() => { return CommonUtilities.runAzCommand( `az webapp create \ --name ${this.webAppDNSName} \ --resource-group ${resourceGroupName} \ --plan ${this.webAppServicePlanName}`, CommonUtilities.azCommandOutputs.json); }); const webAppCreateResult = await webPromise; const baseStorageResources: BaseDeployStorageResource[] = await Promise.all(storagePromisesToWaitFor); let environmentVariablesArray: Array<[string, string]> = []; const secondStepPromises: Array<Promise<any>> = []; const webAppIPs: string[] = webAppCreateResult.outboundIpAddresses.split(","); for (const baseStorageResource of baseStorageResources) { webAppIPs.forEach((ipAddr) => { secondStepPromises.push( baseStorageResource .setFirewallRule(this.baseName, ipAddr)); }); environmentVariablesArray = [...environmentVariablesArray, ...baseStorageResource.getEnvironmentVariables()]; } let environmentalVariables: string = ""; for (const variablePair of environmentVariablesArray) { environmentalVariables += `${variablePair[0]}=${variablePair[1]} `; } environmentalVariables += `${ServiceEnvironment.aiEnvironmentVariableName}=${aiKeyId}`; if (environmentalVariables !== "") { secondStepPromises.push(CommonUtilities.runAzCommand( `az webapp config appsettings set \ --name ${this.webAppDNSName} \ --resource-group ${this.resourceGroup.resourceGroupName} \ --settings ${environmentalVariables}`)); } await Promise.all(secondStepPromises); await CommonUtilities.runAzCommand( `az webapp deployment source config-local-git \ --name ${this.webAppDNSName} --resource-group ${resourceGroupName} \ --query url --output tsv`, CommonUtilities.azCommandOutputs.string); await this.deployToWebApp(developmentDeploy); } /** * Handles copying the local web app code to Azure * @developmentDeploy This is only used for development of sleeveforarm, * it lets us know we need to deploy to the webapp a development version * of sleeveforarm. */ private async deployToWebApp(developmentDeploy = false): Promise<void> { const resourceGroupName = this.resourceGroup.resourceGroupName; const profiles: IPublishingProfile[] = await CommonUtilities.runAzCommand( `az webapp deployment list-publishing-profiles --name ${this.webAppDNSName} \ --resource-group ${resourceGroupName}`); const msDeployProfile = profiles.find((profile) => profile.publishMethod === "MSDeploy"); if (msDeployProfile === undefined) { throw new Error("We didn't find the MSDeploy profile, huh?"); } const username = msDeployProfile.userName; const password = msDeployProfile.userPWD; const gitURL = // tslint:disable-next-line:max-line-length `https://${username}:${password}@${this.webAppDNSName}.scm.azurewebsites.net/${this.webAppDNSName}.git`; const gitCloneDepotParentPath = CommonUtilities.localScratchDirectory(this.targetDirectoryPath); const gitCloneDepotPath = Path.join(gitCloneDepotParentPath, this.webAppDNSName); await FS.emptyDir(gitCloneDepotParentPath); await CommonUtilities.retryAfterFailure<CommonUtilities.IExecOutput>( async () => { return await CommonUtilities.exec(`git clone ${gitURL}`, gitCloneDepotParentPath); }, 60); const directoryContents = await FS.readdir(gitCloneDepotPath); // It's a git depo so it always has a hidden .git file, hence there // will be at least one file if (directoryContents.length > 1) { // This command fails if there isn't at least one file (other than // .git) in the directory, hence why we have the check above. await CommonUtilities.exec("git rm -f -r -q *", gitCloneDepotPath); } const nodeModulesPath = Path.join(this.targetDirectoryPath, "node_modules"); const sleevePath = Path.join(this.targetDirectoryPath, ".sleeve"); await FS.copy(this.targetDirectoryPath, gitCloneDepotPath, { filter: (src) => (src !== nodeModulesPath && src !== sleevePath) }); if (developmentDeploy) { await this.developDeployToWebApp(gitCloneDepotPath); } await CommonUtilities.exec("git add -A", gitCloneDepotPath); const result = await CommonUtilities.exec("git status --porcelain=v2", gitCloneDepotPath); if (result.stdout !== "") { await CommonUtilities.exec("git commit -am \"Prep for release\"", gitCloneDepotPath); await CommonUtilities.exec("git push", gitCloneDepotPath); } } /** * Deploys a development version of SleeveForArm */ private async developDeployToWebApp(gitCloneDepotPath: string) : Promise<void> { // We want to clone node_modules in this case so we need to // get rid of .gitignore await FS.remove(Path.join(gitCloneDepotPath, ".gitignore")); const sleeveForArmClonePath = Path.join(gitCloneDepotPath, "sleeveforarm") ; await FS.ensureDir(sleeveForArmClonePath); const depotPath = Path.join(__dirname, ".."); const disposableTestFilesPath = Path.join(depotPath, "disposableTestFiles"); const nodeModulesPath = Path.join(depotPath, "node_modules"); await FS.copy(depotPath, sleeveForArmClonePath, { filter: (src) => (src !== disposableTestFilesPath) && (src !== nodeModulesPath) }); // Need to make the node module files just look like regular files // Otherwise the WebApp Git Repo will treat sleeveforarm as a // sub-module and not properly copy it over. await FS.remove(Path.join(sleeveForArmClonePath, ".git")); // Otherwise we won't check in any of the .js or other files we normally // ignore. await FS.remove(Path.join(sleeveForArmClonePath, ".gitignore")); } }