UNPKG

sleeveforarm

Version:
285 lines (258 loc) 11.9 kB
import * as Crypto from "crypto"; import * as fs from "fs-extra"; import * as GeneratePassword from "generate-password"; import * as Path from "path"; import * as Winston from "winston"; import BaseDeployStorageResource from "./BaseDeployStorageResource"; import * as CommonUtilities from "./common-utilities"; import * as data from "./data"; import * as IInfrastructure from "./IInfrastructure"; import INamePassword from "./INamePassword"; import IStorageResource from "./IStorageResource"; import * as KeyVaultInfra from "./keyvaultInfrastructure"; import MySqlAzure from "./mysql-azure"; import PromiseGate from "./promiseGate"; import * as Resource from "./resource"; import * as ServiceEnvironmentUtilities from "./serviceEnvironmentUtilities"; export interface ISqlCreateResult { fullyQualifiedDomainName: string; name: string; } export class BaseDeployMySqlAzureInfrastructure implements BaseDeployStorageResource { private readonly environmentVariablesValues: Array<[string, string]> = []; constructor(private baseMySqlAzureInfrastructure: MySqlAzureInfrastructure, createResult: ISqlCreateResult) { const baseName = baseMySqlAzureInfrastructure.getBaseName(); const hostVariableName = `${baseName}${ServiceEnvironmentUtilities.resourceHostSuffix}`; const userVariableName = `${baseName}${ServiceEnvironmentUtilities.resourceUserSuffix}`; const passwordVariableName = `${baseName}${ServiceEnvironmentUtilities.resourcePasswordSuffix}`; this.environmentVariablesValues.push([hostVariableName, createResult.fullyQualifiedDomainName]); this.environmentVariablesValues.push([userVariableName, `${baseMySqlAzureInfrastructure.securityName}@${createResult.name}`]); this.environmentVariablesValues.push([passwordVariableName, baseMySqlAzureInfrastructure.password]); } /** * Returns a list name/value pairs for environment * variables to describe how to connect to this resource. */ public getEnvironmentVariables(): Array<[string, string]> { return this.environmentVariablesValues; } /** * Creates a firewall on the storage resource with the given * name for the given ipAddress. */ public async setFirewallRule(nameOfResourceSettingRule: string, ipAddress: string) : Promise<this> { await this.baseMySqlAzureInfrastructure .setFirewallRule(nameOfResourceSettingRule, ipAddress); return this; } } export class MySqlAzureInfrastructure extends MySqlAzure // tslint:disable-next-line:max-line-length implements IInfrastructure.IInfrastructure<BaseDeployMySqlAzureInfrastructure>, IStorageResource, INamePassword { public securityName: string; public password: string; public isStorageResource: boolean = true; public mySqlAzureFullName: string; private readonly promiseGate = new PromiseGate(); public getBaseName() { return this.baseName; } public initialize(resource: Resource.Resource | null, targetDirectoryPath: string): this { super.initialize(resource, targetDirectoryPath); if (resource !== null) { Object.assign(this, resource); } return this; } public async setup(): Promise<void> { return await MySqlAzureInfrastructure.internalSetup(__filename, this.targetDirectoryPath, data.data.MySQLNameLength); } public async hydrate(resourcesInEnvironment: Resource.Resource[], deploymentType: Resource.DeployType) : Promise<this> { await super.hydrate(resourcesInEnvironment, deploymentType); if (this.mySqlAzureFullName === undefined) { this.mySqlAzureFullName = (this.resourceGroup.resourceGroupName + this.baseName).toLowerCase(); } this.securityName = this.baseName; this.password = GeneratePassword.generate({ // tslint:disable-next-line:max-line-length // This is a combination of https://docs.microsoft.com/en-us/sql/relational-databases/security/strong-passwords // and https://technet.microsoft.com/en-us/library/cc956689.aspx. // The later came up when setting a password! // The & character got added because it's a reserved command, // even in strings. If we want to use it we need to wrap it // as '"&"'. // I also took out double quotes and ^ because they seem to // disappear when I set them on Keyvault. I submitted a bug on that. exclude: "^\"&\/:|<>+=.'[]{}(),;?*!@", length: 32, numbers: true, strict: true, symbols: false, uppercase: true }); // BUGBUG: To meet the symbol requirement for now. this.password += "$"; return this; } public async deployResource(): Promise<this> { try { await this.resourceGroup.getBaseDeployClassInstance(); const promisesToWaitFor = []; const resourceGroupName = this.resourceGroup.resourceGroupName; const createResult = await CommonUtilities.runAzCommand( `az mysql server create \ --resource-group ${resourceGroupName} --name ${this.mySqlAzureFullName} \ --admin-user ${this.securityName} --admin-password ${this.password} \ --ssl-enforcement Enabled`, CommonUtilities.azCommandOutputs.json); this.promiseGate.openGateSuccess( new BaseDeployMySqlAzureInfrastructure(this, createResult)); const keyVault = CommonUtilities .findGlobalDefaultResourceByType( this.resourcesInEnvironment, KeyVaultInfra.KeyVaultInfrastructure) as KeyVaultInfra.KeyVaultInfrastructure; promisesToWaitFor.push( keyVault .getBaseDeployClassInstance() .then((keyVaultBaseClass) => { return keyVaultBaseClass .setSecret(this.securityName, this.password); })); if (this.deploymentType === Resource.DeployType.LocalDevelopment) { promisesToWaitFor.push(this.setFirewallAllowAll()); } const scriptPaths: string [] = []; for (const checkScriptPath of this.pathToMySqlInitializationScripts) { const scriptPath = Path.isAbsolute(checkScriptPath) ? checkScriptPath : Path.join(this.targetDirectoryPath, checkScriptPath); if (await fs.pathExists(scriptPath) === false) { throw new Error(`Submitted mySql initialization script, \ located at ${scriptPath} for ${this.baseName} does not exist.`); } scriptPaths.push(scriptPath); } let firewallRuleName; if (scriptPaths.length !== 0) { firewallRuleName = await this.setUpFirewallForSqlScript(); for (const scriptPath of scriptPaths) { promisesToWaitFor.push(this.runMySqlScript(scriptPath)); } } try { await Promise.all(promisesToWaitFor); } finally { if (this.deploymentType === Resource.DeployType.Production && firewallRuleName) { await this.removeFirewallRule(firewallRuleName); } } return this; } catch (err) { if (!this.promiseGate.isGateOpen) { this.promiseGate.openGateError(err); } throw err; } } public getBaseDeployClassInstance(): Promise<BaseDeployMySqlAzureInfrastructure> { return this.promiseGate.promise.then( function(baseClass: BaseDeployMySqlAzureInfrastructure) { return baseClass; } ); } public async setUpFirewallForSqlScript() { if (this.deploymentType === Resource.DeployType.LocalDevelopment) { return Promise.resolve(); } // We add in the '-e "SHOW DATABASES"' command just to give the // command something to do in the case that we already have // permission. Otherwise we won't ever get the failure we are // expecting. const initSqlCommand = `mysql -h ${this.mySqlAzureFullName}.mysql.database.azure.com \ -u ${this.securityName}@${this.mySqlAzureFullName} \ -p${this.password} -v -e "SHOW DATABASES"`; let devIp: string = ""; const baseFirewallRuleName = Crypto.randomBytes(10).toString("hex"); const re = /Client with IP address '(.*)' is not allowed to/; try { await CommonUtilities.exec(initSqlCommand, this.targetDirectoryPath); Winston.debug("THE SYSTEM IS IN A BAD STATE. THERE IS A DANGLING \ FIREWALL RULE THAT WAS MOST LIKELY LEFT OVER FROM A PREVIOUS FAILED DEPLOY. \ WE DON'T AUTOMATICALLY FIX THIS YET. PLEASE SEE \ https://github.com/yaronyg/SleeveForARM/issues/38 FOR MORE DETAILS"); return ""; } catch (err) { const result = err.message.match(re); if (result.length !== 2) { throw new Error(`Search for dev IP failed with ${err}`); } devIp = result[1]; } if (devIp === "") { throw new Error("Call to get our IP failed!"); } return await this.setFirewallRule(baseFirewallRuleName, devIp); } public async runMySqlScript(pathToScript: string) { const initSqlCommand = `mysql -h ${this.mySqlAzureFullName}.mysql.database.azure.com \ -u ${this.securityName}@${this.mySqlAzureFullName} \ -p${this.password} -v < "${pathToScript}"`; // tslint:disable-next-line:max-line-length return await CommonUtilities.retryAfterFailure<CommonUtilities.IExecOutput> (async () => { return await CommonUtilities.exec(initSqlCommand, this.targetDirectoryPath); }, 60); } public async setFirewallRule(nameOfResourceSettingRule: string, ipAddress: string) { const ipNoDots = ipAddress.replace(/\./g, ""); const ruleName = `${nameOfResourceSettingRule}${ipNoDots}`; await CommonUtilities.runAzCommand( `az mysql server firewall-rule create \ --resource-group ${this.resourceGroup.resourceGroupName} \ --server ${this.mySqlAzureFullName} \ --name ${ruleName} --start-ip-address ${ipAddress} \ --end-ip-address ${ipAddress}`); return ruleName; } public setFirewallAllowAll() { return CommonUtilities.runAzCommand( `az mysql server firewall-rule create \ --resource-group ${this.resourceGroup.resourceGroupName} \ --server ${this.mySqlAzureFullName} \ --name ${this.baseName}AllAccess --start-ip-address 0.0.0.0 \ --end-ip-address 255.255.255.255`); } private removeFirewallRule(name: string) { return CommonUtilities.runAzCommand( `az mysql server firewall-rule delete \ --resource-group ${this.resourceGroup.resourceGroupName} \ --server-name ${this.mySqlAzureFullName} \ --name ${name} --yes`, CommonUtilities.azCommandOutputs.string); } }