UNPKG

iotsuite-cli

Version:

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

237 lines (220 loc) 11 kB
const k8s = require('@kubernetes/typescript-node'); const btoa = require('btoa'); import * as chalk from 'chalk'; import * as fs from 'fs'; import * as path from 'path'; import * as jsyaml from 'js-yaml'; import { Config } from './config'; const MAX_RETRY: number = 36; const DEFAULT_TIMEOUT = 10000; export interface IK8sManager { createNamespace(name: string): Promise<any>; deleteAll(): Promise<any>; deleteConfigMap(): Promise<any>; deleteSecrets(): Promise<any>; deleteDeployment(): Promise<any>; setupAll(): Promise<any>; setupConfigMap(): Promise<any>; setupSecrets(): Promise<any>; setupDeployment(): Promise<any>; } export class K8sManager implements IK8sManager { private _configFilePath: string; private _api: any; private _betaApi: any; private _retryCount: number = 0; private _namespace: string; private _config: Config; private _secret: any; constructor(namespace: string, kubeConfigFilePath: string, config: Config) { this._namespace = namespace; this._configFilePath = kubeConfigFilePath; this._config = config; this._api = k8s.Config.fromFile(this._configFilePath); const kc = new k8s.KubeConfig(); kc.loadFromFile(kubeConfigFilePath); this._betaApi = new k8s.Extensions_v1beta1Api(kc.getCurrentCluster().server); this._betaApi.authentications.default = kc; this._secret = new k8s.V1Secret(); this._secret.apiVersion = 'v1'; this._secret.metadata = new k8s.V1ObjectMeta(); this._secret.metadata.name = 'tls-certificate'; this._secret.metadata.namespace = this._namespace; this._secret.kind = 'Secret'; this._secret.type = 'Opaque'; this._secret.data = {}; } public createNamespace(name: string): Promise<any> { const ns = new k8s.V1Namespace(); ns.apiVersion = 'v1'; ns.kind = 'Namespace'; ns.metadata = {}; ns.metadata.name = this._namespace; return new Promise((resolve, reject) => { const timer = setInterval( () => { return this._api.createNamespace(ns) .then((result: any) => { clearInterval(timer); resolve(result); }) .catch((error: any) => { if (error.code === 'ETIMEDOUT' && this._retryCount < MAX_RETRY) { this._retryCount++; console.log(`${chalk.yellow('Create namespace: retrying', this._retryCount.toString(), 'of', MAX_RETRY.toString())}`); } else { let err = error; if (error.code !== 'ETIMEDOUT') { // Convert a response to properl format in case of json err = JSON.stringify(error, null, 2); } clearInterval(timer); reject(err); } }); }, DEFAULT_TIMEOUT ); }); } public deleteAll(): Promise<any> { return this.deleteSecrets() .then(() => { return this.deleteConfigMap(); }) .then(() => { return this.deleteDeployment(); }); } public deleteSecrets(): Promise<any> { return this._api.deleteNamespacedSecret(this._secret.metadata.name, this._namespace, this._secret); } public deleteConfigMap(): Promise<any> { const configPath = __dirname + path.sep + 'remotemonitoring/scripts/individual/deployment-configmap.yaml'; const configMap = jsyaml.safeLoad(fs.readFileSync(configPath, 'UTF-8')); configMap.metadata.namespace = this._namespace; return this._api.deleteNamespacedConfigMap(configMap.metadata.name, this._namespace, configMap); } public deleteDeployment(): Promise<any> { const promises = new Array<Promise<any>>(); const allInOnePath = process.cwd() + path.sep + 'remotemonitoring/scripts/all-in-one.yaml'; const data = fs.readFileSync(allInOnePath, 'UTF-8'); const allInOne = jsyaml.safeLoadAll(data, (doc: any) => { doc.metadata.namespace = this._namespace; switch (doc.kind) { case 'Service': promises.push(this._api.deleteNamespacedService(doc.metadata.name, this._namespace, doc)); break; case 'ReplicationController': promises.push(this._api.deleteNamespacedReplicationController(doc.metadata.name, this._namespace, doc)); break; case 'Deployment': promises.push(this._betaApi.deleteNamespacedDeployment(doc.metadata.name, this._namespace, doc)); break; case 'Ingress': doc.spec.rules[0].host = this._config.DNS; doc.spec.tls[0].hosts[0] = this._config.DNS; promises.push(this._betaApi.deleteNamespacedIngress(doc.metadata.name, this._namespace, doc)); break; default: console.log('Unexpected kind found in yaml file'); } }); return Promise.all(promises); } public setupAll(): Promise<any> { return this.setupSecrets() .then(() => { return this.setupConfigMap(); }) .then(() => { return this.setupDeployment(); }); } public setupSecrets(): Promise<any> { this._secret.data['tls.crt'] = btoa(this._config.TLS.cert); this._secret.data['tls.key'] = btoa(this._config.TLS.key); return new Promise((resolve, reject) => { const timer = setInterval( () => { return this._api.createNamespacedSecret(this._namespace, this._secret) .then((result: any) => { clearInterval(timer); resolve(result); }) .catch((error: any) => { if (error.code === 'ETIMEDOUT' && this._retryCount < MAX_RETRY) { this._retryCount++; console.log(`${chalk.yellow('Setting up secrets: retrying', this._retryCount.toString(), 'of', MAX_RETRY.toString())}`); } else { let err = error; if (error.code !== 'ETIMEDOUT') { // Convert a response to properl format in case of json err = JSON.stringify(error, null, 2); } clearInterval(timer); reject(err); } }); }, DEFAULT_TIMEOUT ); }); } public setupConfigMap(): Promise<any> { const configPath = process.cwd() + path.sep + 'remotemonitoring/scripts/individual/deployment-configmap.yaml'; const configMap = jsyaml.safeLoad(fs.readFileSync(configPath, 'UTF-8')); configMap.metadata.namespace = this._namespace; configMap.data['auth.aad.global.clientid'] = this._config.ApplicationId; configMap.data['auth.aad.global.tenantid'] = this._config.AADTenantId; configMap.data['auth.aad.global.issuer'] = 'https://sts.windows.net/' + this._config.AADTenantId + '/'; configMap.data['bing.map.key'] = this._config.BingMapApiQueryKey ? this._config.BingMapApiQueryKey : ''; configMap.data['iothub.connstring'] = this._config.IoTHubConnectionString; configMap.data['docdb.connstring'] = this._config.DocumentDBConnectionString; configMap.data['iothubreact.hub.name'] = this._config.EventHubName; configMap.data['iothubreact.hub.endpoint'] = this._config.EventHubEndpoint; configMap.data['iothubreact.hub.partitions'] = this._config.EventHubPartitions; configMap.data['iothubreact.access.connstring'] = this._config.IoTHubConnectionString; configMap.data['iothubreact.azureblob.account'] = this._config.AzureStorageAccountName; configMap.data['iothubreact.azureblob.key'] = this._config.AzureStorageAccountKey; let deploymentConfig = configMap.data['webui-config.js']; deploymentConfig = deploymentConfig.replace('{TenantId}', this._config.AADTenantId); deploymentConfig = deploymentConfig.replace('{ApplicationId}', this._config.ApplicationId); configMap.data['webui-config.js'] = deploymentConfig; return this._api.createNamespacedConfigMap(this._namespace, configMap); } public setupDeployment(): Promise<any> { const promises = new Array<Promise<any>>(); const allInOnePath = process.cwd() + path.sep + 'remotemonitoring/scripts/all-in-one.yaml'; const data = fs.readFileSync(allInOnePath, 'UTF-8'); const allInOne = jsyaml.safeLoadAll(data, (doc: any) => { doc.metadata.namespace = this._namespace; switch (doc.kind) { case 'Service': if (doc.spec.type === 'LoadBalancer') { doc.spec.loadBalancerIP = this._config.LoadBalancerIP; } promises.push(this._api.createNamespacedService(this._namespace, doc)); break; case 'ReplicationController': promises.push(this._api.createNamespacedReplicationController(this._namespace, doc)); break; case 'Deployment': const imageName: string = doc.spec.template.spec.containers[0].image; if (imageName.includes('{runtime}')) { doc.spec.template.spec.containers[0].image = imageName.replace('{runtime}', this._config.Runtime); } promises.push(this._betaApi.createNamespacedDeployment(this._namespace, doc)); break; case 'Ingress': doc.spec.rules[0].host = this._config.DNS; doc.spec.tls[0].hosts[0] = this._config.DNS; promises.push(this._betaApi.createNamespacedIngress(this._namespace, doc)); break; default: console.log('Unexpected kind found in yaml file'); } }); return Promise.all(promises); } }