UNPKG

@vroomlabs/gsdk-deploy

Version:

Google Cloud deployment script for kubernetes clusters using Global Load Balancer

357 lines (284 loc) 19.9 kB
'use strict'; /****************************************************************************** * MIT License * Copyright (c) 2017 https://github.com/vroomlabs * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. * * Created by rogerk on 7/3/17. ******************************************************************************/Object.defineProperty(exports,'__esModule',{value:true});exports.KubernetesControl=undefined;var _typeof=typeof Symbol==='function'&&typeof Symbol.iterator==='symbol'?function(obj){return typeof obj}:function(obj){return obj&&typeof Symbol==='function'&&obj.constructor===Symbol&&obj!==Symbol.prototype?'symbol':typeof obj};var _createClass=function(){function defineProperties(target,props){for(var i=0;i<props.length;i++){var descriptor=props[i];descriptor.enumerable=descriptor.enumerable||false;descriptor.configurable=true;if('value'in descriptor)descriptor.writable=true;Object.defineProperty(target,descriptor.key,descriptor)}}return function(Constructor,protoProps,staticProps){if(protoProps)defineProperties(Constructor.prototype,protoProps);if(staticProps)defineProperties(Constructor,staticProps);return Constructor}}(); var _fs=require('fs');var fs=_interopRequireWildcard(_fs); var _os=require('os');var os=_interopRequireWildcard(_os); var _path=require('path');var path=_interopRequireWildcard(_path); var _logger=require('../util/logger'); var _shell=require('../util/shell');var shell=_interopRequireWildcard(_shell); var _templateArg=require('../util/templateArg');function _interopRequireWildcard(obj){if(obj&&obj.__esModule){return obj}else{var newObj={};if(obj!=null){for(var key in obj){if(Object.prototype.hasOwnProperty.call(obj,key))newObj[key]=obj[key]}}newObj.default=obj;return newObj}}function _asyncToGenerator(fn){return function(){var gen=fn.apply(this,arguments);return new Promise(function(resolve,reject){function step(key,arg){try{var info=gen[key](arg);var value=info.value}catch(error){reject(error);return}if(info.done){resolve(value)}else{return Promise.resolve(value).then(function(value){step('next',value)},function(err){step('throw',err)})}}return step('next')})}}function _classCallCheck(instance,Constructor){if(!(instance instanceof Constructor)){throw new TypeError('Cannot call a class as a function')}}var KubernetesControl=exports.KubernetesControl=function(){ /** * @param {Configuration} config * @param {{name: string, zone: string}} cluster */ function KubernetesControl(config,cluster){_classCallCheck(this,KubernetesControl); this.config=config; this.cluster=cluster; this.kubectl='kubectl'; if(KubernetesControl.isCircleCI()){ this.kubectl='sudo /opt/google-cloud-sdk/bin/kubectl'; } if(process.env.KUBECTL_COMMAND){ this.kubectl=process.env.KUBECTL_COMMAND; } } /** * Reports the state of the named deployment to the console * @param {string} name */_createClass(KubernetesControl,[{key:'reportStatus',value:function(){var _ref=_asyncToGenerator(/*#__PURE__*/regeneratorRuntime.mark(function _callee( name){var depState,podState,messages,ix,podMsgs;return regeneratorRuntime.wrap(function _callee$(_context){while(1){switch(_context.prev=_context.next){case 0:_context.t0= JSON;_context.next=3;return shell.exec(this.kubectl+' get deploy/'+name+' -o=json');case 3:_context.t1=_context.sent;depState=_context.t0.parse.call(_context.t0,_context.t1); depState=depState||{}; depState.status=depState.status||{}; _logger.logger.info(this.cluster.name+' has '+(depState.status.replicas||0)+' replicas, '+(depState.status.unavailableReplicas||0)+' unavailable.');_context.t2= JSON;_context.next=11;return shell.exec(this.kubectl+' get pod -o=json');case 11:_context.t3=_context.sent;podState=_context.t2.parse.call(_context.t2,_context.t3); podState=podState||{}; podState.items=podState.items||[]; podState=podState.items.filter(function(pod){return( pod.metadata.labels.app===name|| pod.metadata.name.substr(name.length)===name)}); messages=[]; for(ix=0;ix<podState.length;ix++){ _logger.logger.info(podState[ix].metadata.name+' phase = '+podState[ix].status.phase); podMsgs=KubernetesControl.allmessages(podState[ix].status,[]); podMsgs.forEach(function(msg){return _logger.logger.warn(msg)}); messages=messages.concat(podMsgs); }return _context.abrupt('return', messages);case 19:case'end':return _context.stop();}}},_callee,this)}));function reportStatus(_x){return _ref.apply(this,arguments)}return reportStatus}() /** * returns the full configuration of a deployment */},{key:'getDeploy',value:function(){var _ref2=_asyncToGenerator(/*#__PURE__*/regeneratorRuntime.mark(function _callee2( name){var json;return regeneratorRuntime.wrap(function _callee2$(_context2){while(1){switch(_context2.prev=_context2.next){case 0:_context2.next=2;return( shell.exec(this.kubectl+' get deploy '+name+' -o=json'));case 2:json=_context2.sent;return _context2.abrupt('return', JSON.parse(json));case 4:case'end':return _context2.stop();}}},_callee2,this)}));function getDeploy(_x2){return _ref2.apply(this,arguments)}return getDeploy}() /** * @param {{name, image, endpointName, endpointVersion, protocol, proxyImage}} service */},{key:'deployImage',value:function(){var _ref3=_asyncToGenerator(/*#__PURE__*/regeneratorRuntime.mark(function _callee3( service){var tempPath,svcConfig,svcTemplate,depConfig,deployTemplate;return regeneratorRuntime.wrap(function _callee3$(_context3){while(1){switch(_context3.prev=_context3.next){case 0: _logger.logger.verbose('Preparing to deploy '+service.name+' on '+this.cluster.name+'.',{image:service,apiVer:service.endpointVersion}); tempPath=path.dirname(this.config.current.path); svcConfig=path.join(tempPath,service.name+'-svc.json'); svcTemplate=this.getServiceTemplate(service.name); fs.writeFileSync(svcConfig,JSON.stringify(svcTemplate,null,2)); depConfig=path.join(tempPath,service.name+'-deploy.json'); deployTemplate=this.getDeployTemplate(service.name,service.image,service.endpointName,service.endpointVersion,service.protocol,service.proxyImage); fs.writeFileSync(depConfig,JSON.stringify(deployTemplate,null,2)); // Install the ssl certificate if missing _context3.next=10;return this.addSslSecret('nginx');case 10: // Deploy service _logger.logger.info('Deploying service '+service.name+' to '+this.cluster.name+'...');_context3.next=13;return( shell.exec(this.kubectl+' apply -f '+svcConfig,{direct:true}));case 13: // Deploy container _logger.logger.info('Deploying container '+service.name+' to '+this.cluster.name+'...');_context3.next=16;return( shell.exec(this.kubectl+' apply -f '+depConfig,{direct:true}));case 16:_context3.prev=16;_context3.next=19;return( this.waitForRollout(service.name,this.config.current.waitTime));case 19:_context3.next=29;break;case 21:_context3.prev=21;_context3.t0=_context3['catch'](16); _logger.logger.error('Rolling back due to: '+_context3.t0.message);_context3.next=26;return( this.reportStatus(service.name));case 26:_context3.next=28;return( this.rollbackOnce(service.name));case 28:throw _context3.t0;case 29:case'end':return _context3.stop();}}},_callee3,this,[[16,21]])}));function deployImage(_x3){return _ref3.apply(this,arguments)}return deployImage}() /** * Updates only the running image on a cluster * @param {string} name * @param {string} image * @param {number} timeout */},{key:'updateImage',value:function(){var _ref4=_asyncToGenerator(/*#__PURE__*/regeneratorRuntime.mark(function _callee4( name,image,timeout){return regeneratorRuntime.wrap(function _callee4$(_context4){while(1){switch(_context4.prev=_context4.next){case 0:_context4.next=2;return( shell.exec(this.kubectl+' set image deploy/'+name+' '+name+'='+image));case 2:_context4.prev=2;_context4.next=5;return( this.waitForRollout(name,timeout));case 5:_context4.next=15;break;case 7:_context4.prev=7;_context4.t0=_context4['catch'](2); _logger.logger.error('Rolling back due to: '+_context4.t0.message);_context4.next=12;return( this.reportStatus(name));case 12:_context4.next=14;return( this.rollbackOnce(name));case 14:throw _context4.t0;case 15:case'end':return _context4.stop();}}},_callee4,this,[[2,7]])}));function updateImage(_x4,_x5,_x6){return _ref4.apply(this,arguments)}return updateImage}() /** * @param {string} name * @param {number=} timeout */},{key:'rollbackDeploy',value:function(){var _ref5=_asyncToGenerator(/*#__PURE__*/regeneratorRuntime.mark(function _callee5( name,timeout){return regeneratorRuntime.wrap(function _callee5$(_context5){while(1){switch(_context5.prev=_context5.next){case 0: _logger.logger.warn('Rolling back '+name+' on '+this.cluster.name);_context5.next=3;return( shell.exec(this.kubectl+' rollout undo deploy/'+name,{direct:true}));case 3:return _context5.abrupt('return', this.waitForRollout(name,timeout));case 4:case'end':return _context5.stop();}}},_callee5,this)}));function rollbackDeploy(_x7,_x8){return _ref5.apply(this,arguments)}return rollbackDeploy}() /** * @param {string} name */},{key:'removeService',value:function(){var _ref6=_asyncToGenerator(/*#__PURE__*/regeneratorRuntime.mark(function _callee6( name){return regeneratorRuntime.wrap(function _callee6$(_context6){while(1){switch(_context6.prev=_context6.next){case 0: _logger.logger.warn('Removing service '+name+' on '+this.cluster.name);_context6.next=3;return( shell.exec(this.kubectl+' delete service '+name,{direct:true}));case 3:case'end':return _context6.stop();}}},_callee6,this)}));function removeService(_x9){return _ref6.apply(this,arguments)}return removeService}() /** * @param {string} name */},{key:'removeDeployment',value:function(){var _ref7=_asyncToGenerator(/*#__PURE__*/regeneratorRuntime.mark(function _callee7( name){return regeneratorRuntime.wrap(function _callee7$(_context7){while(1){switch(_context7.prev=_context7.next){case 0: _logger.logger.warn('Removing deployment '+name+' on '+this.cluster.name);_context7.next=3;return( shell.exec(this.kubectl+' delete deploy '+name,{direct:true}));case 3:case'end':return _context7.stop();}}},_callee7,this)}));function removeDeployment(_x10){return _ref7.apply(this,arguments)}return removeDeployment}() /** * @param {string} name * @param {number} timeout */},{key:'waitForRollout',value:function(){var _ref8=_asyncToGenerator(/*#__PURE__*/regeneratorRuntime.mark(function _callee9( name,timeout){var _this=this;var self,startTime;return regeneratorRuntime.wrap(function _callee9$(_context9){while(1){switch(_context9.prev=_context9.next){case 0: self=this;if( timeout>0){_context9.next=4;break} _logger.logger.debug('Skipping rollout verify, waitTime <= 0');return _context9.abrupt('return');case 4: startTime=Date.now(); _logger.logger.verbose('Waiting on rollout for '+name+' on '+this.cluster.name);return _context9.abrupt('return', new Promise(function(){var _ref9=_asyncToGenerator(/*#__PURE__*/regeneratorRuntime.mark(function _callee8(accept,reject){var cancelled,timerId,successTarget,successCount,faultCount,checkPodStatus;return regeneratorRuntime.wrap(function _callee8$(_context8){while(1){switch(_context8.prev=_context8.next){case 0: cancelled=false; timerId=setTimeout(function(){ cancelled=true; var timeTaken=parseInt((Date.now()-startTime)/1000); reject(new Error('Timeout exceeded ('+timeTaken+' seconds).')); },timeout);_context8.next=4;return( shell.exec(_this.kubectl+' rollout status deploy/'+name,{direct:true}));case 4: /* * Race condition: all pods may be alive, but not health-checked and kube reports no errors. * To reduce false-positives, it requires 3 consecutive success results. */ successTarget=3; successCount=0; faultCount=0; _logger.logger.debug('Rollout completed, checking pods...'); checkPodStatus=function checkPodStatus(){ if(cancelled)return; self.getPodMessages(name). then(function(pods){ if(cancelled)return; if(pods.length===0){ if(++successCount>=successTarget){ _logger.logger.silly('Target success count reached: '+successCount); var timeTaken=parseInt((Date.now()-startTime)/1000); _logger.logger.debug('Rollout successful after '+timeTaken+' seconds.'); clearTimeout(timerId); return accept(); } }else { successCount=0; } _logger.logger.silly('waiting on pods',{pods:pods}); if(pods.length>0&&++faultCount%3===0) _logger.logger.debug('Waiting on '+pods[0].name+': '+pods[0].msgs[0]); setTimeout(checkPodStatus,10000); }). catch(function(ex){return reject(ex)}); }; checkPodStatus();case 10:case'end':return _context8.stop();}}},_callee8,_this)}));return function(_x13,_x14){return _ref9.apply(this,arguments)}}()));case 7:case'end':return _context9.stop();}}},_callee9,this)}));function waitForRollout(_x11,_x12){return _ref8.apply(this,arguments)}return waitForRollout}() /** * @param {string} name */},{key:'getPodMessages',value:function(){var _ref10=_asyncToGenerator(/*#__PURE__*/regeneratorRuntime.mark(function _callee10( name){var podState;return regeneratorRuntime.wrap(function _callee10$(_context10){while(1){switch(_context10.prev=_context10.next){case 0:_context10.t0= JSON;_context10.next=3;return shell.exec(this.kubectl+' get pod -o=json');case 3:_context10.t1=_context10.sent;podState=_context10.t0.parse.call(_context10.t0,_context10.t1); podState=podState||{}; podState=podState.items||[]; podState=podState.filter(function(pod){return pod.metadata.labels.app===name}); podState=podState.map(function(pod){ return{ name:pod.metadata.name, msgs:KubernetesControl.allmessages(pod.status,[])}; }); podState=podState.filter(function(pod){return pod.msgs.length});return _context10.abrupt('return', podState);case 11:case'end':return _context10.stop();}}},_callee10,this)}));function getPodMessages(_x15){return _ref10.apply(this,arguments)}return getPodMessages}() /** * @param {string} name */},{key:'rollbackOnce',value:function(){var _ref11=_asyncToGenerator(/*#__PURE__*/regeneratorRuntime.mark(function _callee11( name){return regeneratorRuntime.wrap(function _callee11$(_context11){while(1){switch(_context11.prev=_context11.next){case 0: _logger.logger.warn('Rolling back '+name+' on '+this.cluster.name);return _context11.abrupt('return', shell.exec(this.kubectl+' rollout undo deploy/'+name,{direct:true}));case 2:case'end':return _context11.stop();}}},_callee11,this)}));function rollbackOnce(_x16){return _ref11.apply(this,arguments)}return rollbackOnce}() /** * Returns the service config */},{key:'getServiceTemplate',value:function getServiceTemplate( name){ var text=fs.readFileSync(this.config.current.serviceTemplate).toString(); var values={ SERVICE_NAME:name, SSL_PORT:this.config.current.sslPort, NODE_PORT:this.config.getNodePort()}; text=(0,_templateArg.replaceInText)(text,values); return JSON.parse(text); } /** * Returns the deploy config */},{key:'getDeployTemplate',value:function getDeployTemplate( name,image,epName,epVersion,protocol,epImage){ process.env.ENDPOINT_NAME=epName; process.env.ENDPOINT_VERSION=epVersion; var text=fs.readFileSync(this.config.current.deployTemplate).toString(); var values={ SERVICE_NAME:name, REPLICAS:this.config.current.replicas, DOCKER_IMAGE:image, APP_PROTOCOL:protocol, APP_PORT:this.config.current.port, SSL_PORT:this.config.current.sslPort, ENDPOINT_NAME:epName, ENDPOINT_VERSION:epVersion, PROXY_IMAGE:epImage, LIVENESS_PROBE:this.config.current.livenessProbe, READINESS_PROBE:this.config.current.readinessProbe, PRINT_PRIMITIVE_FIELDS:this.config.current.printPrimitiveFields? '"--transcoding_always_print_primitive_fields",': ''}; text=(0,_templateArg.replaceInText)(text,values); var template=JSON.parse(text); template.spec.template.spec.containers[0].env=this.config.getEnvironment(); return template; } /** * Generates an ssl key-pair and installs to the specified secret name * @param {string} name */},{key:'addSslSecret',value:function(){var _ref12=_asyncToGenerator(/*#__PURE__*/regeneratorRuntime.mark(function _callee12( name){var tmpdir,keyfile,crtfile;return regeneratorRuntime.wrap(function _callee12$(_context12){while(1){switch(_context12.prev=_context12.next){case 0:_context12.prev=0;_context12.next=3;return( shell.exec(this.kubectl+' get secrets '+name,{direct:'silly'}));case 3:return _context12.abrupt('return');case 6:_context12.prev=6;_context12.t0=_context12['catch'](0);case 8: tmpdir=path.join(os.homedir(),'.temp-ssl'); if(!fs.existsSync(tmpdir))fs.mkdirSync(tmpdir); keyfile=path.join(tmpdir,name+'.key'); crtfile=path.join(tmpdir,name+'.crt'); // Make sure openssl is installed. _logger.logger.warn('Generating missing SSL certificate '+name+' on '+this.cluster.name+'.',{name:name});_context12.prev=13;_context12.next=16;return( shell.exec('openssl version',{direct:'verbose'}));case 16:_context12.next=23;break;case 18:_context12.prev=18;_context12.t1=_context12['catch'](13); _logger.logger.warn('OpenSSL utility missing, installing...');_context12.next=23;return( shell.exec('sudo apt-get install openssl -y',{direct:true}));case 23:_context12.next=25;return( shell.exec( 'openssl req -x509 -nodes -days 1825 -newkey rsa:2048 '+ '-subj /C=US/ST=Delaware/L=Dover/O=ops/OU=dev/CN=example.org '+('-keyout '+ keyfile+' -out '+crtfile), {direct:'debug'}));case 25:_context12.next=27;return( this.installSecret(name,[keyfile,crtfile]));case 27:case'end':return _context12.stop();}}},_callee12,this,[[0,6],[13,18]])}));function addSslSecret(_x17){return _ref12.apply(this,arguments)}return addSslSecret}() /** * Installs a set of files as a named secret * @param {string} name * @param {string[]} files */},{key:'installSecret',value:function(){var _ref13=_asyncToGenerator(/*#__PURE__*/regeneratorRuntime.mark(function _callee13( name,files){var fileArgs;return regeneratorRuntime.wrap(function _callee13$(_context13){while(1){switch(_context13.prev=_context13.next){case 0: // Install the secret ... _logger.logger.warn('Installing secret: '+name); fileArgs=files.map(function(f){return'--from-file='+f}).join(' ');return _context13.abrupt('return', shell.exec(this.kubectl+' create secret generic '+name+' '+fileArgs));case 3:case'end':return _context13.stop();}}},_callee13,this)}));function installSecret(_x18,_x19){return _ref13.apply(this,arguments)}return installSecret}() /** * @returns {string[]} returns any values of fields called 'message' recursively */}],[{key:'allmessages',value:function allmessages( obj,result){ if(obj&&obj.message)result.push((obj.reason?obj.reason+': ':'')+obj.message); if(Array.isArray(obj))obj.forEach(function(ch){return KubernetesControl.allmessages(ch,result)});else if((typeof obj==='undefined'?'undefined':_typeof(obj))==='object')Object.keys(obj||{}).forEach(function(k){return KubernetesControl.allmessages(obj[k],result)}); return result; }},{key:'isCircleCI',value:function isCircleCI() { return(process.env.CIRCLECI||process.env.CI)==='true'; }}]);return KubernetesControl}();