const k8s = require ( '@kubernetes/client-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' ; import DeployUI from './deployui' ; const MAX_RETRY = 36 ; const DEFAULT_TIMEOUT = 10000 ; export interface IK8sManager { createNamespace ( name ) : 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 ; private _deployUI : DeployUI ; constructor ( namespace , contextName , kubeConfigFilePath , 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 . getCluster ( contextName ) . 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 = { } ; this . _deployUI = DeployUI . instance ; } public createNamespace ( name ) { 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 ) => { clearInterval ( timer ) ; resolve ( result ) ; } ) . catch ( ( error ) => { 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' ) { err = JSON . stringify ( error , null , 2 ) ; } clearInterval ( timer ) ; reject ( err ) ; } } ) ; } , DEFAULT_TIMEOUT ) ; } ) ; } public deleteAll ( ) { return this . deleteSecrets ( ) . then ( ( ) => { return this . deleteConfigMap ( ) ; } ) . then ( ( ) => { return this . deleteDeployment ( ) ; } ) ; } public deleteSecrets ( ) { return this . _api . deleteNamespacedSecret ( this . _secret . metadata . name , this . _namespace , this . _secret ) ; } public deleteConfigMap ( ) { const configPath = __dirname + path . sep + 'solutions/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 ( ) { const promises = new Array < Promise < any > > ( ) ; const allInOnePath = __dirname + path . sep + 'solutions/remotemonitoring/scripts/all-in-one.yaml' ; const data = fs . readFileSync ( allInOnePath , 'UTF-8' ) ; const allInOne = jsyaml . safeLoadAll ( data , ( doc ) => { 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 ( ) { this . _deployUI . start ( 'Setting up Kubernetes: Uploading secrets' ) ; return this . setupSecrets ( ) . then ( ( ) => { this . _deployUI . start ( 'Setting up Kubernetes: Uploading config map' ) ; return this . setupConfigMap ( ) ; } ) . then ( ( ) => { this . _deployUI . start ( 'Setting up Kubernetes: Starting web app and microservices' ) ; return this . setupDeployment ( ) ; } ) ; } public setupSecrets ( ) { 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 ) => { clearInterval ( timer ) ; resolve ( result ) ; } ) . catch ( ( error ) => { if ( error . code === 'ETIMEDOUT' && this . _retryCount < MAX_RETRY ) { this . _retryCount ++ ; } else { let err = error ; if ( error . code !== 'ETIMEDOUT' ) { err = JSON . stringify ( error , null , 2 ) ; } clearInterval ( timer ) ; reject ( err ) ; } } ) ; } , DEFAULT_TIMEOUT ) ; } ) ; } public setupConfigMap ( ) { const configPath = __dirname + path . sep + 'solutions/remotemonitoring/scripts/individual/deployment-configmap.yaml' ; const configMap = jsyaml . safeLoad ( fs . readFileSync ( configPath , 'UTF-8' ) ) ; configMap . metadata . namespace = this . _namespace ; configMap . data [ 'keyvault.name' ] = this . _config . KeyVaultName ; configMap . data [ 'security.auth.serviceprincipal.secret' ] = this . _config . ServicePrincipalSecret ; configMap . data [ 'security.auth.audience' ] = this . _config . ApplicationId ; return this . _api . createNamespacedConfigMap ( this . _namespace , configMap ) ; } public setupDeployment ( ) { const promises = new Array < Promise < any > > ( ) ; const allInOnePath = __dirname + path . sep + 'solutions/remotemonitoring/scripts/all-in-one.yaml' ; const data = fs . readFileSync ( allInOnePath , 'UTF-8' ) ; const allInOne = jsyaml . safeLoadAll ( data , ( doc ) => { 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' : let imageName = doc . spec . template . spec . containers [ 0 ] . image ; if ( imageName . includes ( '{runtime}' ) ) { doc . spec . template . spec . containers [ 0 ] . image = imageName . replace ( '{runtime}' , this . _config . Runtime ) ; } imageName = doc . spec . template . spec . containers [ 0 ] . image ; if ( imageName . includes ( '{dockerTag}' ) ) { doc . spec . template . spec . containers [ 0 ] . image = imageName . replace ( '{dockerTag}' , this . _config . DockerTag ) ; } 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 ) ; } }