sleeveforarm
Version:
Making Azure Easy
247 lines • 12.2 kB
JavaScript
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
const Crypto = require("crypto");
const fs = require("fs-extra");
const GeneratePassword = require("generate-password");
const Path = require("path");
const Winston = require("winston");
const CommonUtilities = require("./common-utilities");
const data = require("./data");
const KeyVaultInfra = require("./keyvaultInfrastructure");
const mysql_azure_1 = require("./mysql-azure");
const promiseGate_1 = require("./promiseGate");
const Resource = require("./resource");
const ServiceEnvironmentUtilities = require("./serviceEnvironmentUtilities");
class BaseDeployMySqlAzureInfrastructure {
constructor(baseMySqlAzureInfrastructure, createResult) {
this.baseMySqlAzureInfrastructure = baseMySqlAzureInfrastructure;
this.environmentVariablesValues = [];
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.
*/
getEnvironmentVariables() {
return this.environmentVariablesValues;
}
/**
* Creates a firewall on the storage resource with the given
* name for the given ipAddress.
*/
setFirewallRule(nameOfResourceSettingRule, ipAddress) {
return __awaiter(this, void 0, void 0, function* () {
yield this.baseMySqlAzureInfrastructure
.setFirewallRule(nameOfResourceSettingRule, ipAddress);
return this;
});
}
}
exports.BaseDeployMySqlAzureInfrastructure = BaseDeployMySqlAzureInfrastructure;
class MySqlAzureInfrastructure extends mysql_azure_1.default {
constructor() {
super(...arguments);
this.isStorageResource = true;
this.promiseGate = new promiseGate_1.default();
}
getBaseName() {
return this.baseName;
}
initialize(resource, targetDirectoryPath) {
super.initialize(resource, targetDirectoryPath);
if (resource !== null) {
Object.assign(this, resource);
}
return this;
}
setup() {
return __awaiter(this, void 0, void 0, function* () {
return yield MySqlAzureInfrastructure.internalSetup(__filename, this.targetDirectoryPath, data.data.MySQLNameLength);
});
}
hydrate(resourcesInEnvironment, deploymentType) {
const _super = name => super[name];
return __awaiter(this, void 0, void 0, function* () {
yield _super("hydrate").call(this, 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;
});
}
deployResource() {
return __awaiter(this, void 0, void 0, function* () {
try {
yield this.resourceGroup.getBaseDeployClassInstance();
const promisesToWaitFor = [];
const resourceGroupName = this.resourceGroup.resourceGroupName;
const createResult = yield 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);
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 = [];
for (const checkScriptPath of this.pathToMySqlInitializationScripts) {
const scriptPath = Path.isAbsolute(checkScriptPath) ? checkScriptPath :
Path.join(this.targetDirectoryPath, checkScriptPath);
if ((yield 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 = yield this.setUpFirewallForSqlScript();
for (const scriptPath of scriptPaths) {
promisesToWaitFor.push(this.runMySqlScript(scriptPath));
}
}
try {
yield Promise.all(promisesToWaitFor);
}
finally {
if (this.deploymentType === Resource.DeployType.Production
&& firewallRuleName) {
yield this.removeFirewallRule(firewallRuleName);
}
}
return this;
}
catch (err) {
if (!this.promiseGate.isGateOpen) {
this.promiseGate.openGateError(err);
}
throw err;
}
});
}
getBaseDeployClassInstance() {
return this.promiseGate.promise.then(function (baseClass) {
return baseClass;
});
}
setUpFirewallForSqlScript() {
return __awaiter(this, void 0, void 0, function* () {
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 = "";
const baseFirewallRuleName = Crypto.randomBytes(10).toString("hex");
const re = /Client with IP address '(.*)' is not allowed to/;
try {
yield 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 yield this.setFirewallRule(baseFirewallRuleName, devIp);
});
}
runMySqlScript(pathToScript) {
return __awaiter(this, void 0, void 0, function* () {
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 yield CommonUtilities.retryAfterFailure(() => __awaiter(this, void 0, void 0, function* () {
return yield CommonUtilities.exec(initSqlCommand, this.targetDirectoryPath);
}), 60);
});
}
setFirewallRule(nameOfResourceSettingRule, ipAddress) {
return __awaiter(this, void 0, void 0, function* () {
const ipNoDots = ipAddress.replace(/\./g, "");
const ruleName = `${nameOfResourceSettingRule}${ipNoDots}`;
yield 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;
});
}
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`);
}
removeFirewallRule(name) {
return CommonUtilities.runAzCommand(`az mysql server firewall-rule delete \
--resource-group ${this.resourceGroup.resourceGroupName} \
--server-name ${this.mySqlAzureFullName} \
--name ${name} --yes`, CommonUtilities.azCommandOutputs.string);
}
}
exports.MySqlAzureInfrastructure = MySqlAzureInfrastructure;
//# sourceMappingURL=mysql-azureInfrastructure.js.map