diff --git a/deploy/kubelessDeploy.js b/deploy/kubelessDeploy.js index 30be8b5..30f7548 100644 --- a/deploy/kubelessDeploy.js +++ b/deploy/kubelessDeploy.js @@ -18,115 +18,11 @@ const _ = require('lodash'); const BbPromise = require('bluebird'); -const Api = require('kubernetes-client'); +const deploy = require('../lib/deploy'); const fs = require('fs'); const helpers = require('../lib/helpers'); const JSZip = require('jszip'); -const moment = require('moment'); const path = require('path'); -const url = require('url'); - -function getFunctionDescription( - funcName, - namespace, - runtime, - deps, - funcContent, - handler, - desc, - labels, - env, - memory, - eventType, - eventTrigger -) { - const funcs = { - apiVersion: 'k8s.io/v1', - kind: 'Function', - metadata: { - name: funcName, - namespace, - }, - spec: { - deps: deps || '', - function: funcContent, - handler, - runtime, - }, - }; - if (desc) { - funcs.metadata.annotations = { - 'kubeless.serverless.com/description': desc, - }; - } - if (labels) { - funcs.metadata.labels = labels; - } - if (env || memory) { - const container = { - name: funcName, - }; - if (env) { - container.env = []; - _.each(env, (v, k) => { - container.env.push({ name: k, value: v.toString() }); - }); - } - if (memory) { - // If no suffix is given we assume the unit will be `Mi` - const memoryWithSuffix = memory.toString().match(/\d+$/) ? - `${memory}Mi` : - memory; - container.resources = { - limits: { memory: memoryWithSuffix }, - requests: { memory: memoryWithSuffix }, - }; - } - funcs.spec.template = { - spec: { containers: [container] }, - }; - } - switch (eventType) { - case 'http': - funcs.spec.type = 'HTTP'; - break; - case 'trigger': - funcs.spec.type = 'PubSub'; - if (_.isEmpty(eventTrigger)) { - throw new Error('You should specify a topic for the trigger event'); - } - funcs.spec.topic = eventTrigger; - break; - default: - throw new Error(`Event type ${eventType} is not supported`); - } - return funcs; -} - -function getIngressDescription(funcName, funcPath, funcHost) { - return { - kind: 'Ingress', - metadata: { - name: `ingress-${funcName}`, - labels: { function: funcName }, - annotations: { - 'kubernetes.io/ingress.class': 'nginx', - 'ingress.kubernetes.io/rewrite-target': '/', - }, - }, - spec: { - rules: [{ - host: funcHost, - http: { - paths: [{ - path: funcPath, - backend: { serviceName: funcName, servicePort: 8080 }, - }], - }, - }], - }, - }; -} class KubelessDeploy { constructor(serverless, options) { @@ -184,228 +80,13 @@ class KubelessDeploy { return resultPromise; } - getThirdPartyResources(connectionOptions) { - return new Api.ThirdPartyResources(connectionOptions); - } - - getExtensions(connectionOptions) { - return new Api.Extensions(connectionOptions); - } - - getRuntimeFilenames(runtime, handler) { - let files = null; - if (runtime.match(/python/)) { - files = { - handler: `${handler.toString().split('.')[0]}.py`, - deps: 'requirements.txt', - }; - } else if (runtime.match(/node/)) { - files = { - handler: `${handler.toString().split('.')[0]}.js`, - deps: 'package.json', - }; - } else if (runtime.match(/ruby/)) { - files = { - handler: `${handler.toString().split('.')[0]}.rb`, - deps: 'Gemfile', - }; - } else { - throw new Error( - `The runtime ${runtime} is not supported yet` - ); - } - return files; - } - - waitForDeployment(funcName, requestMoment, namespace) { - const core = new Api.Core(helpers.getConnectionOptions( - helpers.loadKubeConfig(), { namespace }) - ); - let retries = 0; - let successfulCount = 0; - let previousPodStatus = ''; - const loop = setInterval(() => { - if (retries > 3) { - this.serverless.cli.log( - `Giving up, unable to retrieve the status of the ${funcName} deployment. ` - ); - clearInterval(loop); - return; - } - let runningPods = 0; - core.pods.get((err, podsInfo) => { - if (err) { - if (err.message.match(/request timed out/)) { - this.serverless.cli.log('Request timed out. Retrying...'); - } else { - throw err; - } - } else { - // Get the pods for the current function - const functionPods = _.filter( - podsInfo.items, - (pod) => ( - pod.metadata.labels.function === funcName && - // Ignore pods that may still exist from a previous deployment - moment(pod.metadata.creationTimestamp) >= requestMoment - ) - ); - if (_.isEmpty(functionPods)) { - retries++; - this.serverless.cli.log( - `Unable to find any running pod for ${funcName}. Retrying...` - ); - } else { - _.each(functionPods, pod => { - // We assume that the function pods will only have one container - if (pod.status.containerStatuses) { - if (pod.status.containerStatuses[0].ready) { - runningPods++; - } else if (pod.status.containerStatuses[0].restartCount > 2) { - this.serverless.cli.log('ERROR: Failed to deploy the function'); - process.exitCode = process.exitCode || 1; - clearInterval(loop); - } - } - }); - if (runningPods === functionPods.length) { - // The pods may be running for a short time - // so we should ensure that they are stable - successfulCount++; - if (successfulCount === 2) { - this.serverless.cli.log( - `Function ${funcName} succesfully deployed` - ); - clearInterval(loop); - } - } else if (this.options.verbose) { - successfulCount = 0; - const currentPodStatus = _.map(functionPods, p => ( - p.status.containerStatuses ? - JSON.stringify(p.status.containerStatuses[0].state) : - 'unknown' - )); - if (!_.isEqual(previousPodStatus, currentPodStatus)) { - this.serverless.cli.log( - `Pods status: ${currentPodStatus}` - ); - previousPodStatus = currentPodStatus; - } - } - } - } - }); - }, 2000); - } - - deployFunctionAndWait(body, thirdPartyResources) { - const requestMoment = moment().milliseconds(0); - this.serverless.cli.log( - `Deploying function ${body.metadata.name}...` - ); - return new BbPromise((resolve, reject) => { - thirdPartyResources.ns.functions.post({ body }, (err) => { - if (err) { - if (err.code === 409) { - this.serverless.cli.log( - `The function ${body.metadata.name} already exists. ` + - 'Redeploy it usign --force or executing ' + - `"sls deploy function -f ${body.metadata.name}".` - ); - resolve(false); - } else { - reject(new Error( - `Unable to deploy the function ${body.metadata.name}. Received:\n` + - ` Code: ${err.code}\n` + - ` Message: ${err.message}` - )); - } - } else { - this.waitForDeployment( - body.metadata.name, - requestMoment, - thirdPartyResources.namespaces.namespace - ); - resolve(true); - } - }); - }); - } - - redeployFunctionAndWait(body, thirdPartyResources) { - const requestMoment = moment().milliseconds(0); - return new BbPromise((resolve, reject) => { - thirdPartyResources.ns.functions(body.metadata.name).put({ body }, (err) => { - if (err) { - reject(new Error( - `Unable to update the function ${body.metadata.name}. Received:\n` + - ` Code: ${err.code}\n` + - ` Message: ${err.message}` - )); - } else { - this.waitForDeployment(body.metadata.name, requestMoment); - resolve(true); - } - }); - }); - } - - addIngressRuleIfNecessary(funcName, eventType, eventPath, eventHostname, namespace) { - const config = helpers.loadKubeConfig(); - const extensions = this.getExtensions(helpers.getConnectionOptions( - config, { namespace }) - ); - const fpath = eventPath || '/'; - const hostname = eventHostname || - `${url.parse(helpers.getKubernetesAPIURL(config)).hostname}.nip.io`; - return new BbPromise((resolve, reject) => { - if ( - eventType === 'http' && - ((!_.isEmpty(eventPath) && eventPath !== '/') || !_.isEmpty(eventHostname)) - ) { - // Found a path to deploy the function - const absolutePath = _.startsWith(fpath, '/') ? - fpath : - `/${fpath}`; - const ingressDef = getIngressDescription(funcName, absolutePath, hostname); - extensions.ns.ingress.post({ body: ingressDef }, (err) => { - if (err) { - reject( - `Unable to deploy the function ${funcName} in the given path. ` + - `Received: ${err.message}` - ); - } else { - if (this.options.verbose) { - this.serverless.cli.log(`Deployed Ingress rule to map ${absolutePath}`); - } - resolve(); - } - }); - } else { - if (this.options.verbose) { - this.serverless.cli.log('Skiping ingress rule generation'); - } - resolve(); - } - }); - } - deployFunction() { - const errors = []; - let counter = 0; - return new BbPromise((resolve, reject) => { + const runtime = this.serverless.service.provider.runtime; + const populatedFunctions = []; + return new BbPromise((resolve) => { _.each(this.serverless.service.functions, (description, name) => { if (description.handler) { - const runtime = this.serverless.service.provider.runtime; - const files = this.getRuntimeFilenames(runtime, description.handler); - const connectionOptions = helpers.getConnectionOptions( - helpers.loadKubeConfig(), { - namespace: description.namespace || - this.serverless.service.provider.namespace, - } - ); - const thirdPartyResources = this.getThirdPartyResources(connectionOptions); - thirdPartyResources.addResource('functions'); + const files = helpers.getRuntimeFilenames(runtime, description.handler); this.getFunctionContent(files.handler) .then(functionContent => { this.getFunctionContent(files.deps) @@ -413,107 +94,47 @@ class KubelessDeploy { // No requirements found }) .then((requirementsContent) => { - const events = !_.isEmpty(description.events) ? - description.events : - [{ http: { path: '/' } }]; - _.each(events, event => { - const eventType = _.keys(event)[0]; - const funcs = getFunctionDescription( - name, - thirdPartyResources.namespaces.namespace, - this.serverless.service.provider.runtime, - requirementsContent, - functionContent, - description.handler, - description.description, - description.labels, - description.environment, - description.memorySize || this.serverless.service.provider.memorySize, - eventType, - event.trigger - ); - let deploymentPromise = null; - let redeployed = false; - thirdPartyResources.ns.functions.get((err, functionsInfo) => { - if (err) throw err; - // Check if the function has been already deployed - let existingFunction = false; - let existingSameFunction = false; - _.each(functionsInfo.items, item => { - if (_.isEqual(item.metadata.name, funcs.metadata.name)) { - existingFunction = true; - if (_.isEqual(item.spec, funcs.spec)) { - existingSameFunction = true; - } + populatedFunctions.push( + _.assign({}, description, { + id: name, + text: functionContent, + deps: requirementsContent, + events: _.map(description.events, (event) => { + const type = _.keys(event)[0]; + if (type === 'trigger') { + return _.assign({ type }, { trigger: event[type] }); } - }); - if (existingSameFunction) { - // The same function is already deployed, skipping the deployment - this.serverless.cli.log( - `Function ${name} has not changed. Skipping deployment` - ); - deploymentPromise = new BbPromise(r => r(false)); - } else if (existingFunction && this.options.force) { - // The function already exits but with a different content - deploymentPromise = this.redeployFunctionAndWait( - funcs, - thirdPartyResources - ); - redeployed = true; - } else { - deploymentPromise = this.deployFunctionAndWait(funcs, thirdPartyResources); - } - deploymentPromise.catch(deploymentErr => { - errors.push(deploymentErr); - }) - .then((deployed) => { - if (!deployed || redeployed) { - // If there were an error with the deployment - // or the function is already deployed - // don't try to add an ingress rule - return new BbPromise((r) => r()); - } - if (_.isUndefined(event[eventType])) { - throw new Error( - `Wrong defintion for the event ${event}. ` + - 'Expecting an Object with valid keys.' - ); - } - return this.addIngressRuleIfNecessary( - name, - eventType, - event[eventType].path, - this.serverless.service.provider.hostname || event[eventType].hostname, - connectionOptions.namespace - ); - }) - .catch(ingressErr => { - errors.push(ingressErr); - }) - .then(() => { - counter++; - if (counter === _.keys(this.serverless.service.functions).length) { - if (_.isEmpty(errors)) { - resolve(); - } else { - reject( - 'Found errors while deploying the given functions:\n' + - `${errors.join('\n')}` - ); - } - } - }); - }); - }); + return _.assign({ type }, event[type]); + }), + }) + ); + if ( + populatedFunctions.length === + _.keys(this.serverless.service.functions).length + ) { + resolve(); + } }); }); } else { - this.serverless.cli.log( - `Skipping deployment of ${name} since it doesn't have a handler` - ); + populatedFunctions.push(_.assign({}, description, { id: name })); + if (populatedFunctions.length === _.keys(this.serverless.service.functions).length) { + resolve(); + } } }); - }); + }).then(() => deploy( + populatedFunctions, + runtime, + { + namespace: this.serverless.service.provider.namespace, + hostname: this.serverless.service.provider.hostname, + memorySize: this.serverless.service.provider.memorySize, + force: this.options.force, + verbose: this.options.verbose, + log: this.serverless.cli.log.bind(this.serverless.cli), + } + )); } } diff --git a/info/kubelessInfo.js b/info/kubelessInfo.js index bd35a4d..d7fec27 100644 --- a/info/kubelessInfo.js +++ b/info/kubelessInfo.js @@ -17,15 +17,10 @@ 'use strict'; const _ = require('lodash'); -const Api = require('kubernetes-client'); const BbPromise = require('bluebird'); -const chalk = require('chalk'); +const getInfo = require('../lib/get-info'); const helpers = require('../lib/helpers'); -function toMultipleWords(word) { - return word.replace(/([A-Z])/, ' $1').replace(/^./, (l) => l.toUpperCase()); -} - class KubelessInfo { constructor(serverless, options) { this.serverless = serverless; @@ -62,136 +57,12 @@ class KubelessInfo { return BbPromise.resolve(); } - formatMessage(service, f, options) { - if (options && !options.color) chalk.enabled = false; - let message = ''; - message += `\n${chalk.yellow.underline(`Service Information "${service.name}"`)}\n`; - message += `${chalk.yellow('Cluster IP: ')} ${service.ip}\n`; - message += `${chalk.yellow('Type: ')} ${service.type}\n`; - message += `${chalk.yellow('Ports: ')}\n`; - _.each(service.ports, (port) => { - // Ports can have variable properties - _.each(port, (value, key) => { - message += ` ${chalk.yellow(`${toMultipleWords(key)}: `)} ${value}\n`; - }); - }); - if (this.options.verbose) { - message += `${chalk.yellow('Metadata')}\n`; - message += ` ${chalk.yellow('Self Link: ')} ${service.selfLink}\n`; - message += ` ${chalk.yellow('UID: ')} ${service.uid}\n`; - message += ` ${chalk.yellow('Timestamp: ')} ${service.timestamp}\n`; - } - message += `${chalk.yellow.underline('Function Info')}\n`; - if (f.url) { - message += `${chalk.yellow('URL: ')} ${f.url}\n`; - } - if (f.annotations && f.annotations['kubeless.serverless.com/description']) { - message += `${chalk.yellow('Description:')} ` + - `${f.annotations['kubeless.serverless.com/description']}\n`; - } - if (f.labels) { - message += `${chalk.yellow('Labels:\n')}`; - _.each(f.labels, (v, k) => { - message += `${chalk.yellow(` ${k}:`)} ${v}\n`; - }); - } - message += `${chalk.yellow('Handler: ')} ${f.handler}\n`; - message += `${chalk.yellow('Runtime: ')} ${f.runtime}\n`; - if (f.type === 'PubSub' && !_.isEmpty(f.topic)) { - message += `${chalk.yellow('Topic Trigger:')} ${f.topic}\n`; - } else { - message += `${chalk.yellow('Trigger: ')} ${f.type}\n`; - } - message += `${chalk.yellow('Dependencies: ')} ${f.deps}`; - if (this.options.verbose) { - message += `\n${chalk.yellow('Metadata:')}\n`; - message += ` ${chalk.yellow('Self Link: ')} ${f.selfLink}\n`; - message += ` ${chalk.yellow('UID: ')} ${f.uid}\n`; - message += ` ${chalk.yellow('Timestamp: ')} ${f.timestamp}`; - } - return message; - } - infoFunction(options) { - let counter = 0; - let message = ''; - return new BbPromise((resolve) => { - _.each(this.serverless.service.functions, (desc, f) => { - const connectionOptions = helpers.getConnectionOptions(helpers.loadKubeConfig(), { - namespace: desc.namespace || this.serverless.service.provider.namespace, - }); - const core = new Api.Core(connectionOptions); - const thirdPartyResources = new Api.ThirdPartyResources(connectionOptions); - const extensions = new Api.Extensions(connectionOptions); - thirdPartyResources.addResource('functions'); - core.services.get((err, servicesInfo) => { - if (err) throw new this.serverless.classes.Error(err); - thirdPartyResources.ns.functions.get((ferr, functionsInfo) => { - if (ferr) throw new this.serverless.classes.Error(ferr); - extensions.ns.ingress.get((ierr, ingressInfo) => { - if (ierr) throw this.serverless.classes.Error(ierr); - const fDesc = _.find(functionsInfo.items, item => item.metadata.name === f); - const functionService = _.find( - servicesInfo.items, - (service) => ( - service.metadata.labels && - service.metadata.labels.function === f - ) - ); - if (_.isEmpty(functionService) || _.isEmpty(fDesc)) { - this.serverless.cli.consoleLog( - `Not found any information about the function "${f}"` - ); - } else { - const fIngress = _.find(ingressInfo.items, item => ( - item.metadata.labels && item.metadata.labels.function === f - )); - let url = null; - if (fIngress) { - url = `${fIngress.spec.rules[0].host || 'API_URL'}` + - `${fIngress.spec.rules[0].http.paths[0].path}`; - } - const service = { - name: functionService.metadata.name, - ip: functionService.spec.clusterIP, - type: functionService.spec.type, - ports: functionService.spec.ports, - selfLink: functionService.metadata.selfLink, - uid: functionService.metadata.uid, - timestamp: functionService.metadata.creationTimestamp, - }; - const func = { - name: f, - url, - handler: fDesc.spec.handler, - runtime: fDesc.spec.runtime, - topic: fDesc.spec.topic, - type: fDesc.spec.type, - deps: fDesc.spec.deps, - annotations: fDesc.metadata.annotations, - labels: fDesc.metadata.labels, - selfLink: fDesc.metadata.selfLink, - uid: fDesc.metadata.uid, - timestamp: fDesc.metadata.creationTimestamp, - }; - message += this.formatMessage( - service, - func, - _.defaults({}, options, { color: true }) - ); - } - counter++; - if (counter === _.keys(this.serverless.service.functions).length) { - if (!_.isEmpty(message)) { - this.serverless.cli.consoleLog(message); - } - resolve(message); - } - }); - }); - }); - }); - }); + return getInfo(this.serverless.service.functions, _.defaults({}, options, { + namespace: this.serverless.service.provider.namespace, + verbose: this.options.verbose, + log: this.serverless.cli.consoleLog, + })); } } diff --git a/invoke/kubelessInvoke.js b/invoke/kubelessInvoke.js index e54c58d..cabf859 100644 --- a/invoke/kubelessInvoke.js +++ b/invoke/kubelessInvoke.js @@ -19,8 +19,8 @@ const _ = require('lodash'); const BbPromise = require('bluebird'); const path = require('path'); -const request = require('request'); const helpers = require('../lib/helpers'); +const invoke = require('../lib/invoke'); class KubelessInvoke { constructor(serverless, options) { @@ -36,46 +36,7 @@ class KubelessInvoke { }; } - getData(data) { - let result = null; - const d = data || this.options.data; - try { - if (!_.isEmpty(d)) { - try { - // Try to parse data as JSON - result = { - body: JSON.parse(d), - json: true, - }; - } catch (e) { - // Assume data is a string - result = { - body: d, - }; - } - } else if (this.options.path) { - const absolutePath = path.isAbsolute(this.options.path) ? - this.options.path : - path.join(this.serverless.config.servicePath, this.options.path); - if (!this.serverless.utils.fileExistsSync(absolutePath)) { - throw new this.serverless.classes.Error('The file you provided does not exist.'); - } - result = { - body: this.serverless.utils.readFileSync(absolutePath), - json: true, - }; - } - } catch (e) { - throw new this.serverless.classes.Error( - `Unable to parse data given in the arguments: \n${e.message}` - ); - } - return result; - } - validate() { - // Parse data to ensure it has a correct format - this.getData(); const unsupportedOptions = ['stage', 'region', 'type']; helpers.warnUnsupportedOptions( unsupportedOptions, @@ -93,58 +54,19 @@ class KubelessInvoke { invokeFunction(func, data) { const f = func || this.options.function; this.serverless.cli.log(`Calling function: ${f}...`); - const config = helpers.loadKubeConfig(); - const APIRootUrl = helpers.getKubernetesAPIURL(config); - const namespace = this.serverless.service.functions[f].namespace || - this.serverless.service.provider.namespace || - helpers.getDefaultNamespace(config); - const url = `${APIRootUrl}/api/v1/proxy/namespaces/${namespace}/services/${f}/`; - const connectionOptions = Object.assign( - helpers.getConnectionOptions(helpers.loadKubeConfig()), - { url } - ); - const requestData = this.getData(data); - if (this.serverless.service.functions[f].sequence) { - let promise = null; - _.each(this.serverless.service.functions[f].sequence.slice(), sequenceFunction => { - if (promise) { - promise = promise.then( - result => this.invokeFunction(sequenceFunction, result.body) - ); - } else { - promise = this.invokeFunction(sequenceFunction, data); - } - }); - return new BbPromise((resolve, reject) => promise.then( - (response) => resolve(response), - err => reject(err) - )); + let dataPath = this.options.path; + if (dataPath && !path.isAbsolute(dataPath)) { + dataPath = path.join(this.serverless.config.servicePath, dataPath); } - return new BbPromise((resolve, reject) => { - const parseReponse = (err, response) => { - if (err) { - reject(new this.serverless.classes.Error(err.message, err.statusCode)); - } else { - if (response.statusCode !== 200) { - reject(new this.serverless.classes.Error(response.statusMessage, response.statusCode)); - } - resolve(response); - } - }; - if (_.isEmpty(requestData)) { - // There is no data to send, sending a GET request - request.get(connectionOptions, parseReponse); - } else { - // Sending request data with a POST - request.post( - Object.assign( - connectionOptions, - requestData - ), - parseReponse - ); + return invoke( + f, + data || this.options.data, + _.map(this.serverless.service.functions, (desc, ff) => _.assign({}, desc, { id: ff })), + { + namespace: this.serverless.service.provider.namespace, + path: dataPath, } - }); + ); } log(response) { diff --git a/lib/deploy.js b/lib/deploy.js new file mode 100644 index 0000000..5fcf7a5 --- /dev/null +++ b/lib/deploy.js @@ -0,0 +1,455 @@ +/* + Copyright 2017 Bitnami. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +'use strict'; + +const _ = require('lodash'); +const BbPromise = require('bluebird'); +const Api = require('kubernetes-client'); +const helpers = require('./helpers'); +const moment = require('moment'); +const url = require('url'); + +function getFunctionDescription( + funcName, + namespace, + runtime, + deps, + funcContent, + handler, + desc, + labels, + env, + memory, + eventType, + eventTrigger +) { + const funcs = { + apiVersion: 'k8s.io/v1', + kind: 'Function', + metadata: { + name: funcName, + namespace, + }, + spec: { + deps: deps || '', + function: funcContent, + handler, + runtime, + }, + }; + if (desc) { + funcs.metadata.annotations = { + 'kubeless.serverless.com/description': desc, + }; + } + if (labels) { + funcs.metadata.labels = labels; + } + if (env || memory) { + const container = { + name: funcName, + }; + if (env) { + container.env = []; + _.each(env, (v, k) => { + container.env.push({ name: k, value: v.toString() }); + }); + } + if (memory) { + // If no suffix is given we assume the unit will be `Mi` + const memoryWithSuffix = memory.toString().match(/\d+$/) ? + `${memory}Mi` : + memory; + container.resources = { + limits: { memory: memoryWithSuffix }, + requests: { memory: memoryWithSuffix }, + }; + } + funcs.spec.template = { + spec: { containers: [container] }, + }; + } + switch (eventType) { + case 'http': + funcs.spec.type = 'HTTP'; + break; + case 'trigger': + funcs.spec.type = 'PubSub'; + if (_.isEmpty(eventTrigger)) { + throw new Error('You should specify a topic for the trigger event'); + } + funcs.spec.topic = eventTrigger; + break; + default: + throw new Error(`Event type ${eventType} is not supported`); + } + return funcs; +} + +function getIngressDescription(funcName, funcPath, funcHost) { + return { + kind: 'Ingress', + metadata: { + name: `ingress-${funcName}`, + labels: { function: funcName }, + annotations: { + 'kubernetes.io/ingress.class': 'nginx', + 'ingress.kubernetes.io/rewrite-target': '/', + }, + }, + spec: { + rules: [{ + host: funcHost, + http: { + paths: [{ + path: funcPath, + backend: { serviceName: funcName, servicePort: 8080 }, + }], + }, + }], + }, + }; +} + +function getThirdPartyResources(connectionOptions) { + return new Api.ThirdPartyResources(connectionOptions); +} + +function getExtensions(connectionOptions) { + return new Api.Extensions(connectionOptions); +} + +function waitForDeployment(funcName, requestMoment, namespace, options) { + const opts = _.defaults({}, options, { + verbose: false, + log: console.log, + }); + const core = new Api.Core(helpers.getConnectionOptions( + helpers.loadKubeConfig(), { namespace }) + ); + let retries = 0; + let successfulCount = 0; + let previousPodStatus = ''; + return new BbPromise((resolve, reject) => { + const loop = setInterval(() => { + if (retries > 3) { + opts.log( + `Giving up, unable to retrieve the status of the ${funcName} deployment. ` + ); + clearInterval(loop); + reject(`Unable to retrieve the status of the ${funcName} deployment`); + } + let runningPods = 0; + core.pods.get((err, podsInfo) => { + if (err) { + if (err.message.match(/request timed out/)) { + opts.log('Request timed out. Retrying...'); + } else { + throw err; + } + } else { + // Get the pods for the current function + const functionPods = _.filter( + podsInfo.items, + (pod) => ( + pod.metadata.labels.function === funcName && + // Ignore pods that may still exist from a previous deployment + moment(pod.metadata.creationTimestamp) >= requestMoment + ) + ); + if (_.isEmpty(functionPods)) { + retries++; + opts.log(`Unable to find any running pod for ${funcName}. Retrying...`); + } else { + _.each(functionPods, pod => { + // We assume that the function pods will only have one container + if (pod.status.containerStatuses) { + if (pod.status.containerStatuses[0].ready) { + runningPods++; + } else if (pod.status.containerStatuses[0].restartCount > 2) { + opts.log('ERROR: Failed to deploy the function'); + process.exitCode = process.exitCode || 1; + clearInterval(loop); + } + } + }); + if (runningPods === functionPods.length) { + // The pods may be running for a short time + // so we should ensure that they are stable + successfulCount++; + if (successfulCount === 2) { + opts.log(`Function ${funcName} succesfully deployed`); + clearInterval(loop); + resolve(); + } + } else if (opts.verbose) { + successfulCount = 0; + const currentPodStatus = _.map(functionPods, p => ( + p.status.containerStatuses ? + JSON.stringify(p.status.containerStatuses[0].state) : + 'unknown' + )); + if (!_.isEqual(previousPodStatus, currentPodStatus)) { + opts.log(`Pods status: ${currentPodStatus}`); + previousPodStatus = currentPodStatus; + } + } + } + } + }); + }, 2000); + }); +} + +function deployFunctionAndWait(body, thirdPartyResources, options) { + const opts = _.defaults({}, options, { + verbose: false, + log: console.log, + }); + const requestMoment = moment().milliseconds(0); + opts.log(`Deploying function ${body.metadata.name}...`); + return new BbPromise((resolve, reject) => { + thirdPartyResources.ns.functions.post({ body }, (err) => { + if (err) { + if (err.code === 409) { + opts.log( + `The function ${body.metadata.name} already exists. ` + + 'Redeploy it usign --force or executing ' + + `"sls deploy function -f ${body.metadata.name}".` + ); + resolve(false); + } else { + reject(new Error( + `Unable to deploy the function ${body.metadata.name}. Received:\n` + + ` Code: ${err.code}\n` + + ` Message: ${err.message}` + )); + } + } else { + waitForDeployment( + body.metadata.name, + requestMoment, + thirdPartyResources.namespaces.namespace, + { verbose: opts.verbose, log: opts.log } + ).then(() => { + resolve(true); + }); + } + }); + }); +} + +function redeployFunctionAndWait(body, thirdPartyResources, options) { + const opts = _.defaults({}, options, { + verbose: false, + }); + const requestMoment = moment().milliseconds(0); + return new BbPromise((resolve, reject) => { + thirdPartyResources.ns.functions(body.metadata.name).put({ body }, (err) => { + if (err) { + reject(new Error( + `Unable to update the function ${body.metadata.name}. Received:\n` + + ` Code: ${err.code}\n` + + ` Message: ${err.message}` + )); + } else { + waitForDeployment( + body.metadata.name, + requestMoment, + thirdPartyResources.namespaces.namespace, + { verbose: opts.verbose, log: opts.log } + ).then(() => { + resolve(true); + }); + } + }); + }); +} + +function addIngressRuleIfNecessary( + funcName, + eventType, + eventPath, + eventHostname, + namespace, + options +) { + const opts = _.defaults({}, options, { + verbose: false, + log: console.log, + }); + const config = helpers.loadKubeConfig(); + const extensions = getExtensions(helpers.getConnectionOptions( + config, { namespace }) + ); + const fpath = eventPath || '/'; + const hostname = eventHostname || + `${url.parse(helpers.getKubernetesAPIURL(config)).hostname}.nip.io`; + return new BbPromise((resolve, reject) => { + if ( + eventType === 'http' && + ((!_.isEmpty(eventPath) && eventPath !== '/') || !_.isEmpty(eventHostname)) + ) { + // Found a path to deploy the function + const absolutePath = _.startsWith(fpath, '/') ? + fpath : + `/${fpath}`; + const ingressDef = getIngressDescription(funcName, absolutePath, hostname); + extensions.ns.ingress.post({ body: ingressDef }, (err) => { + if (err) { + reject( + `Unable to deploy the function ${funcName} in the given path. ` + + `Received: ${err.message}` + ); + } else { + if (opts.verbose) { + opts.log(`Deployed Ingress rule to map ${absolutePath}`); + } + resolve(); + } + }); + } else { + if (opts.verbose) { + opts.log('Skipping ingress rule generation'); + } + resolve(); + } + }); +} + +function deploy(functions, runtime, options) { + const opts = _.defaults({}, options, { + hostname: null, + namespace: null, + memorySize: null, + force: false, + verbose: false, + log: console.log, + }); + const errors = []; + let counter = 0; + return new BbPromise((resolve, reject) => { + _.each(functions, (description) => { + if (description.handler) { + const connectionOptions = helpers.getConnectionOptions( + helpers.loadKubeConfig(), { + namespace: description.namespace || opts.namespace, + } + ); + const thirdPartyResources = getThirdPartyResources(connectionOptions); + thirdPartyResources.addResource('functions'); + const events = !_.isEmpty(description.events) ? + description.events : + [{ type: 'http', path: '/' }]; + _.each(events, event => { + const funcs = getFunctionDescription( + description.id, + thirdPartyResources.namespaces.namespace, + runtime, + description.deps, + description.text, + description.handler, + description.description, + description.labels, + description.environment, + description.memorySize || opts.memorySize, + event.type, + event.trigger + ); + let deploymentPromise = null; + let redeployed = false; + thirdPartyResources.ns.functions.get((err, functionsInfo) => { + if (err) throw err; + // Check if the function has been already deployed + let existingFunction = false; + let existingSameFunction = false; + _.each(functionsInfo.items, item => { + if (_.isEqual(item.metadata.name, funcs.metadata.name)) { + existingFunction = true; + if (_.isEqual(item.spec, funcs.spec)) { + existingSameFunction = true; + } + } + }); + if (existingSameFunction) { + // The same function is already deployed, skipping the deployment + opts.log(`Function ${description.id} has not changed. Skipping deployment`); + deploymentPromise = new BbPromise(r => r(false)); + } else if (existingFunction && opts.force) { + // The function already exits but with a different content + deploymentPromise = redeployFunctionAndWait( + funcs, + thirdPartyResources, + { verbose: opts.verbose, log: opts.log } + ); + redeployed = true; + } else { + deploymentPromise = deployFunctionAndWait( + funcs, + thirdPartyResources, + { verbose: opts.verbose, log: opts.log } + ); + } + deploymentPromise.catch(deploymentErr => { + errors.push(deploymentErr); + }) + .then((deployed) => { + let p = null; + if (!deployed || redeployed) { + // If there were an error with the deployment + // or the function is already deployed + // don't try to add an ingress rule + p = new BbPromise((r) => r()); + } else { + p = addIngressRuleIfNecessary( + description.id, + event.type, + event.path, + event.hostname || opts.hostname, + connectionOptions.namespace, + { verbose: opts.verbose, log: opts.log } + ); + p.catch(ingressErr => { + errors.push(ingressErr); + }); + } + p.then(() => { + counter++; + if (counter === _.keys(functions).length) { + if (_.isEmpty(errors)) { + resolve(); + } else { + reject(new Error( + 'Found errors while deploying the given functions:\n' + + `${errors.join('\n')}` + )); + } + } + }); + }); + }); + }); + } else { + opts.log( + `Skipping deployment of ${description.id} since it doesn't have a handler` + ); + } + }); + }); +} + +module.exports = deploy; diff --git a/lib/get-info.js b/lib/get-info.js new file mode 100644 index 0000000..a579593 --- /dev/null +++ b/lib/get-info.js @@ -0,0 +1,173 @@ +/* + Copyright 2017 Bitnami. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +'use strict'; + +const _ = require('lodash'); +const Api = require('kubernetes-client'); +const BbPromise = require('bluebird'); +const chalk = require('chalk'); +const helpers = require('../lib/helpers'); + +function toMultipleWords(word) { + return word.replace(/([A-Z])/, ' $1').replace(/^./, (l) => l.toUpperCase()); +} + +function formatMessage(service, f, options) { + const opts = _.defaults({}, options, { + color: false, + verbose: false, + }); + if (!opts.color) chalk.enabled = false; + let message = ''; + message += `\n${chalk.yellow.underline(`Service Information "${service.name}"`)}\n`; + message += `${chalk.yellow('Cluster IP: ')} ${service.ip}\n`; + message += `${chalk.yellow('Type: ')} ${service.type}\n`; + message += `${chalk.yellow('Ports: ')}\n`; + _.each(service.ports, (port) => { + // Ports can have variable properties + _.each(port, (value, key) => { + message += ` ${chalk.yellow(`${toMultipleWords(key)}: `)} ${value}\n`; + }); + }); + if (opts.verbose) { + message += `${chalk.yellow('Metadata')}\n`; + message += ` ${chalk.yellow('Self Link: ')} ${service.selfLink}\n`; + message += ` ${chalk.yellow('UID: ')} ${service.uid}\n`; + message += ` ${chalk.yellow('Timestamp: ')} ${service.timestamp}\n`; + } + message += `${chalk.yellow.underline('Function Info')}\n`; + if (f.url) { + message += `${chalk.yellow('URL: ')} ${f.url}\n`; + } + if (f.annotations && f.annotations['kubeless.serverless.com/description']) { + message += `${chalk.yellow('Description:')} ` + + `${f.annotations['kubeless.serverless.com/description']}\n`; + } + if (f.labels) { + message += `${chalk.yellow('Labels:\n')}`; + _.each(f.labels, (v, k) => { + message += `${chalk.yellow(` ${k}:`)} ${v}\n`; + }); + } + message += `${chalk.yellow('Handler: ')} ${f.handler}\n`; + message += `${chalk.yellow('Runtime: ')} ${f.runtime}\n`; + if (f.type === 'PubSub' && !_.isEmpty(f.topic)) { + message += `${chalk.yellow('Topic Trigger:')} ${f.topic}\n`; + } else { + message += `${chalk.yellow('Trigger: ')} ${f.type}\n`; + } + message += `${chalk.yellow('Dependencies: ')} ${_.trim(f.deps)}\n`; + if (opts.verbose) { + message += `${chalk.yellow('Metadata:')}\n`; + message += ` ${chalk.yellow('Self Link: ')} ${f.selfLink}\n`; + message += ` ${chalk.yellow('UID: ')} ${f.uid}\n`; + message += ` ${chalk.yellow('Timestamp: ')} ${f.timestamp}\n`; + } + return message; +} + +function info(functions, options) { + const opts = _.defaults({}, options, { + namespace: null, + verbose: false, + log: console.log, + }); + let counter = 0; + let message = ''; + return new BbPromise((resolve, reject) => { + _.each(functions, (desc, f) => { + const connectionOptions = helpers.getConnectionOptions(helpers.loadKubeConfig(), { + namespace: desc.namespace || opts.namespace, + }); + const core = new Api.Core(connectionOptions); + const thirdPartyResources = new Api.ThirdPartyResources(connectionOptions); + const extensions = new Api.Extensions(connectionOptions); + thirdPartyResources.addResource('functions'); + core.services.get((err, servicesInfo) => { + if (err) reject(new Error(err)); + thirdPartyResources.ns.functions.get((ferr, functionsInfo) => { + if (ferr) reject(new Error(ferr)); + if (_.isEmpty(functionsInfo)) { + reject(new Error('Unable to find any function')); + } else { + extensions.ns.ingress.get((ierr, ingressInfo) => { + if (ierr) reject(new Error(ierr)); + const fDesc = _.find(functionsInfo.items, item => item.metadata.name === f); + const functionService = _.find( + servicesInfo.items, + (service) => ( + service.metadata.labels && + service.metadata.labels.function === f + ) + ); + if (_.isEmpty(functionService) || _.isEmpty(fDesc)) { + opts.log(`Not found any information about the function "${f}"`); + } else { + const fIngress = _.find(ingressInfo.items, item => ( + item.metadata.labels && item.metadata.labels.function === f + )); + let url = null; + if (fIngress) { + url = `${fIngress.spec.rules[0].host || 'API_URL'}` + + `${fIngress.spec.rules[0].http.paths[0].path}`; + } + const service = { + name: functionService.metadata.name, + ip: functionService.spec.clusterIP, + type: functionService.spec.type, + ports: functionService.spec.ports, + selfLink: functionService.metadata.selfLink, + uid: functionService.metadata.uid, + timestamp: functionService.metadata.creationTimestamp, + }; + const func = { + name: f, + url, + handler: fDesc.spec.handler, + runtime: fDesc.spec.runtime, + topic: fDesc.spec.topic, + type: fDesc.spec.type, + deps: fDesc.spec.deps, + annotations: fDesc.metadata.annotations, + labels: fDesc.metadata.labels, + selfLink: fDesc.metadata.selfLink, + uid: fDesc.metadata.uid, + timestamp: fDesc.metadata.creationTimestamp, + }; + message += formatMessage( + service, + func, + _.defaults({}, opts, { color: true }), + { verbose: opts.verbose } + ); + } + counter++; + if (counter === _.keys(functions).length) { + if (!_.isEmpty(message)) { + opts.log(message); + } + resolve(message); + } + }); + } + }); + }); + }); + }); +} + +module.exports = info; diff --git a/lib/get-logs.js b/lib/get-logs.js new file mode 100644 index 0000000..aa6740a --- /dev/null +++ b/lib/get-logs.js @@ -0,0 +1,135 @@ +/* + Copyright 2017 Bitnami. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +'use strict'; + +const _ = require('lodash'); +const Api = require('kubernetes-client'); +const BbPromise = require('bluebird'); +const helpers = require('../lib/helpers'); +const moment = require('moment'); +const request = require('request'); + +function filterLogs(logs, options) { + const opts = _.defaults({}, options, { + startTime: null, + count: null, + filter: null, + }); + let logEntries = _.compact(logs.split('\n')); + if (opts.count) { + logEntries = logEntries.slice(logEntries.length - opts.count); + } + if (opts.filter) { + logEntries = _.filter(logEntries, entry => !!entry.match(opts.filter)); + } + if (opts.startTime) { + const since = !!opts.startTime.toString().match(/(?:m|h|d)/); + let startMoment = null; + if (since) { + startMoment = moment().subtract( + opts.startTime.replace(/\D/g, ''), + opts.startTime.replace(/\d/g, '') + ).valueOf(); + } else { + startMoment = moment(opts.startTime).valueOf(); + } + const logIndex = _.findIndex(logEntries, (entry) => { + const entryDate = entry.match( + /(\d{2}\/[a-zA-Z]{3}\/\d{4}:\d{2}:\d{2}:\d{2} \+\d{4}|-\d{4})/ + ); + if (entryDate) { + const entryMoment = moment(entryDate[1], 'DD/MMM/YYYY:HH:mm:ss Z').valueOf(); + return entryMoment >= startMoment; + } + return false; + }); + if (logIndex > -1) { + logEntries = logEntries.slice(logIndex); + } else { + // There is no entry after the given startTime + logEntries = []; + } + } + return logEntries.join('\n'); +} + +function printFilteredLogs(logs, opts) { + const filteredLogs = filterLogs(logs, opts); + if (!_.isEmpty(filteredLogs)) { + if (!opts.silent) { + console.log(filteredLogs); + } + } + return filteredLogs; +} + +function printLogs(func, options) { + const config = helpers.loadKubeConfig(); + const opts = _.defaults({}, options, { + namespace: options.namespace || helpers.getDefaultNamespace(config), + startTime: null, + count: null, + filter: null, + silent: false, + tail: false, + onData: (d) => { + const logs = d.toString().trim() || ''; + return printFilteredLogs(logs, opts); + }, + }); + const core = new Api.Core(helpers.getConnectionOptions(config, { namespace: opts.namespace })); + return new BbPromise((resolve, reject) => { + core.ns.pods.get((err, podsInfo) => { + if (err) reject(new Error(err)); + const functionPods = _.filter( + podsInfo.items, + (podInfo) => ( + podInfo.metadata.labels.function === func + ) + ); + if (_.isEmpty(functionPods)) { + reject( + new Error(`Unable to find the pod for the function ${func}. ` + + 'Please ensure that there is a function deployed with that ID') + ); + } else { + _.each(functionPods, functionPod => { + if (opts.tail) { + const APIRootUrl = helpers.getKubernetesAPIURL(helpers.loadKubeConfig()); + const url = `${APIRootUrl}/api/v1/namespaces/${opts.namespace}/pods/` + + `${functionPod.metadata.name}/log?follow=true`; + const connectionOptions = Object.assign( + helpers.getConnectionOptions(helpers.loadKubeConfig()), + { url } + ); + request.get( + connectionOptions + ).on('data', opts.onData); + } else { + core.ns.pods(functionPod.metadata.name).log.get((errLog, logs) => { + if (errLog) reject(new Error(errLog)); + const filteredLogs = printFilteredLogs(logs || '', opts); + resolve(filteredLogs); + }); + } + }); + } + }); + }); +} + +module.exports = printLogs; diff --git a/lib/helpers.js b/lib/helpers.js index f92a0d3..0d10209 100644 --- a/lib/helpers.js +++ b/lib/helpers.js @@ -168,10 +168,36 @@ function warnUnsupportedOptions(unsupportedOptions, definedOptions, logFunction) }); } +function getRuntimeFilenames(runtime, handler) { + let files = null; + if (runtime.match(/python/)) { + files = { + handler: `${handler.toString().split('.')[0]}.py`, + deps: 'requirements.txt', + }; + } else if (runtime.match(/node/)) { + files = { + handler: `${handler.toString().split('.')[0]}.js`, + deps: 'package.json', + }; + } else if (runtime.match(/ruby/)) { + files = { + handler: `${handler.toString().split('.')[0]}.rb`, + deps: 'Gemfile', + }; + } else { + throw new Error( + `The runtime ${runtime} is not supported yet` + ); + } + return files; +} + module.exports = { warnUnsupportedOptions, loadKubeConfig, getKubernetesAPIURL, getDefaultNamespace, getConnectionOptions, + getRuntimeFilenames, }; diff --git a/lib/invoke.js b/lib/invoke.js new file mode 100644 index 0000000..fb43be6 --- /dev/null +++ b/lib/invoke.js @@ -0,0 +1,131 @@ +/* + Copyright 2017 Bitnami. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +'use strict'; + +const _ = require('lodash'); +const BbPromise = require('bluebird'); +const fs = require('fs'); +const path = require('path'); +const request = require('request'); +const helpers = require('../lib/helpers'); + +function getData(data, options) { + const opts = _.defaults({}, options, { + path: null, + }); + let result = null; + try { + if (!_.isEmpty(data)) { + if (_.isPlainObject(data)) { + result = data; + } else { + try { + // Try to parse data as JSON + result = { + body: JSON.parse(data), + json: true, + }; + } catch (e) { + // Assume data is a string + result = { + body: data, + }; + } + } + } else if (opts.path) { + if (!path.isAbsolute(opts.path)) { + throw new Error('Data path should be absolute'); + } + if (!fs.existsSync(opts.path)) { + throw new Error('The file you provided does not exist.'); + } + result = { + body: fs.readFileSync(opts.path, 'utf-8'), + json: true, + }; + } + } catch (e) { + throw new Error( + `Unable to parse data given in the arguments: \n${e.message}` + ); + } + return result; +} + +function invoke(func, data, funcsDesc, options) { + const opts = _.defaults({}, options, { + namespace: null, + path: null, + }); + const config = helpers.loadKubeConfig(); + const APIRootUrl = helpers.getKubernetesAPIURL(config); + const desc = _.find(funcsDesc, d => d.id === func); + const namespace = desc.namespace || + opts.namespace || + helpers.getDefaultNamespace(config); + const url = `${APIRootUrl}/api/v1/proxy/namespaces/${namespace}/services/${func}/`; + const connectionOptions = Object.assign( + helpers.getConnectionOptions(helpers.loadKubeConfig()), + { url } + ); + const requestData = getData(data, { + path: opts.path, + }); + if (desc.sequence) { + let promise = null; + _.each(desc.sequence.slice(), sequenceFunction => { + if (promise) { + promise = promise.then( + result => invoke(sequenceFunction, result.body, funcsDesc, opts) + ); + } else { + promise = invoke(sequenceFunction, requestData, funcsDesc, opts); + } + }); + return new BbPromise((resolve, reject) => promise.then( + response => resolve(response), + err => reject(err) + )); + } + return new BbPromise((resolve, reject) => { + const parseReponse = (err, response) => { + if (err) { + reject(new Error(err.message)); + } else { + if (response.statusCode !== 200) { + reject(new Error(response.statusMessage)); + } + resolve(response); + } + }; + if (_.isEmpty(requestData)) { + // There is no data to send, sending a GET request + request.get(connectionOptions, parseReponse); + } else { + // Sending request data with a POST + request.post( + Object.assign( + connectionOptions, + requestData + ), + parseReponse + ); + } + }); +} + +module.exports = invoke; diff --git a/lib/remove.js b/lib/remove.js new file mode 100644 index 0000000..bdcbc16 --- /dev/null +++ b/lib/remove.js @@ -0,0 +1,122 @@ +/* + Copyright 2017 Bitnami. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +'use strict'; + +const _ = require('lodash'); +const Api = require('kubernetes-client'); +const BbPromise = require('bluebird'); +const helpers = require('../lib/helpers'); + +function removeIngressRuleIfNecessary(funcName, namespace, options) { + const opts = _.defaults({}, options, { + verbose: false, + log: console.log, + }); + const extensions = new Api.Extensions(helpers.getConnectionOptions(helpers.loadKubeConfig(), { + namespace, + })); + return new BbPromise((resolve, reject) => { + extensions.ns.ingress.get((err, ingressInfo) => { + const ingressRule = _.find(ingressInfo.items, item => ( + item.metadata.labels && item.metadata.labels.function === funcName + )); + if (!_.isEmpty(ingressRule)) { + extensions.ns.ingress.delete(ingressRule, (ingErr) => { + if (ingErr) { + reject( + `Unable to remove the ingress rule ${ingressRule}. Received:\n` + + ` Code: ${ingErr.code}\n` + + ` Message: ${ingErr.message}` + ); + } else { + if (opts.verbose) { + opts.log(`Removed Ingress rule ${ingressRule.metadata.name}`); + } + resolve(); + } + }); + } else { + if (opts.verbose) { + opts.log(`Skipping ingress rule clean up for ${funcName}`); + } + resolve(); + } + }); + }); +} + +function removeFunction(functions, options) { + const opts = _.defaults({}, options, { + namespace: null, + verbose: false, + log: console.log, + }); + const errors = []; + let counter = 0; + return new BbPromise((resolve, reject) => { + _.each(functions, (desc) => { + opts.log(`Removing function: ${desc.id}...`); + const connectionOptions = helpers.getConnectionOptions(helpers.loadKubeConfig(), { + namespace: desc.namespace || opts.namespace, + }); + const thirdPartyResources = new Api.ThirdPartyResources(connectionOptions); + thirdPartyResources.addResource('functions'); + // Delete function + thirdPartyResources.ns.functions.delete(desc.id, (err) => { + if (err) { + if (err.code === 404) { + opts.log( + `The function ${desc.id} doesn't exist. ` + + 'Skipping removal.' + ); + } else { + errors.push( + `Unable to remove the function ${desc.id}. Received:\n` + + ` Code: ${err.code}\n` + + ` Message: ${err.message}` + ); + } + } else { + removeIngressRuleIfNecessary( + desc.id, + connectionOptions.namespace, + { verbose: opts.verbose, log: opts.log } + ) + .catch((ingErr) => { + errors.push(ingErr); + }) + .then(() => { + counter++; + if (counter === _.keys(functions).length) { + if (_.isEmpty(errors)) { + resolve(); + } else { + reject( + 'Found errors while removing the given functions:\n' + + `${errors.join('\n')}` + ); + } + } + opts.log(`Function ${desc.id} succesfully deleted`); + }); + } + }); + }); + }); +} + +module.exports = removeFunction; diff --git a/logs/kubelessLogs.js b/logs/kubelessLogs.js index 1f8e6dd..de59c13 100644 --- a/logs/kubelessLogs.js +++ b/logs/kubelessLogs.js @@ -17,11 +17,9 @@ 'use strict'; const _ = require('lodash'); -const Api = require('kubernetes-client'); const BbPromise = require('bluebird'); +const getLogs = require('../lib/get-logs'); const helpers = require('../lib/helpers'); -const moment = require('moment'); -const request = require('request'); class KubelessLogs { constructor(serverless, options) { @@ -64,113 +62,17 @@ class KubelessLogs { return BbPromise.resolve(); } - filterLogs(logs, options) { - const opts = _.defaults({}, options, { - startTime: null, - count: null, - filter: null, - }); - let logEntries = _.compact(logs.split('\n')); - if (opts.count) { - logEntries = logEntries.slice(logEntries.length - opts.count); - } - if (opts.filter) { - logEntries = _.filter(logEntries, entry => !!entry.match(opts.filter)); - } - if (opts.startTime) { - const since = !!opts.startTime.toString().match(/(?:m|h|d)/); - let startMoment = null; - if (since) { - startMoment = moment().subtract( - opts.startTime.replace(/\D/g, ''), - opts.startTime.replace(/\d/g, '') - ).valueOf(); - } else { - startMoment = moment(opts.startTime).valueOf(); - } - const logIndex = _.findIndex(logEntries, (entry) => { - const entryDate = entry.match( - /(\d{2}\/[a-zA-Z]{3}\/\d{4}:\d{2}:\d{2}:\d{2} \+\d{4}|-\d{4})/ - ); - if (entryDate) { - const entryMoment = moment(entryDate[1], 'DD/MMM/YYYY:HH:mm:ss Z').valueOf(); - return entryMoment >= startMoment; - } - return false; - }); - if (logIndex > -1) { - logEntries = logEntries.slice(logIndex); - } else { - // There is no entry after the given startTime - logEntries = []; - } - } - return logEntries.join('\n'); - } - - printFilteredLogs(logs, opts) { - const filteredLogs = this.filterLogs(logs, opts); - if (!_.isEmpty(filteredLogs)) { - if (!opts.silent) { - console.log(filteredLogs); - } - } - return filteredLogs; - } - printLogs(options) { const opts = _.defaults({}, options, { startTime: this.options.startTime, count: this.options.count, filter: this.options.filter, silent: false, + tail: this.options.tail, + namespace: this.serverless.service.functions[this.options.function].namespace || + this.serverless.service.provider.namespace, }); - const config = helpers.loadKubeConfig(); - const namespace = this.serverless.service.functions[this.options.function].namespace || - this.serverless.service.provider.namespace || - helpers.getDefaultNamespace(config); - const core = new Api.Core(helpers.getConnectionOptions(config, { namespace })); - return new BbPromise((resolve, reject) => { - core.ns.pods.get((err, podsInfo) => { - if (err) throw new this.serverless.classes.Error(err); - const functionPods = _.filter( - podsInfo.items, - (podInfo) => ( - podInfo.metadata.labels.function === this.options.function - ) - ); - if (_.isEmpty(functionPods)) { - reject( - `Unable to find the pod for the function ${this.options.function}. ` + - 'Please ensure that there is a function deployed with that ID' - ); - } else { - _.each(functionPods, functionPod => { - if (this.options.tail) { - const APIRootUrl = helpers.getKubernetesAPIURL(helpers.loadKubeConfig()); - const url = `${APIRootUrl}/api/v1/namespaces/${namespace}/pods/` + - `${functionPod.metadata.name}/log?follow=true`; - const connectionOptions = Object.assign( - helpers.getConnectionOptions(helpers.loadKubeConfig()), - { url } - ); - request.get( - connectionOptions - ).on('data', (d) => { - const logs = d.toString().trim() || ''; - return this.printFilteredLogs(logs, opts); - }); - } else { - core.ns.pods(functionPod.metadata.name).log.get((errLog, logs) => { - if (errLog) throw new this.serverless.classes.Error(errLog); - const filteredLogs = this.printFilteredLogs(logs || '', opts); - return resolve(filteredLogs); - }); - } - }); - } - }); - }); + return getLogs(this.options.function, opts); } } diff --git a/package.json b/package.json index 4fd3470..f9d1ac4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "serverless-kubeless", - "version": "0.1.12", + "version": "0.1.13", "description": "This plugin enables support for Kubeless within the [Serverless Framework](https://github.com/serverless).", "main": "index.js", "directories": { @@ -41,6 +41,7 @@ "eslint-plugin-react": "^6.1.1", "fs-extra": "^4.0.1", "mocha": "^3.4.2", + "nock": "^9.0.14", "request": "^2.81.0", "sinon": "^2.3.6" } diff --git a/remove/kubelessRemove.js b/remove/kubelessRemove.js index e5ec7cd..a100456 100644 --- a/remove/kubelessRemove.js +++ b/remove/kubelessRemove.js @@ -17,14 +17,14 @@ 'use strict'; const _ = require('lodash'); -const Api = require('kubernetes-client'); const BbPromise = require('bluebird'); const helpers = require('../lib/helpers'); +const remove = require('../lib/remove'); class KubelessRemove { constructor(serverless, options) { this.serverless = serverless; - this.options = options; + this.options = options || {}; this.provider = this.serverless.getProvider('kubeless'); this.hooks = { @@ -44,88 +44,16 @@ class KubelessRemove { return BbPromise.resolve(); } - removeIngressRuleIfNecessary(funcName, namespace) { - const extensions = new Api.Extensions(helpers.getConnectionOptions(helpers.loadKubeConfig(), { - namespace, - })); - return new BbPromise((resolve, reject) => { - extensions.ns.ingress.get((err, ingressInfo) => { - const ingressRule = _.find(ingressInfo.items, item => ( - item.metadata.labels && item.metadata.labels.function === funcName - )); - if (!_.isEmpty(ingressRule)) { - extensions.ns.ingress.delete(ingressRule, (ingErr) => { - if (ingErr) { - reject( - `Unable to remove the ingress rule ${ingressRule}. Received:\n` + - ` Code: ${ingErr.code}\n` + - ` Message: ${ingErr.message}` - ); - } else { - if (this.options.verbose) { - this.serverless.cli.log(`Removed Ingress rule ${ingressRule.metadata.name}`); - } - resolve(); - } - }); - } else { - if (this.options.verbose) { - this.serverless.cli.log(`Skipping ingress rule clean up for ${funcName}`); - } - resolve(); - } - }); - }); - } - removeFunction() { - const errors = []; - let counter = 0; - return new BbPromise((resolve, reject) => { - _.each(this.serverless.service.functions, (desc, f) => { - this.serverless.cli.log(`Removing function: ${f}...`); - const connectionOptions = helpers.getConnectionOptions(helpers.loadKubeConfig(), { - namespace: desc.namespace || this.serverless.service.provider.namespace, - }); - const thirdPartyResources = new Api.ThirdPartyResources(connectionOptions); - thirdPartyResources.addResource('functions'); - // Delete function - thirdPartyResources.ns.functions.delete(f, (err) => { - if (err) { - if (err.code === 404) { - this.serverless.cli.log( - `The function ${f} doesn't exist. ` + - 'Skipping removal.' - ); - } else { - errors.push( - `Unable to remove the function ${f}. Received:\n` + - ` Code: ${err.code}\n` + - ` Message: ${err.message}` - ); - } - } else { - this.removeIngressRuleIfNecessary(f, connectionOptions.namespace) - .catch((ingErr) => { - errors.push(ingErr); - }) - .then(() => { - counter++; - if (counter === _.keys(this.serverless.service.functions).length) { - if (_.isEmpty(errors)) { - resolve(); - } else { - reject( - 'Found errors while removing the given functions:\n' + - `${errors.join('\n')}` - ); - } - } - this.serverless.cli.log(`Function ${f} succesfully deleted`); - }); - } - }); - }); + this.serverless.cli.log(`Removing Service ${this.serverless.service.service}...`); + const parsedFunctions = _.map( + this.serverless.service.functions, + (f, id) => _.assign({ id }, f) + ); + return remove(parsedFunctions, { + namespace: this.serverless.service.provider.namespace, + verbose: this.options.verbose, + log: this.serverless.cli.log.bind(this.serverless.cli), }); } } diff --git a/test/kubelessDeploy.test.js b/test/kubelessDeploy.test.js index 0bc149d..8933901 100644 --- a/test/kubelessDeploy.test.js +++ b/test/kubelessDeploy.test.js @@ -17,14 +17,14 @@ 'use strict'; const _ = require('lodash'); -const Api = require('kubernetes-client'); const BbPromise = require('bluebird'); const chaiAsPromised = require('chai-as-promised'); const expect = require('chai').expect; const fs = require('fs'); -const helpers = require('../lib/helpers'); +const nock = require('nock'); const mocks = require('./lib/mocks'); const moment = require('moment'); +const os = require('os'); const path = require('path'); const rm = require('./lib/rm'); const sinon = require('sinon'); @@ -95,246 +95,21 @@ describe('KubelessDeploy', () => { describe('#validate', () => { it('prints a message if an unsupported option is given', () => { const kubelessDeploy = new KubelessDeploy(serverless, { region: 'us-east1' }); - sinon.stub(serverless.cli, 'log'); - try { - expect(() => kubelessDeploy.validate()).to.not.throw(); - expect(serverless.cli.log.firstCall.args).to.be.eql( + expect(() => kubelessDeploy.validate()).to.not.throw(); + expect(serverless.cli.log.firstCall.args).to.be.eql( ['Warning: Option region is not supported for the kubeless plugin'] ); - } finally { - serverless.cli.log.restore(); - } - }); - }); - describe('#getThirdPartyResources', () => { - let cwd = null; - beforeEach(() => { - cwd = mocks.kubeConfig(); - }); - afterEach(() => { - mocks.restoreKubeConfig(cwd); - }); - it('should instantiate taking the values from the kubernetes config', () => { - const thirdPartyResources = KubelessDeploy.prototype.getThirdPartyResources( - helpers.getConnectionOptions(helpers.loadKubeConfig()) - ); - expect(thirdPartyResources.url).to.be.eql('http://1.2.3.4:4433'); - expect(thirdPartyResources.requestOptions).to.be.eql({ - ca: Buffer.from('LS0tLS1', 'base64'), - cert: undefined, - key: undefined, - auth: { - user: 'admin', - password: 'password1234', - }, - }); - expect(thirdPartyResources.namespaces.namespace).to.be.eql('custom'); - }); - }); - - describe('#waitForDeployment', () => { - let clock = null; - const kubelessDeploy = instantiateKubelessDeploy('', '', serverless); - kubelessDeploy.waitForDeployment.restore(); - let cwd = null; - beforeEach(() => { - cwd = mocks.kubeConfig(); - sinon.stub(Api.Core.prototype, 'get'); - clock = sinon.useFakeTimers(); - }); - afterEach(() => { - mocks.restoreKubeConfig(cwd); - Api.Core.prototype.get.restore(); - clock.restore(); - }); - it('should wait until a deployment is ready', () => { - const f = 'test'; - Api.Core.prototype.get.onFirstCall().callsFake((opts, ff) => { - ff(null, { - statusCode: 200, - body: { - items: [{ - metadata: { - labels: { function: f }, - creationTimestamp: moment().add('1', 's'), - }, - status: { - containerStatuses: [{ - ready: false, - restartCount: 0, - state: 'Pending', - }], - }, - }], - }, - }); - }); - Api.Core.prototype.get.callsFake((opts, ff) => { - ff(null, { - statusCode: 200, - body: { - items: [{ - metadata: { - labels: { function: f }, - creationTimestamp: moment(), - }, - status: { - containerStatuses: [{ - ready: true, - restartCount: 0, - state: 'Ready', - }], - }, - }], - }, - }); - }); - kubelessDeploy.waitForDeployment(f, moment()); - clock.tick(2001); - expect(Api.Core.prototype.get.callCount).to.be.eql(1); - clock.tick(4001); - expect(Api.Core.prototype.get.callCount).to.be.eql(3); - // The timer should be already cleared - clock.tick(10001); - expect(Api.Core.prototype.get.callCount).to.be.eql(3); - }); - it('should wait until a deployment is ready (with no containerStatuses info)', () => { - const f = 'test'; - Api.Core.prototype.get.onFirstCall().callsFake((opts, ff) => { - ff(null, { - statusCode: 200, - body: { - items: [{ - metadata: { - labels: { function: f }, - creationTimestamp: moment().add('1', 's'), - }, - status: {}, - }], - }, - }); - }); - Api.Core.prototype.get.callsFake((opts, ff) => { - ff(null, { - statusCode: 200, - body: { - items: [{ - metadata: { - labels: { function: f }, - creationTimestamp: moment(), - }, - status: { - containerStatuses: [{ - ready: true, - restartCount: 0, - state: 'Ready', - }], - }, - }], - }, - }); - }); - kubelessDeploy.waitForDeployment(f, moment()); - clock.tick(2001); - expect(Api.Core.prototype.get.callCount).to.be.eql(1); - clock.tick(4001); - expect(Api.Core.prototype.get.callCount).to.be.eql(3); - // The timer should be already cleared - clock.tick(10001); - expect(Api.Core.prototype.get.callCount).to.be.eql(3); - }); - it('should throw an error if the pod failed to start', () => { - const f = 'test'; - Api.Core.prototype.get.callsFake((opts, ff) => { - ff(null, { - statusCode: 200, - body: { - items: [{ - metadata: { - labels: { function: f }, - creationTimestamp: moment().add('1', 's'), - }, - status: { - containerStatuses: [{ - ready: false, - restartCount: 3, - state: 'waiting', - }], - }, - }], - }, - }); - }); - sinon.stub(kubelessDeploy.serverless.cli, 'log'); - try { - kubelessDeploy.waitForDeployment(f, moment()); - clock.tick(4001); - expect(kubelessDeploy.serverless.cli.log.lastCall.args[0]).to.be.eql( - 'ERROR: Failed to deploy the function' - ); - expect(process.exitCode).to.be.eql(1); - } finally { - kubelessDeploy.serverless.cli.log.restore(); - } - }); - it('should retry if it fails to retrieve pods info', () => { - const f = 'test'; - Api.Core.prototype.get.onFirstCall().callsFake((opts, ff) => { - ff(new Error('etcdserver: request timed out')); - }); - Api.Core.prototype.get.callsFake((opts, ff) => { - ff(null, { - statusCode: 200, - body: { - items: [{ - metadata: { - labels: { function: f }, - creationTimestamp: moment(), - }, - status: { - containerStatuses: [{ - ready: true, - restartCount: 0, - state: 'Ready', - }], - }, - }], - }, - }); - }); - kubelessDeploy.waitForDeployment(f, moment()); - clock.tick(2001); - expect(Api.Core.prototype.get.callCount).to.be.eql(1); - clock.tick(4001); - expect(Api.Core.prototype.get.callCount).to.be.eql(3); - // The timer should be already cleared - clock.tick(2001); - expect(Api.Core.prototype.get.callCount).to.be.eql(3); - }); - it('fail if the pod never appears', () => { - const f = 'test'; - Api.Core.prototype.get.callsFake((opts, ff) => { - ff(null, { statusCode: 200, body: {} }); - }); - const logStub = sinon.stub(kubelessDeploy.serverless.cli, 'log'); - try { - kubelessDeploy.waitForDeployment(f, moment()); - clock.tick(10001); - expect(logStub.lastCall.args[0]).to.contain( - 'unable to retrieve the status of the test deployment' - ); - } finally { - logStub.restore(); - // Api.Core.prototype.get.restore(); - } }); }); describe('#deploy', () => { + let clock = null; let cwd = null; + let config = null; let handlerFile = null; let depsFile = null; const functionName = 'myFunction'; + const functionText = 'function code'; const serverlessWithFunction = _.defaultsDeep({}, serverless, { config: {}, service: { @@ -346,40 +121,37 @@ describe('KubelessDeploy', () => { }; let kubelessDeploy = null; - let thirdPartyResources = null; beforeEach(() => { - cwd = mocks.kubeConfig(); + cwd = path.join(os.tmpdir(), moment().valueOf().toString()); + fs.mkdirSync(cwd); + setInterval(() => { + clock.tick(2001); + }, 100); + clock = sinon.useFakeTimers(); + config = mocks.kubeConfig(cwd); serverlessWithFunction.config.servicePath = cwd; handlerFile = path.join(cwd, 'function.py'); - fs.writeFileSync(handlerFile, 'function code'); + fs.writeFileSync(handlerFile, functionText); depsFile = path.join(cwd, 'requirements.txt'); kubelessDeploy = instantiateKubelessDeploy(handlerFile, depsFile, serverlessWithFunction); - thirdPartyResources = mocks.thirdPartyResources(kubelessDeploy); }); - after(() => { + afterEach(() => { + clock.restore(); + nock.cleanAll(); rm(cwd); }); it('should deploy a function (python)', () => { - const result = expect( // eslint-disable-line no-unused-expressions + mocks.createDeploymentNocks(config.clusters[0].cluster.server, functionName, { + deps: '', + function: functionText, + handler: serverlessWithFunction.service.functions[functionName].handler, + runtime: serverlessWithFunction.service.provider.runtime, + type: 'HTTP', + }); + return expect( // eslint-disable-line no-unused-expressions kubelessDeploy.deployFunction() ).to.be.fulfilled; - expect(thirdPartyResources.ns.functions.post.calledOnce).to.be.eql(true); - expect(thirdPartyResources.ns.functions.post.firstCall.args[0].body).to.be.eql( - { apiVersion: 'k8s.io/v1', - kind: 'Function', - metadata: { name: functionName, namespace: 'default' }, - spec: - { deps: '', - function: 'function code', - handler: 'function.hello', - runtime: 'python2.7', - type: 'HTTP' } } - ); - expect( - thirdPartyResources.ns.functions.post.firstCall.args[1] - ).to.be.a('function'); - return result; }); it('should deploy a function (nodejs)', () => { handlerFile = path.join(cwd, 'function.js'); @@ -387,29 +159,19 @@ describe('KubelessDeploy', () => { fs.writeFileSync(handlerFile, 'nodejs function code'); fs.writeFileSync(depsFile, 'nodejs function deps'); kubelessDeploy = instantiateKubelessDeploy(handlerFile, depsFile, _.defaultsDeep( - { service: { provider: { runtime: 'nodejs6.10' } } }, + { service: { provider: { runtime: 'nodejs6' } } }, serverlessWithFunction )); - thirdPartyResources = mocks.thirdPartyResources(kubelessDeploy); - const result = expect( // eslint-disable-line no-unused-expressions + mocks.createDeploymentNocks(config.clusters[0].cluster.server, functionName, { + deps: 'nodejs function deps', + function: 'nodejs function code', + handler: serverlessWithFunction.service.functions[functionName].handler, + runtime: 'nodejs6', + type: 'HTTP', + }); + return expect( // eslint-disable-line no-unused-expressions kubelessDeploy.deployFunction() ).to.be.fulfilled; - expect(thirdPartyResources.ns.functions.post.calledOnce).to.be.eql(true); - expect(thirdPartyResources.ns.functions.post.firstCall.args[0].body).to.be.eql( - { apiVersion: 'k8s.io/v1', - kind: 'Function', - metadata: { name: functionName, namespace: 'default' }, - spec: - { deps: 'nodejs function deps', - function: 'nodejs function code', - handler: 'function.hello', - runtime: 'nodejs6.10', - type: 'HTTP' } } - ); - expect( - thirdPartyResources.ns.functions.post.firstCall.args[1] - ).to.be.a('function'); - return result; }); it('should deploy a function (ruby)', () => { handlerFile = path.join(cwd, 'function.rb'); @@ -420,26 +182,16 @@ describe('KubelessDeploy', () => { { service: { provider: { runtime: 'ruby2.4' } } }, serverlessWithFunction )); - thirdPartyResources = mocks.thirdPartyResources(kubelessDeploy); - const result = expect( // eslint-disable-line no-unused-expressions + mocks.createDeploymentNocks(config.clusters[0].cluster.server, functionName, { + deps: 'ruby function deps', + function: 'ruby function code', + handler: serverlessWithFunction.service.functions[functionName].handler, + runtime: 'ruby2.4', + type: 'HTTP', + }); + return expect( // eslint-disable-line no-unused-expressions kubelessDeploy.deployFunction() ).to.be.fulfilled; - expect(thirdPartyResources.ns.functions.post.calledOnce).to.be.eql(true); - expect(thirdPartyResources.ns.functions.post.firstCall.args[0].body).to.be.eql( - { apiVersion: 'k8s.io/v1', - kind: 'Function', - metadata: { name: functionName, namespace: 'default' }, - spec: - { deps: 'ruby function deps', - function: 'ruby function code', - handler: 'function.hello', - runtime: 'ruby2.4', - type: 'HTTP' } } - ); - expect( - thirdPartyResources.ns.functions.post.firstCall.args[1] - ).to.be.a('function'); - return result; }); it('should deploy a function in a custom namespace (in the provider section)', () => { const serverlessWithCustomNamespace = _.cloneDeep(serverlessWithFunction); @@ -449,18 +201,16 @@ describe('KubelessDeploy', () => { depsFile, serverlessWithCustomNamespace ); - thirdPartyResources = mocks.thirdPartyResources(kubelessDeploy, 'custom'); - const result = expect( // eslint-disable-line no-unused-expressions + mocks.createDeploymentNocks(config.clusters[0].cluster.server, functionName, { + deps: '', + function: functionText, + handler: serverlessWithFunction.service.functions[functionName].handler, + runtime: serverlessWithFunction.service.provider.runtime, + type: 'HTTP', + }, { namespace: 'custom' }); + return expect( // eslint-disable-line no-unused-expressions kubelessDeploy.deployFunction() ).to.be.fulfilled; - expect(thirdPartyResources.ns.functions.post.calledOnce).to.be.eql(true); - expect( - thirdPartyResources.ns.functions.post.firstCall.args[0].body.metadata.namespace - ).to.be.eql('custom'); - expect( - thirdPartyResources.ns.functions.post.firstCall.args[1] - ).to.be.a('function'); - return result; }); it('should deploy a function in a custom namespace (in the function section)', () => { const serverlessWithCustomNamespace = _.cloneDeep(serverlessWithFunction); @@ -470,105 +220,226 @@ describe('KubelessDeploy', () => { depsFile, serverlessWithCustomNamespace ); - thirdPartyResources = mocks.thirdPartyResources(kubelessDeploy, 'custom'); - const result = expect( // eslint-disable-line no-unused-expressions + mocks.createDeploymentNocks(config.clusters[0].cluster.server, functionName, { + deps: '', + function: functionText, + handler: serverlessWithFunction.service.functions[functionName].handler, + runtime: serverlessWithFunction.service.provider.runtime, + type: 'HTTP', + }, { namespace: 'custom' }); + return expect( // eslint-disable-line no-unused-expressions kubelessDeploy.deployFunction() ).to.be.fulfilled; - expect(thirdPartyResources.ns.functions.post.calledOnce).to.be.eql(true); - expect( - thirdPartyResources.ns.functions.post.firstCall.args[0].body.metadata.namespace - ).to.be.eql('custom'); - expect( - thirdPartyResources.ns.functions.post.firstCall.args[1] - ).to.be.a('function'); - return result; }); - it('should skip a deployment if the same specification is already deployed', () => { - thirdPartyResources.ns.functions.get.callsFake((ff) => { - ff(null, { + + it('should wait until a deployment is ready', () => { + const funcSpec = { + deps: '', + function: functionText, + handler: serverlessWithFunction.service.functions[functionName].handler, + runtime: serverlessWithFunction.service.provider.runtime, + type: 'HTTP', + }; + // First call, still deploying: + nock(config.clusters[0].cluster.server) + .get('/api/v1/pods') + .reply(200, { items: [{ metadata: { name: functionName, labels: { function: functionName }, - creationTimestamp: moment(), + creationTimestamp: moment().add('60', 's'), }, + spec: funcSpec, status: { - containerStatuses: [{ - ready: true, - restartCount: 0, - state: 'Ready', - }], + containerStatuses: [{ ready: false, restartCount: 0 }], }, - spec: { - deps: '', - function: 'function code', - handler: 'function.hello', - runtime: 'python2.7', - type: 'HTTP', + }], + }); + // Second call, ready: + mocks.createDeploymentNocks(config.clusters[0].cluster.server, functionName, funcSpec); + return expect( // eslint-disable-line no-unused-expressions + kubelessDeploy.deployFunction().then(() => { + expect(nock.pendingMocks()).to.be.eql([]); + }) + ).to.be.fulfilled; + }); + it('should wait until a deployment is ready (with no containerStatuses info)', () => { + const funcSpec = { + deps: '', + function: functionText, + handler: serverlessWithFunction.service.functions[functionName].handler, + runtime: serverlessWithFunction.service.provider.runtime, + type: 'HTTP', + }; + // First call, still deploying: + nock(config.clusters[0].cluster.server) + .get('/api/v1/pods') + .reply(200, { + items: [{ + metadata: { + name: functionName, + labels: { function: functionName }, + creationTimestamp: moment().add('60', 's'), }, + spec: funcSpec, + status: {}, }], }); + // Second call, ready: + mocks.createDeploymentNocks(config.clusters[0].cluster.server, functionName, funcSpec); + return expect( // eslint-disable-line no-unused-expressions + kubelessDeploy.deployFunction().then(() => { + expect(nock.pendingMocks()).to.be.eql([]); + }) + ).to.be.fulfilled; + }); + it('should throw an error if the pod failed to start', () => { + const funcSpec = { + deps: '', + function: functionText, + handler: serverlessWithFunction.service.functions[functionName].handler, + runtime: serverlessWithFunction.service.provider.runtime, + type: 'HTTP', + }; + nock(config.clusters[0].cluster.server) + .get('/api/v1/pods') + .reply(200, { + items: [{ + metadata: { + name: functionName, + labels: { function: functionName }, + creationTimestamp: moment().add('60', 's'), + }, + spec: funcSpec, + status: { + containerStatuses: [{ ready: false, restartCount: 3 }], + }, + }], + }); + kubelessDeploy.deployFunction().then(() => { + expect(kubelessDeploy.serverless.cli.log.lastCall.args[0]).to.be.eql( + 'ERROR: Failed to deploy the function' + ); + expect(process.exitCode).to.be.eql(1); + kubelessDeploy.serverless.cli.log.restore(); + }); + }); + it('should retry if it fails to retrieve pods info', () => { + const funcSpec = { + deps: '', + function: functionText, + handler: serverlessWithFunction.service.functions[functionName].handler, + runtime: serverlessWithFunction.service.provider.runtime, + type: 'HTTP', + }; + // First call, fails to retrieve status + nock(config.clusters[0].cluster.server) + .get('/api/v1/pods') + .replyWithError('etcdserver: request timed out'); + // Second call, ready: + mocks.createDeploymentNocks(config.clusters[0].cluster.server, functionName, funcSpec); + return expect( // eslint-disable-line no-unused-expressions + kubelessDeploy.deployFunction().then(() => { + expect(nock.pendingMocks()).to.be.eql([]); + }) + ).to.be.fulfilled; + }); + it('fail if the pod never appears', () => { + const funcSpec = { + deps: '', + function: functionText, + handler: serverlessWithFunction.service.functions[functionName].handler, + runtime: serverlessWithFunction.service.provider.runtime, + type: 'HTTP', + }; + // First call, fails to retrieve status + nock(config.clusters[0].cluster.server) + .persist() + .get('/api/v1/pods') + .reply(200, { items: [] }); + // Second call, ready: + mocks.createDeploymentNocks(config.clusters[0].cluster.server, functionName, funcSpec); + // return expect( // eslint-disable-line no-unused-expressions + expect( + kubelessDeploy.deployFunction() + ).to.be.eventually.rejectedWith( + `Unable to retrieve the status of the ${functionName} deployment` + ); + }); + + it('should skip a deployment if the same specification is already deployed', () => { + const funcSpec = { + deps: '', + function: functionText, + handler: serverlessWithFunction.service.functions[functionName].handler, + runtime: serverlessWithFunction.service.provider.runtime, + type: 'HTTP', + }; + mocks.createDeploymentNocks(config.clusters[0].cluster.server, functionName, funcSpec, { + existingFunctions: [{ + metadata: { + name: functionName, + labels: { function: functionName }, + }, + spec: funcSpec, + }], }); - sinon.stub(serverlessWithFunction.cli, 'log'); let result = null; - try { - result = expect( // eslint-disable-line no-unused-expressions - kubelessDeploy.deployFunction() - ).to.be.fulfilled; - expect(serverlessWithFunction.cli.log.lastCall.args).to.be.eql( - [ - `Function ${functionName} has not changed. Skipping deployment`, - ] - ); - expect(thirdPartyResources.ns.functions.post.callCount).to.be.eql(0); - } finally { - serverlessWithFunction.cli.log.restore(); - } + result = expect( // eslint-disable-line no-unused-expressions + kubelessDeploy.deployFunction().then(() => { + expect(serverlessWithFunction.cli.log.lastCall.args).to.be.eql( + [ + `Function ${functionName} has not changed. Skipping deployment`, + ] + ); + expect(nock.pendingMocks()).to.contain( + `POST ${config.clusters[0].cluster.server}/apis/k8s.io/v1/namespaces/default/functions` + ); + }) + ).to.be.fulfilled; return result; }); it('should skip a deployment if an error 409 is returned', () => { - thirdPartyResources.ns.functions.post.callsFake((data, ff) => { - ff({ code: 409 }); - }); - sinon.stub(serverlessWithFunction.cli, 'log'); + nock(config.clusters[0].cluster.server) + .get('/apis/k8s.io/v1/namespaces/default/functions') + .reply(200, []); + nock(config.clusters[0].cluster.server) + .post('/apis/k8s.io/v1/namespaces/default/functions') + .reply(409, 'Resource already exists'); let result = null; - try { - result = expect( // eslint-disable-line no-unused-expressions - kubelessDeploy.deployFunction() + result = expect( // eslint-disable-line no-unused-expressions + kubelessDeploy.deployFunction().then(() => { + expect(serverlessWithFunction.cli.log.lastCall.args).to.be.eql( + [ + 'The function myFunction already exists. ' + + 'Redeploy it usign --force or executing "sls deploy function -f myFunction".', + ] + ); + }) ).to.be.fulfilled; - expect(serverlessWithFunction.cli.log.lastCall.args).to.be.eql( - [ - 'The function myFunction already exists. ' + - 'Redeploy it usign --force or executing "sls deploy function -f myFunction".', - ] - ); - } finally { - serverlessWithFunction.cli.log.restore(); - } return result; }); it('should deploy a function triggered by a topic', () => { const serverlessWithCustomNamespace = _.cloneDeep(serverlessWithFunction); - serverlessWithCustomNamespace.service.functions[functionName].events = [{ trigger: 'topic' }]; + serverlessWithCustomNamespace.service.functions[functionName].events = [{ + trigger: 'topic', + }]; kubelessDeploy = instantiateKubelessDeploy( handlerFile, depsFile, serverlessWithCustomNamespace ); - thirdPartyResources = mocks.thirdPartyResources(kubelessDeploy); + mocks.createDeploymentNocks(config.clusters[0].cluster.server, functionName, { + deps: '', + function: functionText, + handler: serverlessWithFunction.service.functions[functionName].handler, + runtime: serverlessWithFunction.service.provider.runtime, + type: 'PubSub', + }); const result = expect( // eslint-disable-line no-unused-expressions kubelessDeploy.deployFunction() ).to.be.fulfilled; - expect(thirdPartyResources.ns.functions.post.calledOnce).to.be.eql(true); - expect( - thirdPartyResources.ns.functions.post.firstCall.args[0].body.spec.type - ).to.be.eql('PubSub'); - expect( - thirdPartyResources.ns.functions.post.firstCall.args[0].body.spec.topic - ).to.be.eql('topic'); - expect( - thirdPartyResources.ns.functions.post.firstCall.args[1] - ).to.be.a('function'); return result; }); it('should deploy a function with a description', () => { @@ -580,15 +451,16 @@ describe('KubelessDeploy', () => { depsFile, serverlessWithCustomNamespace ); - thirdPartyResources = mocks.thirdPartyResources(kubelessDeploy); + mocks.createDeploymentNocks(config.clusters[0].cluster.server, functionName, { + deps: '', + function: functionText, + handler: serverlessWithFunction.service.functions[functionName].handler, + runtime: serverlessWithFunction.service.provider.runtime, + type: 'HTTP', + }, { description: desc }); const result = expect( // eslint-disable-line no-unused-expressions kubelessDeploy.deployFunction() ).to.be.fulfilled; - expect(thirdPartyResources.ns.functions.post.calledOnce).to.be.eql(true); - expect( - thirdPartyResources.ns.functions.post - .firstCall.args[0].body.metadata.annotations['kubeless.serverless.com/description'] - ).to.be.eql(desc); return result; }); it('should deploy a function with labels', () => { @@ -600,14 +472,16 @@ describe('KubelessDeploy', () => { depsFile, serverlessWithCustomNamespace ); - thirdPartyResources = mocks.thirdPartyResources(kubelessDeploy); + mocks.createDeploymentNocks(config.clusters[0].cluster.server, functionName, { + deps: '', + function: functionText, + handler: serverlessWithFunction.service.functions[functionName].handler, + runtime: serverlessWithFunction.service.provider.runtime, + type: 'HTTP', + }, { labels }); const result = expect( // eslint-disable-line no-unused-expressions kubelessDeploy.deployFunction() ).to.be.fulfilled; - expect(thirdPartyResources.ns.functions.post.calledOnce).to.be.eql(true); - expect( - thirdPartyResources.ns.functions.post.firstCall.args[0].body.metadata.labels - ).to.be.eql(labels); return result; }); it('should deploy a function with environment variables', () => { @@ -619,19 +493,24 @@ describe('KubelessDeploy', () => { depsFile, serverlessWithEnvVars ); - thirdPartyResources = mocks.thirdPartyResources(kubelessDeploy); + mocks.createDeploymentNocks(config.clusters[0].cluster.server, functionName, { + deps: '', + function: functionText, + handler: serverlessWithFunction.service.functions[functionName].handler, + runtime: serverlessWithFunction.service.provider.runtime, + type: 'HTTP', + template: { + spec: { + containers: [{ + name: functionName, + env: [{ name: 'VAR', value: 'test' }, { name: 'OTHER_VAR', value: 'test2' }], + }], + }, + }, + }); const result = expect( // eslint-disable-line no-unused-expressions kubelessDeploy.deployFunction() ).to.be.fulfilled; - expect(thirdPartyResources.ns.functions.post.calledOnce).to.be.eql(true); - expect( - thirdPartyResources.ns.functions.post.firstCall.args[0].body.spec.template.spec.containers - ).to.be.eql([ - { - name: functionName, - env: [{ name: 'VAR', value: 'test' }, { name: 'OTHER_VAR', value: 'test2' }], - }, - ]); return result; }); it('should deploy a function with a memory limit', () => { @@ -642,23 +521,27 @@ describe('KubelessDeploy', () => { depsFile, serverlessWithEnvVars ); - thirdPartyResources = mocks.thirdPartyResources(kubelessDeploy); - const result = expect( // eslint-disable-line no-unused-expressions - kubelessDeploy.deployFunction() - ).to.be.fulfilled; - expect(thirdPartyResources.ns.functions.post.calledOnce).to.be.eql(true); - expect( - thirdPartyResources.ns.functions.post.firstCall.args[0].body.spec.template.spec.containers - ).to.be.eql([ - { - name: functionName, - resources: { - limits: { memory: '128Mi' }, - requests: { memory: '128Mi' }, + mocks.createDeploymentNocks(config.clusters[0].cluster.server, functionName, { + deps: '', + function: functionText, + handler: serverlessWithFunction.service.functions[functionName].handler, + runtime: serverlessWithFunction.service.provider.runtime, + type: 'HTTP', + template: { + spec: { + containers: [{ + name: functionName, + resources: { + limits: { memory: '128Mi' }, + requests: { memory: '128Mi' }, + }, + }], }, }, - ]); - return result; + }); + return expect( // eslint-disable-line no-unused-expressions + kubelessDeploy.deployFunction() + ).to.be.fulfilled; }); it('should deploy a function with a memory limit (in the provider definition)', () => { const serverlessWithEnvVars = _.cloneDeep(serverlessWithFunction); @@ -668,23 +551,27 @@ describe('KubelessDeploy', () => { depsFile, serverlessWithEnvVars ); - thirdPartyResources = mocks.thirdPartyResources(kubelessDeploy); - const result = expect( // eslint-disable-line no-unused-expressions - kubelessDeploy.deployFunction() - ).to.be.fulfilled; - expect(thirdPartyResources.ns.functions.post.calledOnce).to.be.eql(true); - expect( - thirdPartyResources.ns.functions.post.firstCall.args[0].body.spec.template.spec.containers - ).to.be.eql([ - { - name: functionName, - resources: { - limits: { memory: '128Gi' }, - requests: { memory: '128Gi' }, + mocks.createDeploymentNocks(config.clusters[0].cluster.server, functionName, { + deps: '', + function: functionText, + handler: serverlessWithFunction.service.functions[functionName].handler, + runtime: serverlessWithFunction.service.provider.runtime, + type: 'HTTP', + template: { + spec: { + containers: [{ + name: functionName, + resources: { + limits: { memory: '128Gi' }, + requests: { memory: '128Gi' }, + }, + }], }, }, - ]); - return result; + }); + return expect( // eslint-disable-line no-unused-expressions + kubelessDeploy.deployFunction() + ).to.be.fulfilled; }); it('should deploy a function in a specific path', () => { const serverlessWithCustomPath = _.cloneDeep(serverlessWithFunction); @@ -696,35 +583,22 @@ describe('KubelessDeploy', () => { depsFile, serverlessWithCustomPath ); - thirdPartyResources = mocks.thirdPartyResources(kubelessDeploy); - const extensions = mocks.extensions(kubelessDeploy); - const result = expect( // eslint-disable-line no-unused-expressions - kubelessDeploy.deployFunction().then(() => { - expect(extensions.ns.ingress.post.firstCall.args[0].body).to.be.eql({ - kind: 'Ingress', - metadata: { - name: `ingress-${functionName}`, - labels: { function: functionName }, - annotations: - { - 'kubernetes.io/ingress.class': 'nginx', - 'ingress.kubernetes.io/rewrite-target': '/', - }, - }, - spec: { - rules: [{ - host: '1.2.3.4.nip.io', - http: { - paths: [{ - path: '/test', - backend: { serviceName: functionName, servicePort: 8080 }, - }], - }, - }], - }, - }); - })).to.be.fulfilled; - return result; + mocks.createDeploymentNocks(config.clusters[0].cluster.server, functionName, { + deps: '', + function: functionText, + handler: serverlessWithFunction.service.functions[functionName].handler, + runtime: serverlessWithFunction.service.provider.runtime, + type: 'HTTP', + }); + mocks.createIngressNocks( + config.clusters[0].cluster.server, + functionName, + '1.2.3.4.nip.io', + '/test' + ); + return expect( // eslint-disable-line no-unused-expressions + kubelessDeploy.deployFunction() + ).to.be.fulfilled; }); it('should deploy a function with a specific hostname', () => { const serverlessWithCustomPath = _.cloneDeep(serverlessWithFunction); @@ -737,35 +611,22 @@ describe('KubelessDeploy', () => { depsFile, serverlessWithCustomPath ); - thirdPartyResources = mocks.thirdPartyResources(kubelessDeploy); - const extensions = mocks.extensions(kubelessDeploy); - const result = expect( // eslint-disable-line no-unused-expressions - kubelessDeploy.deployFunction().then(() => { - expect(extensions.ns.ingress.post.firstCall.args[0].body).to.be.eql({ - kind: 'Ingress', - metadata: { - name: `ingress-${functionName}`, - labels: { function: functionName }, - annotations: - { - 'kubernetes.io/ingress.class': 'nginx', - 'ingress.kubernetes.io/rewrite-target': '/', - }, - }, - spec: { - rules: [{ - host: 'test.com', - http: { - paths: [{ - path: '/', - backend: { serviceName: functionName, servicePort: 8080 }, - }], - }, - }], - }, - }); - })).to.be.fulfilled; - return result; + mocks.createDeploymentNocks(config.clusters[0].cluster.server, functionName, { + deps: '', + function: functionText, + handler: serverlessWithFunction.service.functions[functionName].handler, + runtime: serverlessWithFunction.service.provider.runtime, + type: 'HTTP', + }); + mocks.createIngressNocks( + config.clusters[0].cluster.server, + functionName, + 'test.com', + '/' + ); + return expect( // eslint-disable-line no-unused-expressions + kubelessDeploy.deployFunction() + ).to.be.fulfilled; }); it('should deploy a function with a specific hostname and path', () => { @@ -778,35 +639,22 @@ describe('KubelessDeploy', () => { depsFile, serverlessWithCustomPath ); - thirdPartyResources = mocks.thirdPartyResources(kubelessDeploy); - const extensions = mocks.extensions(kubelessDeploy); - const result = expect( // eslint-disable-line no-unused-expressions - kubelessDeploy.deployFunction().then(() => { - expect(extensions.ns.ingress.post.firstCall.args[0].body).to.be.eql({ - kind: 'Ingress', - metadata: { - name: `ingress-${functionName}`, - labels: { function: functionName }, - annotations: - { - 'kubernetes.io/ingress.class': 'nginx', - 'ingress.kubernetes.io/rewrite-target': '/', - }, - }, - spec: { - rules: [{ - host: 'test.com', - http: { - paths: [{ - path: '/test', - backend: { serviceName: functionName, servicePort: 8080 }, - }], - }, - }], - }, - }); - })).to.be.fulfilled; - return result; + mocks.createDeploymentNocks(config.clusters[0].cluster.server, functionName, { + deps: '', + function: functionText, + handler: serverlessWithFunction.service.functions[functionName].handler, + runtime: serverlessWithFunction.service.provider.runtime, + type: 'HTTP', + }); + mocks.createIngressNocks( + config.clusters[0].cluster.server, + functionName, + 'test.com', + '/test' + ); + return expect( // eslint-disable-line no-unused-expressions + kubelessDeploy.deployFunction() + ).to.be.fulfilled; }); it('should deploy a function with a specific hostname (in the function section)', () => { const serverlessWithCustomPath = _.cloneDeep(serverlessWithFunction); @@ -818,35 +666,22 @@ describe('KubelessDeploy', () => { depsFile, serverlessWithCustomPath ); - thirdPartyResources = mocks.thirdPartyResources(kubelessDeploy); - const extensions = mocks.extensions(kubelessDeploy); - const result = expect( // eslint-disable-line no-unused-expressions - kubelessDeploy.deployFunction().then(() => { - expect(extensions.ns.ingress.post.firstCall.args[0].body).to.be.eql({ - kind: 'Ingress', - metadata: { - name: `ingress-${functionName}`, - labels: { function: functionName }, - annotations: - { - 'kubernetes.io/ingress.class': 'nginx', - 'ingress.kubernetes.io/rewrite-target': '/', - }, - }, - spec: { - rules: [{ - host: 'test.com', - http: { - paths: [{ - path: '/test', - backend: { serviceName: functionName, servicePort: 8080 }, - }], - }, - }], - }, - }); - })).to.be.fulfilled; - return result; + mocks.createDeploymentNocks(config.clusters[0].cluster.server, functionName, { + deps: '', + function: functionText, + handler: serverlessWithFunction.service.functions[functionName].handler, + runtime: serverlessWithFunction.service.provider.runtime, + type: 'HTTP', + }); + mocks.createIngressNocks( + config.clusters[0].cluster.server, + functionName, + 'test.com', + '/test' + ); + return expect( // eslint-disable-line no-unused-expressions + kubelessDeploy.deployFunction() + ).to.be.fulfilled; }); it('should deploy a function in a specific path (with a custom namespace)', () => { const serverlessWithCustomPath = _.cloneDeep(serverlessWithFunction); @@ -859,14 +694,23 @@ describe('KubelessDeploy', () => { depsFile, serverlessWithCustomPath ); - thirdPartyResources = mocks.thirdPartyResources(kubelessDeploy); - mocks.extensions(kubelessDeploy, 'custom'); - const result = expect( // eslint-disable-line no-unused-expressions - kubelessDeploy.deployFunction().then(() => { - expect(kubelessDeploy.getExtensions.firstCall.args[0].namespace).to.be.eql('custom'); - }) + mocks.createDeploymentNocks(config.clusters[0].cluster.server, functionName, { + deps: '', + function: functionText, + handler: serverlessWithFunction.service.functions[functionName].handler, + runtime: serverlessWithFunction.service.provider.runtime, + type: 'HTTP', + }, { namespace: 'custom' }); + mocks.createIngressNocks( + config.clusters[0].cluster.server, + functionName, + '1.2.3.4.nip.io', + '/test', + { namespace: 'custom' } + ); + return expect( // eslint-disable-line no-unused-expressions + kubelessDeploy.deployFunction() ).to.be.fulfilled; - return result; }); it('should deploy a function in a specific path (with a relative path)', () => { const serverlessWithCustomPath = _.cloneDeep(serverlessWithFunction); @@ -878,20 +722,30 @@ describe('KubelessDeploy', () => { depsFile, serverlessWithCustomPath ); - thirdPartyResources = mocks.thirdPartyResources(kubelessDeploy); - const extensions = mocks.extensions(kubelessDeploy); - const result = expect( // eslint-disable-line no-unused-expressions - kubelessDeploy.deployFunction().then(() => { - expect( - extensions.ns.ingress.post.firstCall.args[0].body.spec.rules[0].http.paths[0].path - ).to.be.eql('/test'); - })).to.be.fulfilled; - return result; + mocks.createDeploymentNocks(config.clusters[0].cluster.server, functionName, { + deps: '', + function: functionText, + handler: serverlessWithFunction.service.functions[functionName].handler, + runtime: serverlessWithFunction.service.provider.runtime, + type: 'HTTP', + }); + mocks.createIngressNocks( + config.clusters[0].cluster.server, + functionName, + '1.2.3.4.nip.io', + '/test' + ); + return expect( // eslint-disable-line no-unused-expressions + kubelessDeploy.deployFunction() + ).to.be.fulfilled; }); it('should fail if a deployment returns an error code', () => { - thirdPartyResources.ns.functions.post.callsFake((data, ff) => { - ff({ code: 500, message: 'Internal server error' }); - }); + nock(config.clusters[0].cluster.server) + .get('/apis/k8s.io/v1/namespaces/default/functions') + .reply(200, []); + nock(config.clusters[0].cluster.server) + .post('/apis/k8s.io/v1/namespaces/default/functions') + .reply(500, 'Internal server error'); return expect( // eslint-disable-line no-unused-expressions kubelessDeploy.deployFunction() ).to.be.eventually.rejectedWith( @@ -919,34 +773,54 @@ describe('KubelessDeploy', () => { }); const functionsDeployed = []; kubelessDeploy = instantiateKubelessDeploy(handlerFile, depsFile, serverlessWithFunctions); - thirdPartyResources = mocks.thirdPartyResources(kubelessDeploy); - thirdPartyResources.ns.functions.post.onFirstCall().callsFake((data, ff) => { - functionsDeployed.push(data.body.metadata.name); - ff(null, { statusCode: 200 }); + const funcSpec = { + deps: '', + function: functionText, + handler: serverlessWithFunction.service.functions[functionName].handler, + runtime: serverlessWithFunction.service.provider.runtime, + type: 'HTTP', + }; + const postReply = (uri, req) => { + functionsDeployed.push(req.metadata.name); + }; + // Call for myFunction1 + mocks.createDeploymentNocks(config.clusters[0].cluster.server, 'myFunction1', funcSpec, { + postReply, }); - thirdPartyResources.ns.functions.post.onSecondCall().callsFake((data, ff) => { - ff({ code: 500, message: 'Internal server error' }); - }); - thirdPartyResources.ns.functions.post.onThirdCall().callsFake((data, ff) => { - functionsDeployed.push(data.body.metadata.name); - ff(null, { statusCode: 200 }); + // Call for myFunction2 + nock(config.clusters[0].cluster.server) + .post('/apis/k8s.io/v1/namespaces/default/functions', { + apiVersion: 'k8s.io/v1', + kind: 'Function', + metadata: { name: 'myFunction2', namespace: 'default' }, + spec: funcSpec, + }) + .reply(500, 'Internal server error'); + // Call for myFunction3 + nock(config.clusters[0].cluster.server) + .post('/apis/k8s.io/v1/namespaces/default/functions', { + apiVersion: 'k8s.io/v1', + kind: 'Function', + metadata: { name: 'myFunction3', namespace: 'default' }, + spec: funcSpec, + }) + .reply(200, postReply); + + kubelessDeploy.deployFunction().catch(e => { + expect(e).to.be.eql( + 'Found errors while deploying the given functions:\n' + + 'Error: Unable to deploy the function myFunction2. Received:\n' + + ' Code: 500\n' + + ' Message: Internal server error' + ); + }).then(() => { + expect(functionsDeployed).to.be.eql(['myFunction1', 'myFunction3']); }); - const result = expect( - kubelessDeploy.deployFunction() - ).to.be.eventually.rejectedWith( - 'Found errors while deploying the given functions:\n' + - 'Error: Unable to deploy the function myFunction2. Received:\n' + - ' Code: 500\n' + - ' Message: Internal server error' - ); - expect(functionsDeployed).to.be.eql(['myFunction1', 'myFunction3']); - return result; }); it('should deploy a function using the given package', () => { kubelessDeploy = new KubelessDeploy(serverlessWithFunction, { package: path.join(cwd, 'package.zip'), }); - thirdPartyResources = mocks.thirdPartyResources(kubelessDeploy); fs.writeFileSync(path.join(path.join(cwd, 'package.zip')), ''); sinon.stub(kubelessDeploy, 'loadZip').returns({ then: (f) => f({ @@ -958,48 +832,37 @@ describe('KubelessDeploy', () => { }), }), }); - mocks.kubeConfig(cwd); - const result = expect(kubelessDeploy.deployFunction()).to.be.fulfilled; - expect(thirdPartyResources.ns.functions.post.calledOnce).to.be.eql(true); - expect(thirdPartyResources.ns.functions.post.firstCall.args[0].body).to.be.eql( - { apiVersion: 'k8s.io/v1', - kind: 'Function', - metadata: { name: functionName, namespace: 'default' }, - spec: - { deps: '', - function: 'different function content', - handler: 'function.hello', - runtime: 'python2.7', - type: 'HTTP' } } - ); - expect( - thirdPartyResources.ns.functions.post.firstCall.args[1] - ).to.be.a('function'); - return result; + mocks.createDeploymentNocks(config.clusters[0].cluster.server, functionName, { + deps: '', + function: 'different function content', + handler: serverlessWithFunction.service.functions[functionName].handler, + runtime: serverlessWithFunction.service.provider.runtime, + type: 'HTTP', + }); + return expect( // eslint-disable-line no-unused-expressions + kubelessDeploy.deployFunction() + ).to.be.fulfilled; }); it('should deploy a function with requirements', () => { kubelessDeploy = new KubelessDeploy(serverlessWithFunction); - thirdPartyResources = mocks.thirdPartyResources(kubelessDeploy); fs.writeFileSync(depsFile, 'request'); - const result = expect( - kubelessDeploy.deployFunction().then(() => { - expect( - thirdPartyResources.ns.functions.post.calledOnce - ).to.be.eql(true); - expect( - thirdPartyResources.ns.functions.post.firstCall.args[0].body.spec.deps - ).to.be.eql('request'); - fs.unlinkSync(path.join(cwd, 'requirements.txt'), 'request'); - }) + mocks.createDeploymentNocks(config.clusters[0].cluster.server, functionName, { + deps: 'request', + function: 'function code', + handler: serverlessWithFunction.service.functions[functionName].handler, + runtime: serverlessWithFunction.service.provider.runtime, + type: 'HTTP', + }); + return expect( // eslint-disable-line no-unused-expressions + kubelessDeploy.deployFunction().then(() => { + fs.unlinkSync(path.join(cwd, 'requirements.txt'), 'request'); + }) ).to.be.fulfilled; - return result; }); it('should deploy a function with requirements using the given package', () => { kubelessDeploy = new KubelessDeploy(serverlessWithFunction, { package: path.join(cwd, 'package.zip'), }); - thirdPartyResources = mocks.thirdPartyResources(kubelessDeploy); - mocks.kubeConfig(cwd); fs.writeFileSync(path.join(path.join(cwd, 'package.zip')), ''); sinon.stub(kubelessDeploy, 'loadZip').returns({ then: (f) => f({ @@ -1011,96 +874,105 @@ describe('KubelessDeploy', () => { }), }), }); - const result = expect(kubelessDeploy.deployFunction()).to.be.fulfilled; - expect( - thirdPartyResources.ns.functions.post.calledOnce - ).to.be.eql(true); - expect( - thirdPartyResources.ns.functions.post.firstCall.args[0].body.spec.deps - ).to.be.eql('request'); - return result; - }); - it('should redeploy a function', () => { - thirdPartyResources.ns.functions.get.callsFake((ff) => { - ff(null, { - items: [{ - metadata: { - name: functionName, - labels: { function: functionName }, - creationTimestamp: moment(), - }, - status: { - containerStatuses: [{ - ready: true, - restartCount: 0, - state: 'Ready', - }], - }, - spec: { - deps: '', - function: 'function code', - handler: 'function.hello', - runtime: 'python2.7', - type: 'HTTP', - }, - }], - }); + mocks.createDeploymentNocks(config.clusters[0].cluster.server, functionName, { + deps: 'request', + function: 'different function content', + handler: serverlessWithFunction.service.functions[functionName].handler, + runtime: serverlessWithFunction.service.provider.runtime, + type: 'HTTP', }); - fs.writeFileSync(handlerFile, 'function code modified'); - kubelessDeploy.options.force = true; - let result = null; - result = expect( // eslint-disable-line no-unused-expressions + return expect( // eslint-disable-line no-unused-expressions kubelessDeploy.deployFunction() ).to.be.fulfilled; - expect(thirdPartyResources.ns.functions().put.calledOnce).to.be.eql(true); - expect(thirdPartyResources.ns.functions().put.firstCall.args[0].body).to.be.eql( - { + }); + it('should redeploy a function', () => { + fs.writeFileSync(handlerFile, 'function code modified'); + mocks.createDeploymentNocks(config.clusters[0].cluster.server, functionName, { + deps: 'request', + function: 'function code modified', + handler: serverlessWithFunction.service.functions[functionName].handler, + runtime: serverlessWithFunction.service.provider.runtime, + type: 'HTTP', + }, { + existingFunctions: [{ + metadata: { + name: functionName, + labels: { function: functionName }, + }, + spec: { + deps: 'request', + function: 'function code', + handler: serverlessWithFunction.service.functions[functionName].handler, + runtime: serverlessWithFunction.service.provider.runtime, + type: 'HTTP', + }, + }], + }); + nock(config.clusters[0].cluster.server) + .put(`/apis/k8s.io/v1/namespaces/default/functions/${functionName}`, { apiVersion: 'k8s.io/v1', kind: 'Function', - metadata: { name: functionName, namespace: 'default' }, - spec: - { + metadata: { name: 'myFunction', namespace: 'default' }, + spec: { deps: '', function: 'function code modified', handler: 'function.hello', runtime: 'python2.7', type: 'HTTP', }, - } - ); - expect(thirdPartyResources.ns.functions().put.firstCall.args[1]).to.be.a('function'); + }) + .reply(200, 'OK'); + kubelessDeploy.options.force = true; + let result = null; + result = expect( // eslint-disable-line no-unused-expressions + kubelessDeploy.deployFunction().then(() => { + expect(nock.pendingMocks()).to.contain( + `POST ${config.clusters[0].cluster.server}/apis/k8s.io/v1/namespaces/default/functions` + ); + expect(nock.pendingMocks()).to.not.contain( + `POST ${config.clusters[0].cluster.server}/apis/k8s.io/v1/namespaces/default/functions/${functionName}` // eslint-disable-line max-len + ); + }) + ).to.be.fulfilled; return result; }); it('should fail if a redeployment returns an error code', () => { - thirdPartyResources.ns.functions.get.callsFake((ff) => { - ff(null, { - items: [{ - metadata: { - name: functionName, - labels: { function: functionName }, - creationTimestamp: moment(), - }, - status: { - containerStatuses: [{ - ready: true, - restartCount: 0, - state: 'Ready', - }], - }, - spec: { - deps: '', - function: 'function code', - handler: 'function.hello', - runtime: 'python2.7', - type: 'HTTP', - }, - }], - }); - }); - thirdPartyResources.ns.functions().put.callsFake((data, ff) => { - ff({ code: 500, message: 'Internal server error' }); - }); fs.writeFileSync(handlerFile, 'function code modified'); + mocks.createDeploymentNocks(config.clusters[0].cluster.server, functionName, { + deps: 'request', + function: 'function code modified', + handler: serverlessWithFunction.service.functions[functionName].handler, + runtime: serverlessWithFunction.service.provider.runtime, + type: 'HTTP', + }, { + existingFunctions: [{ + metadata: { + name: functionName, + labels: { function: functionName }, + }, + spec: { + deps: 'request', + function: 'function code', + handler: serverlessWithFunction.service.functions[functionName].handler, + runtime: serverlessWithFunction.service.provider.runtime, + type: 'HTTP', + }, + }], + }); + nock(config.clusters[0].cluster.server) + .put(`/apis/k8s.io/v1/namespaces/default/functions/${functionName}`, { + apiVersion: 'k8s.io/v1', + kind: 'Function', + metadata: { name: 'myFunction', namespace: 'default' }, + spec: { + deps: '', + function: 'function code modified', + handler: 'function.hello', + runtime: 'python2.7', + type: 'HTTP', + }, + }) + .reply(500, 'Internal server error'); kubelessDeploy.options.force = true; return expect( // eslint-disable-line no-unused-expressions kubelessDeploy.deployFunction() @@ -1111,39 +983,5 @@ describe('KubelessDeploy', () => { ' Message: Internal server error' ); }); - it('should not try to redeploy a new ingress controller', () => { - thirdPartyResources.ns.functions.get.callsFake((ff) => { - ff(null, { - items: [{ - metadata: { - name: functionName, - labels: { function: functionName }, - creationTimestamp: moment(), - }, - status: { - containerStatuses: [{ - ready: true, - restartCount: 0, - state: 'Ready', - }], - }, - spec: { - deps: '', - function: 'function code', - handler: 'function.hello', - runtime: 'python2.7', - type: 'HTTP', - }, - }], - }); - }); - sinon.stub(kubelessDeploy, 'getExtensions'); - const result = expect( // eslint-disable-line no-unused-expressions - kubelessDeploy.deployFunction().then(() => { - expect(kubelessDeploy.getExtensions.callCount).to.be.eql(0); - }) - ).to.be.fulfilled; - return result; - }); }); }); diff --git a/test/kubelessDeployFunction.test.js b/test/kubelessDeployFunction.test.js index 3d21a48..a899ffc 100644 --- a/test/kubelessDeployFunction.test.js +++ b/test/kubelessDeployFunction.test.js @@ -22,6 +22,8 @@ const expect = require('chai').expect; const fs = require('fs'); const mocks = require('./lib/mocks'); const moment = require('moment'); +const nock = require('nock'); +const os = require('os'); const path = require('path'); const sinon = require('sinon'); @@ -51,13 +53,14 @@ function instantiateKubelessDeploy(handlerFile, depsFile, serverlessWithFunction return f(null); } }) }) ); - sinon.stub(kubelessDeployFunction, 'waitForDeployment'); return kubelessDeployFunction; } describe('KubelessDeployFunction', () => { describe('#deploy', () => { let cwd = null; + let clock = null; + let config = null; let handlerFile = null; let depsFile = null; const serverlessWithFunction = _.defaultsDeep({}, serverless, { @@ -76,69 +79,73 @@ describe('KubelessDeployFunction', () => { }; let kubelessDeployFunction = null; - let thirdPartyResources = null; - before(() => { - cwd = mocks.kubeConfig(); - }); beforeEach(() => { + cwd = path.join(os.tmpdir(), moment().valueOf().toString()); + fs.mkdirSync(cwd); + config = mocks.kubeConfig(cwd); handlerFile = path.join(cwd, 'function.py'); fs.writeFileSync(handlerFile, 'function code'); depsFile = path.join(cwd, 'requirements.txt'); + setInterval(() => { + clock.tick(2001); + }, 100); + clock = sinon.useFakeTimers(); kubelessDeployFunction = instantiateKubelessDeploy( handlerFile, depsFile, serverlessWithFunction ); - thirdPartyResources = mocks.thirdPartyResources(kubelessDeployFunction); + mocks.createDeploymentNocks(config.clusters[0].cluster.server, functionName, { + deps: 'request', + function: 'function code modified', + handler: serverlessWithFunction.service.functions[functionName].handler, + runtime: serverlessWithFunction.service.provider.runtime, + type: 'HTTP', + }, { + existingFunctions: [{ + metadata: { + name: functionName, + labels: { function: functionName }, + }, + spec: { + deps: 'request', + function: 'function code', + handler: serverlessWithFunction.service.functions[functionName].handler, + runtime: serverlessWithFunction.service.provider.runtime, + type: 'HTTP', + }, + }], + }); + nock(config.clusters[0].cluster.server) + .put(`/apis/k8s.io/v1/namespaces/default/functions/${functionName}`, { + apiVersion: 'k8s.io/v1', + kind: 'Function', + metadata: { name: 'myFunction', namespace: 'default' }, + spec: { + deps: '', + function: 'function code', + handler: 'function.hello', + runtime: 'python2.7', + type: 'HTTP', + }, + }) + .reply(200, 'OK'); }); - after(() => { + afterEach(() => { mocks.restoreKubeConfig(cwd); + nock.cleanAll(); + clock.restore(); }); - it('should deploy the chosen function', () => { - const result = expect( // eslint-disable-line no-unused-expressions - kubelessDeployFunction.deployFunction() - ).to.be.fulfilled; - expect(thirdPartyResources.ns.functions.post.calledOnce).to.be.eql(true); - expect( - thirdPartyResources.ns.functions.post.firstCall.args[0].body.metadata.name - ).to.be.eql(functionName); - return result; - }); - it('should redeploy only the chosen function', () => { - kubelessDeployFunction.getThirdPartyResources().ns.functions.get.callsFake((ff) => { - ff(null, { - items: [{ - metadata: { - name: functionName, - labels: { function: functionName }, - creationTimestamp: moment(), - }, - status: { - containerStatuses: [{ - ready: true, - restartCount: 0, - state: 'Ready', - }], - }, - spec: { - deps: '', - function: 'previous function code', - handler: 'function.hello', - runtime: 'python2.7', - type: 'HTTP', - }, - }], - }); - }); - const result = expect( // eslint-disable-line no-unused-expressions + it('should deploy the chosen function', () => expect( kubelessDeployFunction.deployFunction() - ).to.be.fulfilled; - expect(thirdPartyResources.ns.functions().put.calledOnce).to.be.eql(true); - expect( - thirdPartyResources.ns.functions().put.firstCall.args[0].body.metadata.name - ).to.be.eql(functionName); - return result; - }); + ).to.be.fulfilled); + it('should redeploy only the chosen function', () => expect( + kubelessDeployFunction.deployFunction().then(() => { + expect(kubelessDeployFunction.serverless.service.functions).to.be.eql( + { myFunction: { handler: 'function.hello' } } + ); + }) + ).to.be.fulfilled); }); }); diff --git a/test/kubelessInfo.test.js b/test/kubelessInfo.test.js index d786d78..69e8e0f 100644 --- a/test/kubelessInfo.test.js +++ b/test/kubelessInfo.test.js @@ -70,15 +70,10 @@ describe('KubelessInfo', () => { describe('#validate', () => { it('prints a message if an unsupported option is given', () => { const kubelessInfo = new KubelessInfo(serverless, { region: 'us-east1' }); - sinon.stub(serverless.cli, 'log'); - try { - expect(() => kubelessInfo.validate()).to.not.throw(); - expect(serverless.cli.log.firstCall.args).to.be.eql( + expect(() => kubelessInfo.validate()).to.not.throw(); + expect(serverless.cli.log.firstCall.args).to.be.eql( ['Warning: Option region is not supported for the kubeless plugin'] ); - } finally { - serverless.cli.log.restore(); - } }); }); function mockGetCalls(functions, functionModif) { @@ -181,7 +176,7 @@ describe('KubelessInfo', () => { `Handler: ${f}.hello\n` + 'Runtime: python2.7\n' + 'Trigger: HTTP\n' + - 'Dependencies: '; + 'Dependencies: \n'; } diff --git a/test/kubelessInvoke.test.js b/test/kubelessInvoke.test.js index 3bdd0d9..fa05e15 100644 --- a/test/kubelessInvoke.test.js +++ b/test/kubelessInvoke.test.js @@ -20,10 +20,8 @@ const _ = require('lodash'); const BbPromise = require('bluebird'); const chaiAsPromised = require('chai-as-promised'); const expect = require('chai').expect; -const fs = require('fs'); const helpers = require('../lib/helpers'); const loadKubeConfig = require('./lib/load-kube-config'); -const path = require('path'); const request = require('request'); const sinon = require('sinon'); @@ -68,36 +66,12 @@ describe('KubelessInvoke', () => { }); }); describe('#validate', () => { - it('throws an error if the given path with the data does not exists', () => { - const kubelessInvoke = new KubelessInvoke(serverless, { path: '/not-exist' }); - expect(() => kubelessInvoke.validate()).to.throw( - 'The file you provided does not exist' - ); - }); - it('throws an error if the given path with the data does not contain a valid JSON', () => { - const filePath = path.join('/tmp/data.json'); - fs.writeFileSync(filePath, 'not-a-json'); - try { - const kubelessInvoke = new KubelessInvoke(serverless, { path: '/tmp/data.json' }); - expect(() => kubelessInvoke.validate()).to.throw( - 'Unable to parse data given in the arguments: \n' + - 'Unexpected token o in JSON at position 1' - ); - } finally { - fs.unlinkSync('/tmp/data.json'); - } - }); it('prints a message if an unsupported option is given', () => { const kubelessInvoke = new KubelessInvoke(serverless, { region: 'us-east1', function: func }); - sinon.stub(serverless.cli, 'log'); - try { - expect(() => kubelessInvoke.validate()).to.not.throw(); - expect(serverless.cli.log.firstCall.args).to.be.eql( + expect(() => kubelessInvoke.validate()).to.not.throw(); + expect(serverless.cli.log.firstCall.args).to.be.eql( ['Warning: Option region is not supported for the kubeless plugin'] ); - } finally { - serverless.cli.log.restore(); - } }); it('throws an error if the function provider is not present in the description', () => { const kubelessInvoke = new KubelessInvoke(serverless, { @@ -120,6 +94,15 @@ describe('KubelessInvoke', () => { request.get.restore(); helpers.loadKubeConfig.restore(); }); + it('throws an error if the given path with the data does not exists', () => { + const kubelessInvoke = new KubelessInvoke(serverless, { + function: func, + path: '/not-exist', + }); + expect(() => kubelessInvoke.invokeFunction()).to.throw( + 'The file you provided does not exist' + ); + }); it('calls the API end point with the correct arguments (without data)', () => { const kubelessInvoke = new KubelessInvoke(serverless, { function: func, diff --git a/test/kubelessLogs.test.js b/test/kubelessLogs.test.js index 1425f40..3f801ed 100644 --- a/test/kubelessLogs.test.js +++ b/test/kubelessLogs.test.js @@ -72,15 +72,10 @@ describe('KubelessLogs', () => { describe('#validate', () => { it('prints a message if an unsupported option is given', () => { const kubelessLogs = new KubelessLogs(serverless, { region: 'us-east1', function: f }); - sinon.stub(serverless.cli, 'log'); - try { - expect(() => kubelessLogs.validate()).to.not.throw(); - expect(serverless.cli.log.firstCall.args).to.be.eql( - ['Warning: Option region is not supported for the kubeless plugin'] - ); - } finally { - serverless.cli.log.restore(); - } + expect(() => kubelessLogs.validate()).to.not.throw(); + expect(serverless.cli.log.firstCall.args).to.be.eql( + ['Warning: Option region is not supported for the kubeless plugin'] + ); }); it('throws an error if the function provider is not present in the description', () => { const kubelessInvoke = new KubelessLogs(serverless, { diff --git a/test/kubelessRemove.test.js b/test/kubelessRemove.test.js index 6aebb23..2024208 100644 --- a/test/kubelessRemove.test.js +++ b/test/kubelessRemove.test.js @@ -76,30 +76,28 @@ describe('KubelessRemove', () => { describe('#validate', () => { it('prints a message if an unsupported option is given', () => { const kubelessRemove = new KubelessRemove(serverless, { region: 'us-east1' }); - sinon.stub(serverless.cli, 'log'); - try { - expect(() => kubelessRemove.validate()).to.not.throw(); - expect(serverless.cli.log.firstCall.args).to.be.eql( - ['Warning: Option region is not supported for the kubeless plugin'] - ); - } finally { - serverless.cli.log.restore(); - } + expect(() => kubelessRemove.validate()).to.not.throw(); + expect(serverless.cli.log.firstCall.args).to.be.eql( + ['Warning: Option region is not supported for the kubeless plugin'] + ); }); }); describe('#remove', () => { let cwd = null; - const serverlessWithFunction = _.defaultsDeep({}, serverless, { - service: { - functions: { - myFunction: { - handler: 'function.hello', + let serverlessWithFunction = null; + let kubelessRemove = null; + + beforeEach(() => { + serverlessWithFunction = _.defaultsDeep({}, serverless, { + service: { + functions: { + myFunction: { + handler: 'function.hello', + }, }, }, - }, - }); - let kubelessRemove = new KubelessRemove(serverlessWithFunction); - beforeEach(() => { + }); + kubelessRemove = new KubelessRemove(serverlessWithFunction); cwd = path.join(os.tmpdir(), moment().valueOf().toString()); fs.mkdirSync(cwd); fs.writeFileSync(path.join(cwd, 'function.py'), 'function code'); @@ -138,17 +136,12 @@ describe('KubelessRemove', () => { Api.ThirdPartyResources.prototype.delete.callsFake((data, ff) => { ff({ code: 404 }); }); - sinon.stub(serverlessWithFunction.cli, 'log'); - try { - expect( // eslint-disable-line no-unused-expressions - kubelessRemove.removeFunction(cwd) - ).to.be.fulfilled; - expect(serverlessWithFunction.cli.log.lastCall.args).to.be.eql( - ['The function myFunction doesn\'t exist. Skipping removal.'] - ); - } finally { - serverlessWithFunction.cli.log.restore(); - } + expect( // eslint-disable-line no-unused-expressions + kubelessRemove.removeFunction(cwd) + ).to.be.fulfilled; + expect(serverlessWithFunction.cli.log.lastCall.args).to.be.eql( + ['The function myFunction doesn\'t exist. Skipping removal.'] + ); }); it('should fail if a removal returns an error code', () => { Api.ThirdPartyResources.prototype.delete.callsFake((data, ff) => { diff --git a/test/lib/mocks.js b/test/lib/mocks.js index 5173597..5cfb040 100644 --- a/test/lib/mocks.js +++ b/test/lib/mocks.js @@ -3,10 +3,11 @@ const _ = require('lodash'); const fs = require('fs'); const moment = require('moment'); -const os = require('os'); +const nock = require('nock'); const path = require('path'); const rm = require('./rm'); const sinon = require('sinon'); +const yaml = require('js-yaml'); function thirdPartyResources(kubelessDeploy, namespace) { const put = sinon.stub().callsFake((body, callback) => { @@ -55,9 +56,7 @@ function extensions(kubelessDeploy, namespace) { return result; } -function kubeConfig() { - const cwd = path.join(os.tmpdir(), moment().valueOf().toString()); - fs.mkdirSync(cwd); +function kubeConfig(cwd) { fs.mkdirSync(path.join(cwd, '.kube')); fs.writeFileSync( path.join(cwd, '.kube/config'), @@ -81,7 +80,7 @@ function kubeConfig() { ' password: password1234\n' ); process.env.HOME = cwd; - return cwd; + return yaml.safeLoad(fs.readFileSync(path.join(cwd, '.kube/config'))); } const previousEnv = _.cloneDeep(process.env); @@ -91,9 +90,86 @@ function restoreKubeConfig(cwd) { process.env = _.cloneDeep(previousEnv); } + +function createDeploymentNocks(endpoint, func, funcSpec, options) { + const opts = _.defaults({}, options, { + namespace: 'default', + existingFunctions: [], + description: null, + labels: null, + postReply: 'OK', + }); + const postBody = { + apiVersion: 'k8s.io/v1', + kind: 'Function', + metadata: { name: func, namespace: opts.namespace }, + spec: funcSpec, + }; + if (opts.description) { + postBody.metadata.annotations = { + 'kubeless.serverless.com/description': opts.description, + }; + } + if (opts.labels) { + postBody.metadata.labels = opts.labels; + } + nock(endpoint) + .persist() + .get(`/apis/k8s.io/v1/namespaces/${opts.namespace}/functions`) + .reply(200, { items: opts.existingFunctions }); + nock(endpoint) + .post(`/apis/k8s.io/v1/namespaces/${opts.namespace}/functions`, postBody) + .reply(200, opts.postReply); + nock(endpoint) + .persist() + .get('/api/v1/pods') + .reply(200, { + items: [{ + metadata: { + name: func, + labels: { function: func }, + creationTimestamp: moment().add('60', 's'), + }, + spec: funcSpec, + status: { + containerStatuses: [{ ready: true, restartCount: 0 }], + }, + }], + }); +} + +function createIngressNocks(endpoint, func, hostname, p, options) { + const opts = _.defaults({}, options, { + namespace: 'default', + }); + nock(endpoint) + .post(`/apis/extensions/v1beta1/namespaces/${opts.namespace}/ingresses`, { + kind: 'Ingress', + metadata: { + name: 'ingress-myFunction', + labels: { function: func }, + annotations: { + 'kubernetes.io/ingress.class': 'nginx', + 'ingress.kubernetes.io/rewrite-target': '/', + }, + }, + spec: { + rules: [{ + host: hostname, + http: { + paths: [{ path: p, backend: { serviceName: func, servicePort: 8080 } }], + }, + }], + }, + }) + .reply(200, 'OK'); +} + module.exports = { thirdPartyResources, extensions, kubeConfig, restoreKubeConfig, + createDeploymentNocks, + createIngressNocks, }; diff --git a/test/lib/serverless.js b/test/lib/serverless.js index 302b1ea..e641516 100644 --- a/test/lib/serverless.js +++ b/test/lib/serverless.js @@ -18,11 +18,12 @@ const _ = require('lodash'); const fs = require('fs'); +const sinon = require('sinon'); class CLI { constructor() { - this.log = function () {}; - this.consoleLog = function () {}; + this.log = sinon.stub(); + this.consoleLog = () => {}; } }