iotsuite-cli
Version:
Command Line Interface for deploying pre-configured IoT solutions through Azure
302 lines • 16.1 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const chalk = require("chalk");
const fs = require("fs");
const os = require("os");
const path = require("path");
const azure_arm_resource_1 = require("azure-arm-resource");
const ms_rest_azure_1 = require("ms-rest-azure");
const deployui_1 = require("./deployui");
const ssh2_1 = require("ssh2");
const k8smanager_1 = require("./k8smanager");
const config_1 = require("./config");
const MAX_RETRY = 36;
const KUBEDIR = os.homedir() + path.sep + '.kube';
// We are using BingMap APIs with plan = internal1
// It only allows to have 2 apis per subscription
const MAX_BING_MAP_APIS_FOR_INTERNAL1_PLAN = 2;
class DeploymentManager {
constructor(options, subscriptionId, solutionType, sku) {
this._options = options;
this._solutionType = solutionType;
this._sku = sku;
this._subscriptionId = subscriptionId;
const baseUri = this._options.environment ? this._options.environment.resourceManagerEndpointUrl : undefined;
this._client = new azure_arm_resource_1.ResourceManagementClient(new ms_rest_azure_1.DeviceTokenCredentials(this._options), subscriptionId, baseUri);
}
getLocations() {
// Currently IotHub is not supported in all the regions so using it to get the available locations
return this._client.providers.get('Microsoft.Devices')
.then((providers) => {
if (providers.resourceTypes) {
const resourceType = providers.resourceTypes.filter((x) => x.resourceType && x.resourceType.toLowerCase() === 'iothubs');
if (resourceType && resourceType.length) {
return resourceType[0].locations;
}
}
});
}
submit(answers) {
if (!!!answers || !!!answers.solutionName || !!!answers.subscriptionId || !!!answers.location) {
return Promise.reject('Solution name, subscription id and location cannot be empty');
}
const location = answers.location;
const deployment = { properties: {
mode: 'Incremental',
} };
const deployUI = deployui_1.default.instance;
const deploymentName = 'deployment-' + answers.solutionName;
let deploymentProperties = null;
let resourceGroupUrl;
let freeBingMapResourceCount = 0;
let resourceGroup = {
location,
// TODO: Explore if it makes sense to add more tags, e.g. Language(Java/.Net), version etc
tags: { IotSolutionType: this._solutionType },
};
const parametersFileName = this._sku + '-parameters.json';
return this._client.resources.list({ filter: 'resourceType eq \'Microsoft.BingMaps/mapApis\'' })
.then((resources) => {
// using static map for China environment by default since Bing Map resource is not available.
if (this._options.environment && this._options.environment.name === ms_rest_azure_1.AzureEnvironment.AzureChina.name) {
this._sku += '-static-map';
}
else {
resources.forEach((resource) => {
if (resource.plan && resource.plan.name && resource.plan.name.toLowerCase() === 'internal1') {
freeBingMapResourceCount++;
}
});
if (freeBingMapResourceCount >= MAX_BING_MAP_APIS_FOR_INTERNAL1_PLAN) {
this._sku += '-static-map';
}
}
const solutionFileName = this._sku + '.json';
try {
const armTemplatePath = __dirname + path.sep + this._solutionType + path.sep + 'armtemplates' + path.sep;
this._template = require(armTemplatePath + solutionFileName);
this._parameters = require(armTemplatePath + parametersFileName);
// Change the default suffix for basic sku based on current environment
if (this._options.environment && answers.deploymentSku === 'basic') {
switch (this._options.environment.name) {
case ms_rest_azure_1.AzureEnvironment.AzureChina.name:
this._parameters.storageEndpointSuffix = { value: 'core.chinacloudapi.cn' };
this._parameters.vmFQDNSuffix = { value: 'cloudapp.chinacloudapi.cn' };
break;
case ms_rest_azure_1.AzureEnvironment.AzureGermanCloud.name:
this._parameters.storageEndpointSuffix = { value: 'core.cloudapi.de' };
this._parameters.vmFQDNSuffix = { value: 'cloudapp.azure.de' };
break;
case ms_rest_azure_1.AzureEnvironment.AzureUSGovernment.name:
this._parameters.storageEndpointSuffix = { value: 'core.cloudapi.us' };
this._parameters.vmFQDNSuffix = { value: 'cloudapp.azure.us' };
break;
// use default parameter values of global azure environment
default:
this._parameters.storageEndpointSuffix = { value: 'core.windows.net' };
this._parameters.vmFQDNSuffix = { value: 'cloudapp.azure.com' };
}
}
this.setupParameters(answers);
}
catch (ex) {
throw new Error('Could not find template or parameters file, Exception:');
}
deployment.properties.parameters = this._parameters;
deployment.properties.template = this._template;
return deployment;
})
.then((properties) => {
return this._client.resourceGroups.createOrUpdate(answers.solutionName, resourceGroup);
})
.then((result) => {
resourceGroup = result;
resourceGroupUrl = 'https://portal.azure.com/#resource' + resourceGroup.id;
return this._client.deployments.validate(answers.solutionName, deploymentName, deployment);
})
.then((validationResult) => {
if (validationResult.error) {
deployUI.stop('Deployment validation failed:\n' + JSON.stringify(validationResult.error, null, 2));
throw new Error(JSON.stringify(validationResult.error));
}
deployUI.start(this._client, answers.solutionName, deploymentName, deployment.properties.template.resources.length);
return this._client.deployments.createOrUpdate(answers.solutionName, deploymentName, deployment);
})
.then((res) => {
deployUI.stop();
deploymentProperties = res.properties;
const directoryPath = process.cwd() + path.sep + 'deployments';
if (!fs.existsSync(directoryPath)) {
fs.mkdirSync(directoryPath);
}
const fileName = directoryPath + path.sep + deploymentName + '-output.json';
fs.writeFileSync(fileName, JSON.stringify(deploymentProperties.outputs, null, 2));
if (deploymentProperties.outputs.azureWebsite) {
const webUrl = deploymentProperties.outputs.azureWebsite.value;
if (answers.deploymentSku === 'standard') {
console.log('The app will be available on following url after kubernetes setup is done:');
}
console.log('Please click %s %s %s', `${chalk.cyan(webUrl)}`, 'to deployed solution:', `${chalk.green(answers.solutionName)}`);
}
console.log('Please click %s %s', `${chalk.cyan(resourceGroupUrl)}`, 'to manage your deployed resources');
console.log('Output saved to file: %s', `${chalk.cyan(fileName)}`);
if (answers.deploymentSku === 'standard') {
console.log('Downloading the kubeconfig file from:', `${chalk.cyan(deploymentProperties.outputs.masterFQDN.value)}`);
return this.downloadKubeConfig(deploymentProperties.outputs, answers.sshFilePath);
}
return Promise.resolve('');
})
.then((kubeConfigPath) => {
if (answers.deploymentSku === 'standard') {
const outputs = deploymentProperties.outputs;
const config = new config_1.Config();
config.AADTenantId = answers.aadTenantId;
config.ApplicationId = answers.appId;
config.AzureStorageAccountKey = outputs.storageAccountKey.value;
config.AzureStorageAccountName = outputs.storageAccountName.value;
// If we are under the plan limi then we should have received a query key
if (freeBingMapResourceCount < MAX_BING_MAP_APIS_FOR_INTERNAL1_PLAN) {
config.BingMapApiQueryKey = outputs.mapApiQueryKey.value;
}
config.DNS = outputs.agentFQDN.value;
config.DocumentDBConnectionString = outputs.documentDBConnectionString.value;
config.EventHubEndpoint = outputs.eventHubEndpoint.value;
config.EventHubName = outputs.eventHubName.value;
config.EventHubPartitions = outputs.eventHubPartitions.value.toString();
config.IoTHubConnectionString = outputs.iotHubConnectionString.value;
config.LoadBalancerIP = outputs.loadBalancerIp.value;
config.Runtime = answers.runtime;
config.TLS = answers.certData;
const k8sMananger = new k8smanager_1.K8sManager('default', kubeConfigPath, config);
console.log(`${chalk.cyan('Setting up kubernetes')}`);
return k8sMananger.setupAll()
.catch((err) => {
console.log(err);
});
}
return Promise.resolve();
})
.then(() => {
console.log('Setup done sucessfully, the website will be ready in 2-5 minutes');
})
.catch((err) => {
let errorMessage = err.toString();
if (err.toString().includes('Entry not found in cache.')) {
errorMessage = 'Session expired, Please run pcs login again. \n\
Resources are being deployed at ' + resourceGroupUrl;
}
deployUI.stop(errorMessage);
});
}
downloadKubeConfig(outputs, sshFilePath) {
if (!fs.existsSync) {
fs.mkdirSync(KUBEDIR);
}
const localKubeConfigPath = KUBEDIR + path.sep + 'config' + '-' + outputs.containerServiceName.value;
const remoteKubeConfig = '.kube/config';
const sshDir = sshFilePath.substring(0, sshFilePath.lastIndexOf(path.sep));
const sshPrivateKeyPath = sshDir + path.sep + 'id_rsa';
const pk = fs.readFileSync(sshPrivateKeyPath, 'UTF-8');
const sshClient = new ssh2_1.Client();
const config = {
host: outputs.masterFQDN.value,
port: 22,
privateKey: pk,
username: outputs.adminUsername.value
};
return new Promise((resolve, reject) => {
let retryCount = 0;
const timer = setInterval(() => {
// First remove all listeteners so that we don't have duplicates
sshClient.removeAllListeners();
sshClient
.on('ready', (message) => {
sshClient.sftp((error, sftp) => {
if (error) {
sshClient.end();
reject(error);
clearInterval(timer);
return;
}
sftp.fastGet(remoteKubeConfig, localKubeConfigPath, (err) => {
sshClient.end();
if (err) {
reject(err);
return;
}
console.log('kubectl config file downloaded to: %s', `${chalk.cyan(localKubeConfigPath)}`);
clearInterval(timer);
resolve(localKubeConfigPath);
});
});
})
.on('error', (err) => {
if (retryCount++ > MAX_RETRY) {
clearInterval(timer);
reject(err);
}
else {
console.log(`${chalk.yellow('Retrying connection to: ' +
outputs.masterFQDN.value + ' ' + retryCount + ' of ' + MAX_RETRY)}`);
}
})
.on('timeout', () => {
if (retryCount++ > MAX_RETRY) {
clearInterval(timer);
reject(new Error('Failed after maximum number of tries'));
}
else {
console.log(`${chalk.yellow('Retrying connection to: ' +
outputs.masterFQDN.value + ' ' + retryCount + ' of ' + MAX_RETRY)}`);
}
})
.connect(config);
}, 5000);
});
}
setupParameters(answers) {
this._parameters.solutionName.value = answers.solutionName;
// Temporary check, in future both types of deployment will always have username and passord
// If the parameters file has adminUsername section then add the value that was passed in by user
if (this._parameters.adminUsername) {
this._parameters.adminUsername.value = answers.adminUsername;
}
// If the parameters file has adminPassword section then add the value that was passed in by user
if (this._parameters.adminPassword) {
this._parameters.adminPassword.value = answers.adminPassword;
}
if (this._parameters.servicePrincipalSecret) {
this._parameters.servicePrincipalSecret.value = answers.servicePrincipalSecret;
}
if (this._parameters.servicePrincipalClientId) {
this._parameters.servicePrincipalClientId.value = answers.appId;
}
if (this._parameters.sshRSAPublicKey) {
this._parameters.sshRSAPublicKey.value = fs.readFileSync(answers.sshFilePath, 'UTF-8');
}
if (this._parameters.azureWebsiteName) {
this._parameters.azureWebsiteName.value = answers.azureWebsiteName;
}
if (this._parameters.remoteEndpointSSLThumbprint) {
this._parameters.remoteEndpointSSLThumbprint.value = answers.certData.fingerPrint;
}
if (this._parameters.remoteEndpointCertificate) {
this._parameters.remoteEndpointCertificate.value = answers.certData.cert;
}
if (this._parameters.remoteEndpointCertificateKey) {
this._parameters.remoteEndpointCertificateKey.value = answers.certData.key;
}
if (this._parameters.aadTenantId) {
this._parameters.aadTenantId.value = answers.aadTenantId;
}
if (this._parameters.aadClientId) {
this._parameters.aadClientId.value = answers.appId;
}
if (this._parameters.microServiceRuntime) {
this._parameters.microServiceRuntime.value = answers.runtime;
}
}
}
exports.DeploymentManager = DeploymentManager;
exports.default = DeploymentManager;
//# sourceMappingURL=deploymentmanager.js.map