UNPKG

iotsuite-cli

Version:

Command Line Interface for deploying pre-configured IoT solutions through Azure

302 lines 16.1 kB
"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