@vroomlabs/gsdk-deploy
Version:
Google Cloud deployment script for kubernetes clusters using Global Load Balancer
357 lines (284 loc) • 19.9 kB
JavaScript
'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}();