From f131a6d8b3214cd982349f8772c526542326d6ec Mon Sep 17 00:00:00 2001 From: Adriano Raiano Date: Fri, 29 Apr 2016 10:57:06 +0200 Subject: [PATCH] push push2.cloud to GitHub --- .editorconfig | 10 + .eslintrc | 93 ++++++ .gitignore | 42 +++ LICENSE | 201 +++++++++++++ README.md | 3 + fns/associateRoute.js | 48 ++++ fns/bindService.js | 115 ++++++++ fns/createApp.js | 94 +++++++ fns/createRoute.js | 111 ++++++++ fns/createServiceInstance.js | 83 ++++++ fns/deleteApp.js | 43 +++ fns/deleteRoute.js | 33 +++ fns/deleteServiceInstance.js | 33 +++ fns/disassociateRoute.js | 46 +++ fns/getActualDeploymentConfig.js | 75 +++++ fns/getAppInfo.js | 31 ++ fns/getAppInstances.js | 34 +++ fns/getAppRoutes.js | 29 ++ fns/getDomains.js | 18 ++ fns/getInfo.js | 23 ++ fns/getLogWebSocket.js | 35 +++ fns/getOrgInfo.js | 26 ++ fns/getRoutes.js | 28 ++ fns/getServiceBinding.js | 24 ++ fns/getServiceBindings.js | 25 ++ fns/getServiceInstance.js | 31 ++ fns/getServicePlans.js | 27 ++ fns/getServices.js | 29 ++ fns/getSpaceInfo.js | 30 ++ fns/getSpaceSummary.js | 22 ++ fns/graceRequest.js | 99 +++++++ fns/init.js | 114 ++++++++ fns/login.js | 49 ++++ fns/mapRoute.js | 15 + fns/pushApp.js | 12 + fns/refreshLogin.js | 43 +++ fns/restageApp.js | 45 +++ fns/restartApp.js | 11 + fns/scaleApp.js | 9 + fns/setEnv.js | 9 + fns/stageApp.js | 59 ++++ fns/startApp.js | 6 + fns/startAppAndWaitForCompleteMessage.js | 36 +++ fns/startAppAndWaitForInstances.js | 28 ++ fns/startAppAndWaitForMessages.js | 34 +++ fns/stopApp.js | 6 + fns/tailAppLogsAndWaitFor.js | 57 ++++ fns/unbindService.js | 64 +++++ fns/unmapRoute.js | 12 + fns/updateApp.js | 67 +++++ fns/uploadApp.js | 62 ++++ fns/verifyInstancesNotCrashing.js | 51 ++++ fns/waitForAllInstancesRunning.js | 51 ++++ index.js | 74 +++++ lib/convertSize.js | 16 ++ lib/parseCFSummaryIntoDeploymentConfig.js | 83 ++++++ lib/zipGenerator.js | 39 +++ package.json | 37 +++ test/can.basically.talk.with.cf.js | 286 +++++++++++++++++++ test/can.be.used.with.promises.js | 42 +++ test/can.work.completely.without.guids.js | 327 ++++++++++++++++++++++ test/mocha.opts | 1 + test/sampleApp/.cfignore | 1 + test/sampleApp/package.json | 11 + test/sampleApp/server.js | 5 + test/sampleApp/special.file | 1 + test/sampleApp/test.sh | 1 + 67 files changed, 3305 insertions(+) create mode 100644 .editorconfig create mode 100644 .eslintrc create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 fns/associateRoute.js create mode 100644 fns/bindService.js create mode 100644 fns/createApp.js create mode 100644 fns/createRoute.js create mode 100644 fns/createServiceInstance.js create mode 100644 fns/deleteApp.js create mode 100644 fns/deleteRoute.js create mode 100644 fns/deleteServiceInstance.js create mode 100644 fns/disassociateRoute.js create mode 100644 fns/getActualDeploymentConfig.js create mode 100644 fns/getAppInfo.js create mode 100644 fns/getAppInstances.js create mode 100644 fns/getAppRoutes.js create mode 100644 fns/getDomains.js create mode 100644 fns/getInfo.js create mode 100644 fns/getLogWebSocket.js create mode 100644 fns/getOrgInfo.js create mode 100644 fns/getRoutes.js create mode 100644 fns/getServiceBinding.js create mode 100644 fns/getServiceBindings.js create mode 100644 fns/getServiceInstance.js create mode 100644 fns/getServicePlans.js create mode 100644 fns/getServices.js create mode 100644 fns/getSpaceInfo.js create mode 100644 fns/getSpaceSummary.js create mode 100644 fns/graceRequest.js create mode 100644 fns/init.js create mode 100644 fns/login.js create mode 100644 fns/mapRoute.js create mode 100644 fns/pushApp.js create mode 100644 fns/refreshLogin.js create mode 100644 fns/restageApp.js create mode 100644 fns/restartApp.js create mode 100644 fns/scaleApp.js create mode 100644 fns/setEnv.js create mode 100644 fns/stageApp.js create mode 100644 fns/startApp.js create mode 100644 fns/startAppAndWaitForCompleteMessage.js create mode 100644 fns/startAppAndWaitForInstances.js create mode 100644 fns/startAppAndWaitForMessages.js create mode 100644 fns/stopApp.js create mode 100644 fns/tailAppLogsAndWaitFor.js create mode 100644 fns/unbindService.js create mode 100644 fns/unmapRoute.js create mode 100644 fns/updateApp.js create mode 100644 fns/uploadApp.js create mode 100644 fns/verifyInstancesNotCrashing.js create mode 100644 fns/waitForAllInstancesRunning.js create mode 100644 index.js create mode 100644 lib/convertSize.js create mode 100644 lib/parseCFSummaryIntoDeploymentConfig.js create mode 100644 lib/zipGenerator.js create mode 100644 package.json create mode 100644 test/can.basically.talk.with.cf.js create mode 100644 test/can.be.used.with.promises.js create mode 100644 test/can.work.completely.without.guids.js create mode 100644 test/mocha.opts create mode 100644 test/sampleApp/.cfignore create mode 100644 test/sampleApp/package.json create mode 100644 test/sampleApp/server.js create mode 100644 test/sampleApp/special.file create mode 100755 test/sampleApp/test.sh diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..19a41cf --- /dev/null +++ b/.editorconfig @@ -0,0 +1,10 @@ +# EditorConfig is awesome: http://EditorConfig.org +root = true + +[*.{js,json}] +end_of_line = lf +insert_final_newline = true +charset = utf-8 +indent_style = space +indent_size = 2 +trim_trailing_whitespace = true diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..e03a475 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,93 @@ +env: + node: true + es6: true + +rules: + # Possible Errors + # http://eslint.org/docs/rules/#possible-errors + comma-dangle: [2, "only-multiline"] + no-control-regex: 2 + no-debugger: 2 + no-dupe-args: 2 + no-dupe-keys: 2 + no-duplicate-case: 2 + no-empty-character-class: 2 + no-ex-assign: 2 + no-extra-boolean-cast: 2 + no-extra-parens: [2, "functions"] + no-extra-semi: 2 + no-func-assign: 2 + no-invalid-regexp: 2 + no-irregular-whitespace: 2 + no-negated-in-lhs: 2 + no-obj-calls: 2 + no-proto: 2 + no-unexpected-multiline: 2 + no-unreachable: 2 + use-isnan: 2 + valid-typeof: 2 + + # Best Practices + # http://eslint.org/docs/rules/#best-practices + no-fallthrough: 2 + no-octal: 2 + no-redeclare: 2 + no-self-assign: 2 + no-unused-labels: 2 + + # Strict Mode + # http://eslint.org/docs/rules/#strict-mode + # strict: [2, "global"] + + # Variables + # http://eslint.org/docs/rules/#variables + no-delete-var: 2 + no-undef: 2 + no-unused-vars: [2, {vars: all, args: none}] + + # Node.js and CommonJS + # http://eslint.org/docs/rules/#nodejs-and-commonjs + no-mixed-requires: 2 + no-new-require: 2 + no-path-concat: 2 + no-restricted-modules: [2, "sys", "_linklist"] + + # Stylistic Issues + # http://eslint.org/docs/rules/#stylistic-issues + comma-spacing: 2 + eol-last: 2 + indent: [2, 2, {SwitchCase: 1}] + keyword-spacing: 2 + # max-len: [2, 80, 2] + new-parens: 2 + no-mixed-spaces-and-tabs: 2 + # no-multiple-empty-lines: [2, {max: 2}] + no-trailing-spaces: 2 + quotes: [2, "single", "avoid-escape"] + semi: 2 + space-before-blocks: [2, "always"] + space-before-function-paren: [2, "never"] + space-in-parens: [2, "never"] + space-infix-ops: 2 + space-unary-ops: 2 + + # ECMAScript 6 + # http://eslint.org/docs/rules/#ecmascript-6 + arrow-parens: [2, "always"] + arrow-spacing: [2, {"before": true, "after": true}] + constructor-super: 2 + no-class-assign: 2 + no-confusing-arrow: 2 + no-const-assign: 2 + no-dupe-class-members: 2 + no-new-symbol: 2 + no-this-before-super: 2 + prefer-const: 2 + +globals: + describe: false + it: false + before: false + after: false + beforeEach: false + afterEach: false diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..635d1b5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,42 @@ +# IntelliJ IDEA files +*.iml +*.ipr +*.ids +*.iws +.idea/ + +# Eclipse files +.project +.metadata +local.properties +.classpath +.settings/ +.loadpath +bin/ + +# OS dependent files +.DS_Store +.Spotlight-V100 +.Trashes +Thumbs.db +Desktop.ini +*~ + +# Ignore Vagrant generated directories +.vagrant + +# Ignore various Node.js related directories and files +node_modules +node_modules/**/* + +private_node_modules +private_node_modules/**/* + +.config +.node-gyp +.npm +tmp +.pm2 + +__workspace +__manifests diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..5e0fd33 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, +and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by +the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all +other entities that control, are controlled by, or are under common +control with that entity. For the purposes of this definition, +"control" means (i) the power, direct or indirect, to cause the +direction or management of such entity, whether by contract or +otherwise, or (ii) ownership of fifty percent (50%) or more of the +outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity +exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, +including but not limited to software source code, documentation +source, and configuration files. + +"Object" form shall mean any form resulting from mechanical +transformation or translation of a Source form, including but +not limited to compiled object code, generated documentation, +and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or +Object form, made available under the License, as indicated by a +copyright notice that is included in or attached to the work +(an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object +form, that is based on (or derived from) the Work and for which the +editorial revisions, annotations, elaborations, or other modifications +represent, as a whole, an original work of authorship. For the purposes +of this License, Derivative Works shall not include works that remain +separable from, or merely link (or bind by name) to the interfaces of, +the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including +the original version of the Work and any modifications or additions +to that Work or Derivative Works thereof, that is intentionally +submitted to Licensor for inclusion in the Work by the copyright owner +or by an individual or Legal Entity authorized to submit on behalf of +the copyright owner. For the purposes of this definition, "submitted" +means any form of electronic, verbal, or written communication sent +to the Licensor or its representatives, including but not limited to +communication on electronic mailing lists, source code control systems, +and issue tracking systems that are managed by, or on behalf of, the +Licensor for the purpose of discussing and improving the Work, but +excluding communication that is conspicuously marked or otherwise +designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity +on behalf of whom a Contribution has been received by Licensor and +subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of +this License, each Contributor hereby grants to You a perpetual, +worldwide, non-exclusive, no-charge, royalty-free, irrevocable +copyright license to reproduce, prepare Derivative Works of, +publicly display, publicly perform, sublicense, and distribute the +Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of +this License, each Contributor hereby grants to You a perpetual, +worldwide, non-exclusive, no-charge, royalty-free, irrevocable +(except as stated in this section) patent license to make, have made, +use, offer to sell, sell, import, and otherwise transfer the Work, +where such license applies only to those patent claims licensable +by such Contributor that are necessarily infringed by their +Contribution(s) alone or by combination of their Contribution(s) +with the Work to which such Contribution(s) was submitted. If You +institute patent litigation against any entity (including a +cross-claim or counterclaim in a lawsuit) alleging that the Work +or a Contribution incorporated within the Work constitutes direct +or contributory patent infringement, then any patent licenses +granted to You under this License for that Work shall terminate +as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the +Work or Derivative Works thereof in any medium, with or without +modifications, and in Source or Object form, provided that You +meet the following conditions: + +(a) You must give any other recipients of the Work or +Derivative Works a copy of this License; and + +(b) You must cause any modified files to carry prominent notices +stating that You changed the files; and + +(c) You must retain, in the Source form of any Derivative Works +that You distribute, all copyright, patent, trademark, and +attribution notices from the Source form of the Work, +excluding those notices that do not pertain to any part of +the Derivative Works; and + +(d) If the Work includes a "NOTICE" text file as part of its +distribution, then any Derivative Works that You distribute must +include a readable copy of the attribution notices contained +within such NOTICE file, excluding those notices that do not +pertain to any part of the Derivative Works, in at least one +of the following places: within a NOTICE text file distributed +as part of the Derivative Works; within the Source form or +documentation, if provided along with the Derivative Works; or, +within a display generated by the Derivative Works, if and +wherever such third-party notices normally appear. The contents +of the NOTICE file are for informational purposes only and +do not modify the License. You may add Your own attribution +notices within Derivative Works that You distribute, alongside +or as an addendum to the NOTICE text from the Work, provided +that such additional attribution notices cannot be construed +as modifying the License. + +You may add Your own copyright statement to Your modifications and +may provide additional or different license terms and conditions +for use, reproduction, or distribution of Your modifications, or +for any such Derivative Works as a whole, provided Your use, +reproduction, and distribution of the Work otherwise complies with +the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, +any Contribution intentionally submitted for inclusion in the Work +by You to the Licensor shall be under the terms and conditions of +this License, without any additional terms or conditions. +Notwithstanding the above, nothing herein shall supersede or modify +the terms of any separate license agreement you may have executed +with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade +names, trademarks, service marks, or product names of the Licensor, +except as required for reasonable and customary use in describing the +origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or +agreed to in writing, Licensor provides the Work (and each +Contributor provides its Contributions) on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +implied, including, without limitation, any warranties or conditions +of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A +PARTICULAR PURPOSE. You are solely responsible for determining the +appropriateness of using or redistributing the Work and assume any +risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, +whether in tort (including negligence), contract, or otherwise, +unless required by applicable law (such as deliberate and grossly +negligent acts) or agreed to in writing, shall any Contributor be +liable to You for damages, including any direct, indirect, special, +incidental, or consequential damages of any character arising as a +result of this License or out of the use or inability to use the +Work (including but not limited to damages for loss of goodwill, +work stoppage, computer failure or malfunction, or any and all +other commercial damages or losses), even if such Contributor +has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing +the Work or Derivative Works thereof, You may choose to offer, +and charge a fee for, acceptance of support, warranty, indemnity, +or other liability obligations and/or rights consistent with this +License. However, in accepting such obligations, You may act only +on Your own behalf and on Your sole responsibility, not on behalf +of any other Contributor, and only if You agree to indemnify, +defend, and hold each Contributor harmless for any liability +incurred by, or claims asserted against, such Contributor by reason +of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + +To apply the Apache License to your work, attach the following +boilerplate notice, with the fields enclosed by brackets "{}" +replaced with your own identifying information. (Don't include +the brackets!) The text should be enclosed in the appropriate +comment syntax for the file format. We also recommend that a +file or class name and description of purpose be included on the +same "printed page" as the copyright notice for easier +identification within third-party archives. + +Copyright {yyyy} {name of copyright owner} + +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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..dd497b4 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# push2cloud-cf-adapter + +This repository is part of the push2cloud project. For contribution guidelines, issues, and general documentation, visit the main [push2cloud project page](https://github.com/push2cloud/push2cloud). diff --git a/fns/associateRoute.js b/fns/associateRoute.js new file mode 100644 index 0000000..d26578e --- /dev/null +++ b/fns/associateRoute.js @@ -0,0 +1,48 @@ +const _ = require('lodash'); + +module.exports = (api) => { + const assoc = (options, callback) => { + options = options || {}; + + if (!options.appGuid && options.app && api.actualDeploymentConfig) { + const a = _.find(api.actualDeploymentConfig.apps, { name: options.app }); + if (a) { + options.appGuid = a.guid; + } + } + + if (!options.routeGuid && options.hostname && options.domain && api.actualDeploymentConfig) { + var r = _.find(api.actualDeploymentConfig.routes, { hostname: options.hostname, domain: options.domain }); + if (r) { + options.routeGuid = r.guid; + } + } + + if (!options.appGuid) { + return callback(new Error('Please provide an appGuid! \n' + JSON.stringify(options, null, 2))); + } + + if (!options.routeGuid) { + return callback(new Error('Please provide an routeGuid! \n' + JSON.stringify(options, null, 2))); + } + + api.graceRequest({ + method: 'PUT', + uri: `/v2/apps/${options.appGuid}/routes/${options.routeGuid}` + }, (err, response, result) => { + if (err) { + return callback(err); + } + + if (api.actualDeploymentConfig) { + var r = _.find(api.actualDeploymentConfig.routes, { guid: options.routeGuid }); + r.appGuid = options.appGuid; + r.app = _.findKey(api.actualDeploymentConfig.apps, { guid: options.appGuid }); + } + + callback(null, result); + }); + }; + + return assoc; +}; diff --git a/fns/bindService.js b/fns/bindService.js new file mode 100644 index 0000000..e2e88d1 --- /dev/null +++ b/fns/bindService.js @@ -0,0 +1,115 @@ +const _ = require('lodash'); +const debug = require('debug')('push2cloud-cf-adapter:bindService'); + +module.exports = (api) => { + return (options, callback) => { + options = options || {}; + + options.parameters = options.parameters || {}; + + if (!api.spaceGuid) { + return callback(new Error('Please provide a space! \n' + JSON.stringify(options, null, 2))); + } + + if (!options.appGuid && options.app && api.actualDeploymentConfig) { + var a = _.find(api.actualDeploymentConfig.apps, { name: options.app }); + if (a) { + options.appGuid = a.guid; + } + } + + if (!options.serviceInstanceGuid && options.service && api.actualDeploymentConfig) { + var s = _.find(api.actualDeploymentConfig.services, { name: options.service }); + if (s) { + options.serviceInstanceGuid = s.guid; + } + } + + if (!options.serviceInstanceGuid) { + return callback(new Error('Please provide a serviceInstanceGuid! \n' + JSON.stringify(options, null, 2))); + } + + if (!options.appGuid) { + return callback(new Error('Please provide an appGuid! \n' + JSON.stringify(options, null, 2))); + } + + api.graceRequest({ + method: 'POST', + uri: '/v2/service_bindings', + json: { + service_instance_guid: options.serviceInstanceGuid, + app_guid: options.appGuid, + parameters: options.parameters + } + }, (err, response, result) => { + if (result && result.code === 90003) { + if (!_.find(api.actualDeploymentConfig.serviceBindings, { appGuid: options.appGuid, + serviceInstanceGuid: options.serviceInstanceGuid })) { + api.getServiceBindings(options, (err, results) => { + if (err) { + return callback(err); + } + + if (!results || results.length === 0) { + debug('Missing service binding infos!', results); + return callback(new Error('Missing service binding infos!')); + } + + var sb = _.find(results, (item) => { + return item.entity.app_guid === options.appGuid + && item.entity.service_instance_guid === options.serviceInstanceGuid; + }); + + if (!sb) { + debug('Missing service binding infos!', results); + return callback(new Error('Missing service binding infos!')); + } + + var service, app; + if (api.actualDeploymentConfig) { + service = _.find(api.actualDeploymentConfig.services, { guid: options.serviceInstanceGuid }); + app = _.find(api.actualDeploymentConfig.apps, { guid: options.appGuid }); + } + + api.actualDeploymentConfig.serviceBindings.push({ + guid: sb.metadata.guid, + appGuid: options.appGuid, + serviceInstanceGuid: options.serviceInstanceGuid, + service: service.name, + app: app.name + }); + + callback(null, sb); + }); + return; + } else { + return callback(new Error(result.description)); + } + } + + if (err && !(result && result.code === 90003)) { + return callback(err); + } + + if (!result || !result.metadata) { + return callback(new Error('Not expected result! \n' + JSON.stringify(result, null, 2))); + } + + var service, app; + if (api.actualDeploymentConfig) { + service = _.find(api.actualDeploymentConfig.services, { guid: options.serviceInstanceGuid }); + app = _.find(api.actualDeploymentConfig.apps, { guid: options.appGuid }); + } + + api.actualDeploymentConfig.serviceBindings.push({ + guid: result.metadata.guid, + appGuid: options.appGuid, + serviceInstanceGuid: options.serviceInstanceGuid, + service: service.name, + app: app.name + }); + + callback(null, result); + }); + }; +}; diff --git a/fns/createApp.js b/fns/createApp.js new file mode 100644 index 0000000..0f5738f --- /dev/null +++ b/fns/createApp.js @@ -0,0 +1,94 @@ +const _ = require('lodash'); +const convertSize = require('../lib/convertSize'); +const semver = require('semver'); +const debug = require('debug')('push2cloud-cf-adapter:createApp'); + +module.exports = (api) => { + const create = (options, callback) => { + const defaults = { + buildpack: null, + command: null, + env: {}, + disk: '256M', + memory: '256M', + instances: 1 + }; + + _.defaults(options, defaults); + + if (!api.spaceGuid) { + return callback(new Error('Please provide an space! \n' + JSON.stringify(options, null, 2))); + } + + if (!options.name) { + return callback(new Error('Please provide a name! \n' + JSON.stringify(options, null, 2))); + } + + function updateActualDeploymentConfig(result) { + var appIdx = _.findIndex(api.actualDeploymentConfig.apps, { name: result.entity.name }); + if (appIdx < 0) appIdx = api.actualDeploymentConfig.apps.length; + + var indexOfVersion = result.entity.name.lastIndexOf('-'); + var unversionedName = result.entity.name; + if (indexOfVersion >= 0) unversionedName = result.entity.name.substring(0, indexOfVersion); + + api.actualDeploymentConfig.apps[appIdx] = { + name: result.entity.name, + unversionedName: unversionedName, + guid: result.metadata.guid, + instances: result.entity.instances, + memory: result.entity.memory, + disk: result.entity.disk_quota, + state: result.entity.state, + version: semver.valid(result.entity.name.substring(result.entity.name.lastIndexOf('-') + 1)) ? result.entity.name.substring(result.entity.name.lastIndexOf('-') + 1) : undefined, + package_state: result.entity.package_state + }; + } + + api.graceRequest({ + method: 'POST', + uri: '/v2/apps', + json : { + name: options.name, + space_guid: api.spaceGuid, + buildpack: options.buildpack, + command: options.command, + environment_json: options.env, + disk_quota: convertSize(options.disk) || defaults.disk, + memory: convertSize(options.memory) || defaults.memory, + instances: convertSize(options.instances) || defaults.instances + } + }, (err, response, result) => { + if (result && result.code === 100002) { + if (!_.find(api.actualDeploymentConfig.apps, { name: options.name })) { + api.getAppInfo(options, (err, result) => { + if (err) { + return callback(err); + } + + if (!result || !result.metadata || !result.metadata.guid) { + debug('Missing app infos!'); + return callback(new Error('Missing app infos!')); + } + + updateActualDeploymentConfig(result); + callback(null, result); + }); + return; + } else { + return callback(new Error(result.description)); + } + } + + if (err && !(result && result.code === 10002)) { + return callback(err); + } + + updateActualDeploymentConfig(result); + + callback(null, result); + }); + }; + + return create; +}; diff --git a/fns/createRoute.js b/fns/createRoute.js new file mode 100644 index 0000000..ec19188 --- /dev/null +++ b/fns/createRoute.js @@ -0,0 +1,111 @@ +const _ = require('lodash'); +const debug = require('debug')('push2cloud-cf-adapter:createRoute'); + +module.exports = (api) => { + const create = (options, callback) => { + options = options || {}; + + options.parameters = options.parameters || {}; + + if (!api.spaceGuid) { + return callback(new Error('Please provide a space! \n' + JSON.stringify(options, null, 2))); + } + + if (!options.domainGuid && !options.domain) { + return callback(new Error('Please provide a domain! \n' + JSON.stringify(options, null, 2))); + } + + if (!options.hostname) { + return callback(new Error('Please provide a host! \n' + JSON.stringify(options, null, 2))); + } + + if (!options.domainGuid) { + var dom = _.find(api.domains, { entity: { name: options.domain } }); + + if (!dom) { + return callback(new Error('Please provide a domainGuid! \n' + JSON.stringify(options, null, 2))); + } + + options.domainGuid = dom.metadata.guid; + } + + api.graceRequest({ + method: 'POST', + uri: '/v2/routes', + json: { + domain_guid: options.domainGuid, + space_guid: api.spaceGuid, + host: options.hostname + } + }, (err, response, result) => { + if (result && result.code === 210003) { + if (!_.find(api.actualDeploymentConfig.routes, { domainGuid: options.domainGuid, + hostname: options.hostname })) { + api.getRoutes(options, (err, results) => { + if (err) { + return callback(err); + } + + if (!results || results.length === 0) { + debug('Missing route infos!'); + return callback(new Error('Missing route infos!')); + } + + var r = _.find(results, (item) => { + return item.entity.domain_guid === options.domainGuid + && item.entity.host === options.hostname; + }); + + if (!r) { + debug('Missing route infos!'); + return callback(new Error('Missing route infos!')); + } + + var dName; + if (api.actualDeploymentConfig) { + var d = _.find(api.actualDeploymentConfig.domains, { guid: options.domainGuid }); + if (d) dName = d.name; + } + + api.actualDeploymentConfig.routes.push({ + guid: r.metadata.guid, + domainGuid: options.domainGuid, + appGuid: undefined, + hostname: options.hostname, + domain: dName, + app: undefined + }); + + callback(null, r); + }); + return; + } else { + return callback(new Error(result.description)); + } + } + + if (err && !(result && result.code === 210003)) { + return callback(err); + } + + var dName; + if (api.actualDeploymentConfig) { + var d = _.find(api.actualDeploymentConfig.domains, { guid: options.domainGuid }); + if (d) dName = d.name; + } + + api.actualDeploymentConfig.routes.push({ + guid: result.metadata.guid, + domainGuid: options.domainGuid, + appGuid: undefined, + hostname: options.hostname, + domain: dName, + app: undefined + }); + + callback(null, result); + }); + }; + + return create; +}; diff --git a/fns/createServiceInstance.js b/fns/createServiceInstance.js new file mode 100644 index 0000000..508cfd9 --- /dev/null +++ b/fns/createServiceInstance.js @@ -0,0 +1,83 @@ +const _ = require('lodash'); +const debug = require('debug')('push2cloud-cf-adapter:createServiceInstance'); + +module.exports = (api) => { + const createServiceInstance = (options, callback) => { + var defaults = { + tags: [], + parameters: {} + }; + + _.defaults(options, defaults); + + if (!api.spaceGuid) { + return callback(new Error('Please provide a space! \n' + JSON.stringify(options, null, 2))); + } + + if (!options.name) { + return callback(new Error('Please provide a name! \n' + JSON.stringify(options, null, 2))); + } + + const servicePlan = api.getServicePlan(options.type, options.plan); + + if (!servicePlan) { + return callback(new Error(`No servicePlan found with service ${options.type} and plan ${options.plan}!`)); + } + + api.graceRequest({ + method: 'POST', + uri: '/v2/service_instances', + json : { + name: options.name, + space_guid: api.spaceGuid, + service_plan_guid: servicePlan.metadata.guid, + parameters: options.parameters, + tags: options.tags + }, + qs: { + accepts_incomplete: options.acceptsIncomplete === undefined || options.acceptsIncomplete === null ? true : options.acceptsIncomplete + } + }, (err, response, result) => { + if (result && result.code === 60002) { + if (!_.find(api.actualDeploymentConfig.services, { name: options.name })) { + api.getServiceInstance(options, (err, result) => { + if (err) { + return callback(err); + } + + if (!result || !result.metadata || !result.metadata.guid) { + debug('Missing service instance infos!'); + return callback(new Error('Missing service instance infos!')); + } + + api.actualDeploymentConfig.services.push({ + name: options.name, + guid: result.metadata.guid, + type: options.type, + plan: options.plan + }); + + callback(null, result); + }); + return; + } else { + return callback(new Error(result.description)); + } + } + + if (err && !(result && result.code === 60002)) { + return callback(err); + } + + api.actualDeploymentConfig.services.push({ + name: options.name, + guid: result.metadata.guid, + type: options.type, + plan: options.plan + }); + + callback(null, result); + }); + }; + return createServiceInstance; +}; diff --git a/fns/deleteApp.js b/fns/deleteApp.js new file mode 100644 index 0000000..193f3ff --- /dev/null +++ b/fns/deleteApp.js @@ -0,0 +1,43 @@ +const _ = require('lodash'); + +module.exports = (api) => { + return (options, callback) => { + options = options || {}; + + if (!options.appGuid && options.name && api.actualDeploymentConfig) { + var a = _.find(api.actualDeploymentConfig.apps, { name: options.name }); + if (a) { + options.appGuid = a.guid; + } + } + + if (!options.appGuid) { + return callback(new Error('Please provide an appGuid! \n' + JSON.stringify(options, null, 2))); + } + + api.graceRequest({ + method: 'DELETE', + uri: `/v2/apps/${options.appGuid}` + }, (err, response, result) => { + var appIdx; + if (result && result.code === 100004) { + appIdx = _.findIndex(api.actualDeploymentConfig.apps, { guid: options.appGuid }); + if (appIdx) { + delete api.actualDeploymentConfig.apps[appIdx]; + return callback(null); + } else { + return callback(new Error(result.description)); + } + } + + if (err && !(result && result.code === 100004)) { + return callback(err); + } + + appIdx = _.findIndex(api.actualDeploymentConfig.apps, { guid: options.appGuid }); + delete api.actualDeploymentConfig.apps[appIdx]; + + callback(null, result); + }); + }; +}; diff --git a/fns/deleteRoute.js b/fns/deleteRoute.js new file mode 100644 index 0000000..801fd26 --- /dev/null +++ b/fns/deleteRoute.js @@ -0,0 +1,33 @@ +const _ = require('lodash'); + +module.exports = (api) => { + return (options, callback) => { + options = options || {}; + + if (!options.routeGuid && options.hostname && options.domain && api.actualDeploymentConfig) { + var r = _.find(api.actualDeploymentConfig.routes, { hostname: options.hostname, domain: options.domain }); + if (r) { + options.routeGuid = r.guid; + } + } + + if (!options.routeGuid) { + return callback(new Error('Please provide a routeGuid \n' + JSON.stringify(options, null, 2))); + } + + api.graceRequest({ + method: 'DELETE', + uri: `/v2/routes/${options.routeGuid}` + }, (err, response, result) => { + if (err) { + return callback(err); + } + + if (api.actualDeploymentConfig) { + _.remove(api.actualDeploymentConfig.routes, { guid: options.routeGuid }); + } + + callback(null, result); + }); + }; +}; diff --git a/fns/deleteServiceInstance.js b/fns/deleteServiceInstance.js new file mode 100644 index 0000000..07a81d4 --- /dev/null +++ b/fns/deleteServiceInstance.js @@ -0,0 +1,33 @@ +const _ = require('lodash'); + +module.exports = (api) => { + return (options, callback) => { + options = options || {}; + + if (!options.serviceInstanceGuid && options.name && api.actualDeploymentConfig) { + var s = _.find(api.actualDeploymentConfig.services, { name: options.name }); + if (s) { + options.serviceInstanceGuid = s.guid; + } + } + + if (!options.serviceInstanceGuid) { + return callback(new Error('Please provide a serviceInstanceGuid! \n' + JSON.stringify(options, null, 2))); + } + + api.graceRequest({ + method: 'DELETE', + uri: `/v2/service_instances/${options.serviceInstanceGuid}` + }, (err, response, result) => { + if (err) { + return callback(err); + } + + if (api.actualDeploymentConfig) { + _.remove(api.actualDeploymentConfig.services, (service) => service.guid = options.serviceInstanceGuid); + } + + callback(null, result); + }); + }; +}; diff --git a/fns/disassociateRoute.js b/fns/disassociateRoute.js new file mode 100644 index 0000000..5b0134f --- /dev/null +++ b/fns/disassociateRoute.js @@ -0,0 +1,46 @@ +const _ = require('lodash'); + +module.exports = (api) => { + return (options, callback) => { + options = options || {}; + + if (!options.appGuid && options.app && api.actualDeploymentConfig) { + var a = _.find(api.actualDeploymentConfig.apps, { name: options.app }); + if (a) { + options.appGuid = a.guid; + } + } + + if (!options.routeGuid && options.hostname && options.domain && api.actualDeploymentConfig) { + var r = _.find(api.actualDeploymentConfig.routes, { hostname: options.hostname, domain: options.domain }); + if (r) { + options.routeGuid = r.guid; + } + } + + if (!options.appGuid) { + return callback(new Error('Please provide an appGuid! \n' + JSON.stringify(options, null, 2))); + } + + if (!options.routeGuid) { + return callback(new Error('Please provide an routeGuid! \n' + JSON.stringify(options, null, 2))); + } + + api.graceRequest({ + method: 'DELETE', + uri: `/v2/apps/${options.appGuid}/routes/${options.routeGuid}` + }, (err, response, result) => { + if (err) { + return callback(err); + } + + if (api.actualDeploymentConfig) { + var r = _.find(api.actualDeploymentConfig.routes, { guid: options.routeGuid }); + r.appGuid = undefined; + r.app = undefined; + } + + callback(null, result); + }); + }; +}; diff --git a/fns/getActualDeploymentConfig.js b/fns/getActualDeploymentConfig.js new file mode 100644 index 0000000..8ac2695 --- /dev/null +++ b/fns/getActualDeploymentConfig.js @@ -0,0 +1,75 @@ +const async = require('async'); +const _ = require('lodash'); +const parseCFSummaryIntoDeploymentConfig = require('../lib/parseCFSummaryIntoDeploymentConfig'); + +module.exports = (api) => { + return (done) => { + api.getSpaceSummary((err, summary) => { + if (err) return done(err); + + if (!summary) return done(null); + + const deploymentConfig = parseCFSummaryIntoDeploymentConfig(summary); + + async.each(deploymentConfig.services, (service, callback) => { + api.getServiceBindings({ + serviceInstanceGuid: service.guid + }, (err, result) => { + if (err) return callback(err); + + if (result.length > 0) { + _.each(result, (b) => { + var binding = _.find(deploymentConfig.serviceBindings, { + serviceInstanceGuid: b.entity.service_instance_guid, + appGuid: b.entity.app_guid + }); + if (binding) { + binding.guid = b.metadata.guid; + } + }); + } + + callback(null); + }); + }, (err) => { + if (err) return done(err); + + api.getDomains((err, domains) => { + if (err) return done(err); + api.domains = domains; + + deploymentConfig.domains = _.map(domains, (d) => { + return { guid: d.metadata.guid, name: d.entity.name }; + }); + + api.actualDeploymentConfig = deploymentConfig; + + + api.getRoutes((err, routes) => { + if (err) return done(err); + + const newRoutes = _.map(routes, (route) => { + const dom = _.find(deploymentConfig.domains, { guid: route.entity.domain_guid }); + var foundRoute = _.find(deploymentConfig.routes, { guid: route.metadata.guid }); + if (foundRoute) { + return foundRoute; + } + return { + guid: route.metadata.guid, + domain: dom ? dom.name : undefined, + domainGuid: route.entity.domain_guid, + hostname: route.entity.host + }; + }); + api.routes = newRoutes; + deploymentConfig.routes = newRoutes; + + api.actualDeploymentConfig = deploymentConfig; + + done(null, deploymentConfig); + }); + }); + }); + }); + }; +}; diff --git a/fns/getAppInfo.js b/fns/getAppInfo.js new file mode 100644 index 0000000..060ac3d --- /dev/null +++ b/fns/getAppInfo.js @@ -0,0 +1,31 @@ +module.exports = (api) => { + return (options, callback) => { + options = options || {}; + + if (!options.name) { + return callback(new Error('Please provide a name! \n' + JSON.stringify(options, null, 2))); + } + + if (!api.spaceGuid) { + return callback(new Error('Please provide a space! \n' + JSON.stringify(options, null, 2))); + } + + api.graceRequest({ + method: 'GET', + uri: `/v2/spaces/${api.spaceGuid}/apps`, + qs: { + 'q': `name:${options.name}` + } + }, (err, response, result) => { + if (err) { + return callback(err); + } + + if (!result || result.total_results !== 1) { + return callback(new Error(`No app found ${options.name}!`)); + } + + callback(null, result.resources[0]); + }); + }; +}; diff --git a/fns/getAppInstances.js b/fns/getAppInstances.js new file mode 100644 index 0000000..63b1d01 --- /dev/null +++ b/fns/getAppInstances.js @@ -0,0 +1,34 @@ +const _ = require('lodash'); + +module.exports = (api) => { + return (options, callback) => { + options = options || {}; + + if (!options.appGuid && options.name && api.actualDeploymentConfig) { + var a = _.find(api.actualDeploymentConfig.apps, { name: options.name }); + if (a) { + options.appGuid = a.guid; + } + } + + if (!options.appGuid) { + return callback(new Error('Please provide an appGuid! \n' + JSON.stringify(options, null, 2))); + } + + api.graceRequest({ + method: 'GET', + // uri: `/v2/apps/${options.appGuid}/instances` + uri: `/v2/apps/${options.appGuid}/stats` + }, (err, response, result) => { + if (err) { + return callback(err); + } + + if (!result) { + return callback(new Error('No app instances info!')); + } + + callback(null, result); + }); + }; +}; diff --git a/fns/getAppRoutes.js b/fns/getAppRoutes.js new file mode 100644 index 0000000..546a6e9 --- /dev/null +++ b/fns/getAppRoutes.js @@ -0,0 +1,29 @@ +module.exports = (api) => { + return (options, callback) => { + options = options || {}; + + if (!options.appGuid) { + return callback(new Error('Please provide an appGuid! \n' + JSON.stringify(options, null, 2))); + } + + api.graceRequest({ + method: 'GET', + uri: `/v2/apps/${options.appGuid}/routes`, + qs: options.hostname ? { + 'q': `host:${options.hostname}` + } : { + 'q': `domain_guid:${options.domainGuid}` + } + }, (err, response, result) => { + if (err) { + return callback(err); + } + + if (!result) { + return callback(new Error('No routes!')); + } + + callback(null, result.resources); + }); + }; +}; diff --git a/fns/getDomains.js b/fns/getDomains.js new file mode 100644 index 0000000..363d4c8 --- /dev/null +++ b/fns/getDomains.js @@ -0,0 +1,18 @@ +module.exports = (api) => { + return (callback) => { + api.graceRequest({ + method: 'GET', + uri: '/v2/domains' + }, (err, response, result) => { + if (err) { + return callback(err); + } + + if (!result) { + return callback(new Error('No domains found!')); + } + + callback(null, result.resources); + }); + }; +}; diff --git a/fns/getInfo.js b/fns/getInfo.js new file mode 100644 index 0000000..0ad9c09 --- /dev/null +++ b/fns/getInfo.js @@ -0,0 +1,23 @@ +const request = require('request'); + +module.exports = (api) => { + return (callback) => { + request({ + method: 'GET', + baseUrl: api.options.api, + uri: '/v2/info', + rejectUnauthorized: api.options.rejectUnauthorized, + json: true + }, (err, response, result) => { + if (err) { + return callback(err); + } + + if (!result) { + return callback(new Error('No taget information!')); + } + + callback(null, result); + }); + }; +}; diff --git a/fns/getLogWebSocket.js b/fns/getLogWebSocket.js new file mode 100644 index 0000000..e96ab85 --- /dev/null +++ b/fns/getLogWebSocket.js @@ -0,0 +1,35 @@ +const WebSocket = require('ws'); +const _ = require('lodash'); + +module.exports = (api) => { + return (options, callback) => { + options = options || {}; + + if (!options.appGuid && options.name && api.actualDeploymentConfig) { + var a = _.find(api.actualDeploymentConfig.apps, { name: options.name }); + if (a) { + options.appGuid = a.guid; + } + } + + if (!options.appGuid) { + var err = new Error('Please provide an appGuid! \n' + JSON.stringify(options, null, 2)); + return callback(err); + } + + var ws = new WebSocket(`${api.targetInfo.logging_endpoint}/tail/?app=${options.appGuid}`, { + headers : { + Authorization: `${api.token.token_type} ${api.token.access_token}` + }, + origin: 'http://localhost' + }); + + ws.on('open', function open() { + callback(null, ws); + }); + + ws.on('error', function error(err) { + callback(err); + }); + }; +}; diff --git a/fns/getOrgInfo.js b/fns/getOrgInfo.js new file mode 100644 index 0000000..fd65e03 --- /dev/null +++ b/fns/getOrgInfo.js @@ -0,0 +1,26 @@ +module.exports = (api) => { + return (callback) => { + if (!api.orgGuid && !api.options.org) { + return callback(new Error('Please provide an org! \n' + JSON.stringify(api.options, null, 2))); + } + + api.graceRequest({ + method: 'GET', + uri: '/v2/organizations' + (api.orgGuid ? `/${api.orgGuid}` : ''), + qs: api.orgGuid ? undefined : { + 'q': `name:${api.options.org}`, + 'inline-relations-depth': 1 + } + }, (err, response, result) => { + if (err) { + return callback(err); + } + + if (!result || result.total_results !== 1) { + return callback(new Error('No org information!')); + } + + callback(null, result.resources[0]); + }); + }; +}; diff --git a/fns/getRoutes.js b/fns/getRoutes.js new file mode 100644 index 0000000..8cb3d56 --- /dev/null +++ b/fns/getRoutes.js @@ -0,0 +1,28 @@ +module.exports = (api) => { + return (options, callback) => { + if (!callback) { + callback = options; + options = {}; + } + + api.graceRequest({ + method: 'GET', + uri: `/v2/spaces/${api.spaceGuid}/routes`, + qs: options.hostname ? { + 'q': `host:${options.hostname}` + } : (options.domainGuid ? { + 'q': `domain_guid:${options.domainGuid}` + } : {}) + }, (err, response, result) => { + if (err) { + return callback(err); + } + + if (!result) { + return callback(new Error('No routes found!')); + } + + callback(null, result.resources); + }); + }; +}; diff --git a/fns/getServiceBinding.js b/fns/getServiceBinding.js new file mode 100644 index 0000000..0ddec16 --- /dev/null +++ b/fns/getServiceBinding.js @@ -0,0 +1,24 @@ +module.exports = (api) => { + return (options, callback) => { + options = options || {}; + + if (!options.serviceBindingGuid) { + return callback(new Error('Please provide a serviceBindingGuid! \n' + JSON.stringify(options, null, 2))); + } + + if (!api.spaceGuid) { + return callback(new Error('Please provide a space! \n' + JSON.stringify(options, null, 2))); + } + + api.graceRequest({ + method: 'GET', + uri: `/v2/service_bindings/${options.serviceBindingGuid}` + }, (err, response, result) => { + if (err) { + return callback(err); + } + + callback(null, result); + }); + }; +}; diff --git a/fns/getServiceBindings.js b/fns/getServiceBindings.js new file mode 100644 index 0000000..aa02c1a --- /dev/null +++ b/fns/getServiceBindings.js @@ -0,0 +1,25 @@ +module.exports = (api) => { + return (options, callback) => { + options = options || {}; + + api.graceRequest({ + method: 'GET', + uri: '/v2/service_bindings', + qs: options.serviceInstanceGuid ? { + 'q': `service_instance_guid:${options.serviceInstanceGuid}` + } : { + 'q': `app_guid:${options.appGuid}` + } + }, (err, response, result) => { + if (err) { + return callback(err); + } + + if (!result) { + return callback(new Error('No service bindings!')); + } + + callback(null, result.resources); + }); + }; +}; diff --git a/fns/getServiceInstance.js b/fns/getServiceInstance.js new file mode 100644 index 0000000..c4d3863 --- /dev/null +++ b/fns/getServiceInstance.js @@ -0,0 +1,31 @@ +module.exports = (api) => { + return (options, callback) => { + options = options || {}; + + if (!options.name) { + return callback(new Error('Please provide a name! \n' + JSON.stringify(options, null, 2))); + } + + if (!api.spaceGuid) { + return callback(new Error('Please provide a space! \n' + JSON.stringify(options, null, 2))); + } + + api.graceRequest({ + method: 'GET', + uri: '/v2/service_instances', + qs: { + 'q': `name:${options.name};space_guid:${api.spaceGuid}` + } + }, (err, response, result) => { + if (err) { + return callback(err); + } + + if (!result || result.total_results !== 1) { + return callback(new Error(`No service found ${options.name}!`)); + } + + callback(null, result.resources[0]); + }); + }; +}; diff --git a/fns/getServicePlans.js b/fns/getServicePlans.js new file mode 100644 index 0000000..49661be --- /dev/null +++ b/fns/getServicePlans.js @@ -0,0 +1,27 @@ +module.exports = (api) => { + return (options, callback) => { + options = options || {}; + + if (!options.serviceId) { + return callback(new Error('Please provide a serviceId! \n' + JSON.stringify(options, null, 2))); + } + + api.graceRequest({ + method: 'GET', + uri: '/v2/service_plans', + qs: { + 'q': `service_guid:${options.serviceId}` + } + }, (err, response, result) => { + if (err) { + return callback(err); + } + + if (!result || result.total_results === 0) { + return callback(new Error(`No service plan for ${options.serviceId}!`)); + } + + callback(null, result.resources); + }); + }; +}; diff --git a/fns/getServices.js b/fns/getServices.js new file mode 100644 index 0000000..d205a2d --- /dev/null +++ b/fns/getServices.js @@ -0,0 +1,29 @@ +const async = require('async'); + +module.exports = (api) => { + return (callback) => { + api.graceRequest({ + method: 'GET', + uri: '/v2/services' + }, (err, response, result) => { + if (err) { + return callback(err); + } + + if (!result || result.total_results === 0) { + return callback(new Error('No service plans!')); + } + + async.each(result.resources, (service, callback) => { + api.getServicePlans({ serviceId: service.metadata.guid }, (err, res) => { + if (err) return callback(err); + service.servicePlans = res; + callback(null); + }); + }, (err) => { + if (err) return callback(err); + callback(null, result.resources); + }); + }); + }; +}; diff --git a/fns/getSpaceInfo.js b/fns/getSpaceInfo.js new file mode 100644 index 0000000..2555759 --- /dev/null +++ b/fns/getSpaceInfo.js @@ -0,0 +1,30 @@ +module.exports = (api) => { + return (callback) => { + if (!api.orgGuid) { + return callback(new Error('Please provide an orgGuid!')); + } + + if (!api.spaceGuid && !api.options.space) { + return callback(new Error('Please provide a space! \n' + JSON.stringify(api.options, null, 2))); + } + + api.graceRequest({ + method: 'GET', + uri: `/v2/organizations/${api.orgGuid}/spaces` + (api.spaceGuid ? `/${api.spaceGuid}` : ''), + qs: api.spaceGuid ? undefined : { + 'q': `name:${api.options.space}`, + 'inline-relations-depth': 1 + } + }, (err, response, result) => { + if (err) { + return callback(err); + } + + if (!result || result.total_results !== 1) { + return callback(new Error('No space information!')); + } + + callback(null, result.resources[0]); + }); + }; +}; diff --git a/fns/getSpaceSummary.js b/fns/getSpaceSummary.js new file mode 100644 index 0000000..1229901 --- /dev/null +++ b/fns/getSpaceSummary.js @@ -0,0 +1,22 @@ +module.exports = (api) => { + return (callback) => { + if (!api.spaceGuid) { + return callback(new Error('Please provide a spaceGuid!')); + } + + api.graceRequest({ + method: 'GET', + uri: `/v2/spaces/${api.spaceGuid}/summary` + }, (err, response, result) => { + if (err) { + return callback(err); + } + + if (!result) { + return callback(new Error('No space summary!')); + } + + callback(null, result); + }); + }; +}; diff --git a/fns/graceRequest.js b/fns/graceRequest.js new file mode 100644 index 0000000..ce3f293 --- /dev/null +++ b/fns/graceRequest.js @@ -0,0 +1,99 @@ +const debug = require('debug')('push2cloud-cf-adapter:graceRequest'); +const request = require('request'); +const _ = require('lodash'); + +module.exports = (api) => { + return (opt, callback) => { + opt = opt || {}; + + var defaults = { + method: 'GET', + baseUrl: api.options.api, + rejectUnauthorized: api.options.rejectUnauthorized, + json: true, + headers: api.token ? { + Authorization: `${api.token.token_type} ${api.token.access_token}` + } : undefined + }; + + _.defaults(opt, defaults); + + var attempt = 0; + + (function retry() { + setTimeout(() => { + request(opt, (err, response, result) => { + if (err) debug(err, result, opt); + if (!err && response && ( + response.statusCode < 300/* || + result && result.code*/ + )) { + return callback(err, response, result); + } + if (attempt >= api.options.maxRetries) { + if (result && result.error_code) { + return callback(new Error(result.description), response, result); + } + return callback(err, response, result); + } + if (response) { + debug(`StatusCode: ${response.statusCode}`); + debug(err, result, opt); + } + // it seems, that sometimes statusCode is not a nubmer, therefore only == + if (response && response.statusCode == 400) { + attempt++; + debug(`${attempt}. retry`); + return retry(); + } + if (result && result.error_code && result.error_code.toLowerCase().indexOf('timeout') >= 0) { + attempt++; + debug(`${attempt}. retry`); + return retry(); + } + if (result && result.error_code === 'UnknownError') { + attempt++; + debug(`${attempt}. retry`); + return retry(); + } + + if (result && (result.code === 1000 || result.error_description === 'Unable to verify token')) { + api.login((err, refreshedToken) => { + if (err) return debug(err); + if (refreshedToken) { + api.token = refreshedToken; + } + + opt.headers = api.token ? { + Authorization: `${api.token.token_type} ${api.token.access_token}` + } : undefined; + + attempt++; + debug(`${attempt}. retry`); + retry(); + }); + return; + } + + if (result && _.includes([10001, 10006, 60016, 10011], result.code)) { + attempt++; + debug(`${attempt}. retry`); + return retry(); + } + + if (result && result.error_code) { + return callback(new Error(result.description), response, result); + } + + if (err && err.code === 'ECONNRESET') { + attempt++; + debug(`${attempt}. retry`); + return retry(); + } + + callback(err, response, result); + }); + }, attempt * api.options.delay * api.options.delayFactor); + })(); + }; +}; diff --git a/fns/init.js b/fns/init.js new file mode 100644 index 0000000..c541bfa --- /dev/null +++ b/fns/init.js @@ -0,0 +1,114 @@ +const debug = require('debug')('push2cloud-cf-adapter:init'); +const _ = require('lodash'); + +module.exports = (api) => { + function reinit(callback) { + if (api.inited) { + return callback(null); + } + + api.logout = () => { + api.loggedOut = true; + if (api.loginRefreshHandle) clearTimeout(api.loginRefreshHandle); + }; + + api.loggedOut = false; + + if (!api.targetInfo) { + api.getInfo((err, info) => { + if (err) return callback(err); + debug(info); + api.targetInfo = info; + reinit(callback); + }); + return; + } + + if (!api.token) { + api.login((err, token) => { + if (err) return callback(err); + api.token = token; + + (function refresh() { + if (api.loggedOut) return; + api.loginRefreshHandle = setTimeout(() => { + api.refreshLogin((err, refreshedToken) => { + if (err) return debug(err); + api.token = refreshedToken; + refresh(); + }); + }, api.token.expires_in * 1000 * 0.5); + })(); + + reinit(callback); + }); + return; + } + + if (!api.orgGuid) { + api.getOrgInfo((err, orgInfo) => { + if (err) return callback(err); + api.orgInfo = orgInfo; + api.orgGuid = orgInfo.metadata.guid; + reinit(callback); + }); + return; + } + + if (!api.spaceGuid) { + api.getSpaceInfo((err, spaceInfo) => { + if (err) return callback(err); + api.spaceInfo = spaceInfo; + api.spaceGuid = spaceInfo.metadata.guid; + reinit(callback); + }); + return; + } + + if (!api.services) { + api.getServices((err, services) => { + if (err) return callback(err); + api.services = services; + reinit(callback); + }); + + api.getService = (serviceName) => { + const service = _.find(api.services, { entity: { label: serviceName } }); + return service; + }; + + api.getServicePlan = (serviceName, planName) => { + const service = api.getService(serviceName); + if (!service || !service.servicePlans) return null; + const servicePlan = _.find(service.servicePlans, { entity: { name: planName } }); + if (!servicePlan) return null; + return servicePlan; + }; + + return; + } + + if (!api.domains) { + api.getDomains((err, domains) => { + if (err) return callback(err); + api.domains = domains; + reinit(callback); + }); + return; + } + + if (!api.actualDeploymentConfig) { + api.getActualDeploymentConfig((err) => { + if (err) return callback(err); + reinit(callback); + }); + return; + } + + api.inited = true; + + callback(null, _.cloneDeep(api.actualDeploymentConfig)); + } + + return reinit; +}; diff --git a/fns/login.js b/fns/login.js new file mode 100644 index 0000000..ae97692 --- /dev/null +++ b/fns/login.js @@ -0,0 +1,49 @@ +const request = require('request'); + +module.exports = (api) => { + return (callback) => { + if (!api.targetInfo) { + return callback(new Error('No taget information!')); + } + + if (!api.options.username) { + return callback(new Error('Please provide a username! \n' + JSON.stringify(api.options, null, 2))); + } + + if (!api.options.password) { + return callback(new Error('Please provide a password! \n' + JSON.stringify(api.options, null, 2))); + } + + request({ + method: 'POST', + baseUrl: api.targetInfo.authorization_endpoint, + uri: '/oauth/token', + rejectUnauthorized: api.options.rejectUnauthorized, + json: true, + headers: { + 'Authorization': 'Basic Y2Y6', + 'Content-Type': 'application/x-www-form-urlencoded' + }, + form: { + grant_type: 'password', + client_id: 'cf', + username: api.options.username, + password: api.options.password + } + }, (err, response, result) => { + if (err) { + return callback(err); + } + + if (!result) { + return callback(new Error('No token information!')); + } + + if (result.error) { + return callback(new Error(result.error + ': ' + result.error_description)); + } + + callback(null, result); + }); + }; +}; diff --git a/fns/mapRoute.js b/fns/mapRoute.js new file mode 100644 index 0000000..7394a63 --- /dev/null +++ b/fns/mapRoute.js @@ -0,0 +1,15 @@ +const _ = require('lodash'); +const async = require('async'); + +module.exports = (api) => { + const mapRoute = (options, callback) => { + async.series( + [ _.curry(api.createRoute, 2)(options) + , _.curry(api.associateRoute, 2)(options) + ] + , callback + ); + }; + + return mapRoute; +}; diff --git a/fns/pushApp.js b/fns/pushApp.js new file mode 100644 index 0000000..da9287d --- /dev/null +++ b/fns/pushApp.js @@ -0,0 +1,12 @@ +module.exports = (api) => { + return (options, callback) => { + api.createApp(options, (err, result) => { + if (err) return callback(err); + options.appGuid = result.metadata.guid; + api.uploadApp(options, (err) => { + if (err) return callback(err); + callback(null, result); + }); + }); + }; +}; diff --git a/fns/refreshLogin.js b/fns/refreshLogin.js new file mode 100644 index 0000000..ffe6a52 --- /dev/null +++ b/fns/refreshLogin.js @@ -0,0 +1,43 @@ +const request = require('request'); + +module.exports = (api) => { + return (callback) => { + if (!api.targetInfo) { + return callback(new Error('No taget information!')); + } + + if (!api.token) { + return callback(new Error('Please provide a token!')); + } + + request({ + method: 'POST', + baseUrl: api.targetInfo.authorization_endpoint, + uri: '/oauth/token', + rejectUnauthorized: api.options.rejectUnauthorized, + json: true, + headers: { + 'Authorization': 'Basic Y2Y6', + 'Content-Type': 'application/x-www-form-urlencoded' + }, + form: { + grant_type: 'refresh_token', + refresh_token: api.token.refresh_token + } + }, (err, response, result) => { + if (err) { + return callback(err); + } + + if (!result) { + return callback(new Error('No token information!')); + } + + if (result.error) { + return callback(new Error(result.error + ': ' + result.error_description)); + } + + callback(null, result); + }); + }; +}; diff --git a/fns/restageApp.js b/fns/restageApp.js new file mode 100644 index 0000000..fc3e4d5 --- /dev/null +++ b/fns/restageApp.js @@ -0,0 +1,45 @@ +const _ = require('lodash'); + +module.exports = (api) => { + return (options, callback) => { + options = options || {}; + + if (!options.appGuid && options.name && api.actualDeploymentConfig) { + var a = _.find(api.actualDeploymentConfig.apps, { name: options.name }); + if (a) { + options.appGuid = a.guid; + } + } + + if (!options.appGuid) { + return callback(new Error('Please provide an appGuid! \n' + JSON.stringify(options, null, 2))); + } + api.graceRequest({ + method: 'POST', + uri: `/v2/apps/${options.appGuid}/restage` + }, (err, response, result) => { + if (err) { + return callback(err); + } + + if (result.entity.name !== options.newName && options.newName) { + delete _.remove(api.actualDeploymentConfig.apps, (a) => a.name === options.name); + } + + var appIdx = _.findIndex(api.actualDeploymentConfig.apps, { name: result.entity.name }); + if (appIdx < 0) appIdx = api.actualDeploymentConfig.apps.length; + api.actualDeploymentConfig.apps[appIdx] = { + name: result.entity.name, + guid: result.metadata.guid, + instances: result.entity.instances, + memory: result.entity.memory, + disk: result.entity.disk_quota, + state: result.entity.state, + version: result.entity.name.substring(result.entity.name.lastIndexOf('-') + 1), + package_state: result.entity.package_state + }; + + callback(null, result); + }); + }; +}; diff --git a/fns/restartApp.js b/fns/restartApp.js new file mode 100644 index 0000000..197eb14 --- /dev/null +++ b/fns/restartApp.js @@ -0,0 +1,11 @@ +module.exports = (api) => { + return (options, callback) => { + api.stopApp( + options, + (err) => { + if (err) return callback(err); + api.startApp(options, callback); + } + ); + }; +}; diff --git a/fns/scaleApp.js b/fns/scaleApp.js new file mode 100644 index 0000000..f5503b0 --- /dev/null +++ b/fns/scaleApp.js @@ -0,0 +1,9 @@ +module.exports = (api) => { + return (options, callback) => { + if (!options.disk && !options.memory && !options.instances) { + return callback(new Error('Please provide a scaling option! \n' + JSON.stringify(options, null, 2))); + } + + api.updateApp(options, callback); + }; +}; diff --git a/fns/setEnv.js b/fns/setEnv.js new file mode 100644 index 0000000..3ef21e0 --- /dev/null +++ b/fns/setEnv.js @@ -0,0 +1,9 @@ +module.exports = (api) => { + return (options, callback) => { + if (!options.env) { + return callback(new Error('Please provide an env option! \n' + JSON.stringify(options, null, 2))); + } + + api.updateApp(options, callback); + }; +}; diff --git a/fns/stageApp.js b/fns/stageApp.js new file mode 100644 index 0000000..9bbf061 --- /dev/null +++ b/fns/stageApp.js @@ -0,0 +1,59 @@ +const debug = require('debug')('push2cloud-cf-adapter:stageApp'); +const _ = require('lodash'); + +module.exports = (api) => { + return (options, callback) => { + options = options || {}; + + if (!options.appGuid && options.name && api.actualDeploymentConfig) { + var a = _.find(api.actualDeploymentConfig.apps, { name: options.name }); + if (a) { + options.appGuid = a.guid; + } + } + + if (!options.appGuid) { + return callback(new Error('Please provide an appGuid! \n' + JSON.stringify(options, null, 2))); + } + + options.messages = ['Starting app instance', 'Uploading complete']; + options.failMessages = ['staging failed']; + + options.stageTimeout = options.stageTimeout || 300; //seconds + + var attempt = 0; + + var timeoutMsg = `Staging for app ${options.name || options.appGuid} took longer than ${options.stageTimeout} seconds!`; + + (function retry() { + var timer; + var clb = _.once((err) => { + clearTimeout(timer); + if (err) { + debug(err); + + if (!err.message || (err.message.indexOf(options.failMessages[0]) < 0 && err.message !== timeoutMsg)) { + return callback(err); + } + + if (attempt >= api.options.maxRetries) { + return callback(err); + } + + attempt++; + debug(`${attempt}. retry`); + + retry(); + return api.restageApp({ appGuid: options.appGuid }, _.noop); + } + api.stopApp({ appGuid: options.appGuid }, callback); + }); + timer = setTimeout(() => { + clb(new Error(timeoutMsg)); + }, 1000 * options.stageTimeout); + api.tailAppLogsAndWaitFor(options, clb); + })(); + + api.startApp({ appGuid: options.appGuid, name: options.name }, _.noop); + }; +}; diff --git a/fns/startApp.js b/fns/startApp.js new file mode 100644 index 0000000..959b2de --- /dev/null +++ b/fns/startApp.js @@ -0,0 +1,6 @@ +module.exports = (api) => { + return (options, callback) => { + options.state = 'STARTED'; + api.updateApp(options, callback); + }; +}; diff --git a/fns/startAppAndWaitForCompleteMessage.js b/fns/startAppAndWaitForCompleteMessage.js new file mode 100644 index 0000000..3820169 --- /dev/null +++ b/fns/startAppAndWaitForCompleteMessage.js @@ -0,0 +1,36 @@ +const _ = require('lodash'); + +module.exports = (api) => { + return (options, callback) => { + options = options || {}; + + if (!options.appGuid && options.name && api.actualDeploymentConfig) { + var a = _.find(api.actualDeploymentConfig.apps, { name: options.name }); + if (a) { + options.appGuid = a.guid; + } + } + + if (!options.appGuid) { + return callback(new Error('Please provide an appGuid! \n' + JSON.stringify(options, null, 2))); + } + + options.messages = ['Starting app instance', 'Successfully created container']; + + options.startTimeout = options.startTimeout || 30; //seconds + + var timer; + + const clb = _.once((err, res) => { + clearTimeout(timer); + if (err) return callback(err); + callback(null, res); + }); + + timer = setTimeout(() => { + clb(new Error(`Starting for app ${options.name || options.appGuid} took longer than ${options.startTimeout} seconds!`)); + }, 1000 * options.startTimeout); + options.onReady = () => api.startApp({ appGuid: options.appGuid, name: options.name }, _.noop); + api.tailAppLogsAndWaitFor(options, clb); + }; +}; diff --git a/fns/startAppAndWaitForInstances.js b/fns/startAppAndWaitForInstances.js new file mode 100644 index 0000000..f4a291f --- /dev/null +++ b/fns/startAppAndWaitForInstances.js @@ -0,0 +1,28 @@ +const _ = require('lodash'); + +module.exports = (api) => { + return (options, callback) => { + options = options || {}; + + if (!options.appGuid && options.name && api.actualDeploymentConfig) { + var a = _.find(api.actualDeploymentConfig.apps, { name: options.name }); + if (a) { + options.appGuid = a.guid; + } + } + + if (!options.appGuid) { + return callback(new Error('Please provide an appGuid! \n' + JSON.stringify(options, null, 2))); + } + + api.startAppAndWaitForCompleteMessage(_.clone(options), (err) => { + if (err) return callback(err); + + api.waitForAllInstancesRunning(_.clone(options), (err) => { + if (err) return callback(err); + + api.verifyInstancesNotCrashing(_.clone(options), callback); + }); + }); + }; +}; diff --git a/fns/startAppAndWaitForMessages.js b/fns/startAppAndWaitForMessages.js new file mode 100644 index 0000000..3ba5626 --- /dev/null +++ b/fns/startAppAndWaitForMessages.js @@ -0,0 +1,34 @@ +const _ = require('lodash'); + +module.exports = (api) => { + return (options, callback) => { + options = options || {}; + + if (!options.appGuid && options.name && api.actualDeploymentConfig) { + var a = _.find(api.actualDeploymentConfig.apps, { name: options.name }); + if (a) { + options.appGuid = a.guid; + } + } + + if (!options.appGuid) { + return callback(new Error('Please provide an appGuid! \n' + JSON.stringify(options, null, 2))); + } + + options.startTimeout = options.startTimeout || 90; //seconds + + var timer; + + const clb = _.once((err, res) => { + clearTimeout(timer); + if (err) return callback(err); + callback(null, res); + }); + + timer = setTimeout(() => { + clb(new Error(`Starting for app ${options.name || options.appGuid} took longer than ${options.startTimeout} seconds!`)); + }, 1000 * options.startTimeout); + options.onReady = () => api.startApp({ appGuid: options.appGuid, name: options.name }, _.noop); + api.tailAppLogsAndWaitFor(options, clb); + }; +}; diff --git a/fns/stopApp.js b/fns/stopApp.js new file mode 100644 index 0000000..76fadaa --- /dev/null +++ b/fns/stopApp.js @@ -0,0 +1,6 @@ +module.exports = (api) => { + return (options, callback) => { + options.state = 'STOPPED'; + api.updateApp(options, callback); + }; +}; diff --git a/fns/tailAppLogsAndWaitFor.js b/fns/tailAppLogsAndWaitFor.js new file mode 100644 index 0000000..4996a6d --- /dev/null +++ b/fns/tailAppLogsAndWaitFor.js @@ -0,0 +1,57 @@ +const debug = require('debug')('push2cloud-cf-adapter:tailAppLogsAndWaitFor'); +const _ = require('lodash'); + +module.exports = (api) => { + return (options, callback) => { + options = options || {}; + + if (!options.appGuid && options.name && api.actualDeploymentConfig) { + var a = _.find(api.actualDeploymentConfig.apps, { name: options.name }); + if (a) { + options.appGuid = a.guid; + } + } + + if (!options.appGuid) { + return callback(new Error('Please provide an appGuid! \n' + JSON.stringify(options, null, 2))); + } + + if (!options.messages || options.messages.length === 0) { + return callback(new Error('Please provide a messages option! \n' + JSON.stringify(options, null, 2))); + } + + api.getLogWebSocket(options, (err, ws) => { + if (err) return callback(err); + + if (options.onReady) options.onReady(); + + ws.on('message', (data) => { + const stringData = data.toString(); + debug(`${options.name || options.appGuid} log: ${stringData}`); + const found = _.find(options.messages, (k) => { + return stringData.indexOf(k) >= 0; + }); + if (found) { + debug(`Token ${stringData} in app log detected`); + ws.close(); + callback(null, stringData); + } + + if (options.failMessages) { + const foundErr = _.find(options.failMessages, (k) => { + return stringData.indexOf(k) >= 0; + }); + if (foundErr) { + debug(`Token ${stringData} in app log detected`); + ws.close(); + callback(new Error(stringData)); + } + } + }); + ws.on('open', () => { + debug(`Listening on log for app ${options.name || options.appGuid}`); + }); + ws.on('error', callback); + }); + }; +}; diff --git a/fns/unbindService.js b/fns/unbindService.js new file mode 100644 index 0000000..5bae5ae --- /dev/null +++ b/fns/unbindService.js @@ -0,0 +1,64 @@ +const _ = require('lodash'); + +module.exports = (api) => { + return (options, callback) => { + options = options || {}; + + if (!api.spaceGuid) { + return callback(new Error('Please provide a space! \n' + JSON.stringify(options, null, 2))); + } + + if (!options.serviceBindingGuid && options.app && options.service && api.actualDeploymentConfig) { + var b = _.find(api.actualDeploymentConfig.serviceBindings, { app: options.app, service: options.service }); + if (b) { + options.serviceBindingGuid = b.guid; + } + } + + if (!options.serviceBindingGuid) { + return callback(new Error('Please provide an serviceBindingGuid! \n' + JSON.stringify(options, null, 2))); + } + + api.graceRequest({ + method: 'DELETE', + uri: `/v2/service_bindings/${options.serviceBindingGuid}` + }, (err, response, result) => { + if (err && response && response.error_code === 'CF-ServiceBindingNotFound') { + api.getServiceBinding({ serviceBindingGuid: options.serviceBindingGuid }, (newError, result) => { + if (newError) { + return callback(newError); + } + + if (!result) { + return callback(err); + } + + api.graceRequest({ + method: 'DELETE', + uri: `/v2/service_bindings/${options.serviceBindingGuid}` + }, (err, response, result) => { + if (err) { + return callback(err); + } + + if (api.actualDeploymentConfig) { + _.remove(api.actualDeploymentConfig.serviceBindings, { guid: options.serviceBindingGuid }); + } + + callback(null, result); + }); + }); + } + + if (err) { + return callback(err); + } + + if (api.actualDeploymentConfig) { + _.remove(api.actualDeploymentConfig.serviceBindings, { guid: options.serviceBindingGuid }); + } + + callback(null, result); + }); + }; +}; diff --git a/fns/unmapRoute.js b/fns/unmapRoute.js new file mode 100644 index 0000000..4d8d003 --- /dev/null +++ b/fns/unmapRoute.js @@ -0,0 +1,12 @@ +const _ = require('lodash'); +const async = require('async'); + +module.exports = (api) => { + return (options, callback) => { + async.series([ + _.curry(api.disassociateRoute, 2)(options) + , _.curry(api.deleteRoute, 2)(options) + ], + callback); + }; +}; diff --git a/fns/updateApp.js b/fns/updateApp.js new file mode 100644 index 0000000..c759c47 --- /dev/null +++ b/fns/updateApp.js @@ -0,0 +1,67 @@ +const convertSize = require('../lib/convertSize'); +const _ = require('lodash'); + +module.exports = (api) => { + return (options, callback) => { + options = options || {}; + + if (!api.spaceGuid) { + return callback(new Error('Please provide a space! \n' + JSON.stringify(options, null, 2))); + } + + if (!options.appGuid && options.name && api.actualDeploymentConfig) { + var a = _.find(api.actualDeploymentConfig.apps, { name: options.name }); + if (a) { + options.appGuid = a.guid; + } + } + + if (!options.appGuid) { + return callback(new Error('Please provide an appGuid! \n' + JSON.stringify(options, null, 2))); + } + api.graceRequest({ + method: 'PUT', + uri: `/v2/apps/${options.appGuid}`, + json : { + name: options.newName, + space_guid: api.spaceGuid, + buildpack: options.buildpack, + command: options.command, + environment_json: options.env || undefined, + disk_quota: convertSize(options.disk) || undefined, + memory: convertSize(options.memory) || undefined, + instances: convertSize(options.instances) || undefined, + state: options.state + } + }, (err, response, result) => { + if (err) { + return callback(err); + } + + if (result.entity.name !== options.newName && options.newName) { + delete _.remove(api.actualDeploymentConfig.apps, (a) => a.name === options.name); + } + + var appIdx = _.findIndex(api.actualDeploymentConfig.apps, { name: result.entity.name }); + if (appIdx < 0) appIdx = api.actualDeploymentConfig.apps.length; + + var indexOfVersion = result.entity.name.lastIndexOf('-'); + var unversionedName = result.entity.name; + if (indexOfVersion >= 0) unversionedName = result.entity.name.substring(0, indexOfVersion); + + api.actualDeploymentConfig.apps[appIdx] = { + name: result.entity.name, + unversionedName: unversionedName, + guid: result.metadata.guid, + instances: result.entity.instances, + memory: result.entity.memory, + disk: result.entity.disk_quota, + state: result.entity.state, + version: result.entity.name.substring(result.entity.name.lastIndexOf('-') + 1), + package_state: result.entity.package_state + }; + + callback(null, result); + }); + }; +}; diff --git a/fns/uploadApp.js b/fns/uploadApp.js new file mode 100644 index 0000000..5d85d82 --- /dev/null +++ b/fns/uploadApp.js @@ -0,0 +1,62 @@ +const _ = require('lodash'); +const debug = require('debug')('push2cloud-cf-adapter:uploadApp'); +const fs = require('fs'); +const zipGenerator = require('../lib/zipGenerator'); + +module.exports = (api) => { + return (options, callback) => { + if (!api.spaceGuid) { + return callback(new Error('Please provide a space! \n' + JSON.stringify(options, null, 2))); + } + + if (!options.appGuid && options.name && api.actualDeploymentConfig) { + var a = _.find(api.actualDeploymentConfig.apps, { name: options.name }); + if (a) { + options.appGuid = a.guid; + } + } + + if (!options.appGuid) { + return callback(new Error('Please provide an appGuid! \n' + JSON.stringify(options, null, 2))); + } + + if (!options.path) { + return callback(new Error('Please provide an path! \n' + JSON.stringify(options, null, 2))); + } + + var defaults = { + async: false, + zipResources: [], + tmpZipPath: options.path + '.zip.tmp' + }; + + _.defaults(options, defaults); + + zipGenerator(options.path, options.tmpZipPath, (err) => { + if (err) return callback(err); + + api.graceRequest({ + method: 'PUT', + uri: `/v2/apps/${options.appGuid}/bits`, + qs: { + async: options.async + }, + formData: { + resources: JSON.stringify(options.zipResources), + application: fs.createReadStream(options.tmpZipPath) + }, + json: false + }, (err, response, result) => { + fs.unlink(options.tmpZipPath, (err) => { + if (err) debug(err); + }); + + if (err) { + return callback(err); + } + + callback(null, result); + }); + }); + }; +}; diff --git a/fns/verifyInstancesNotCrashing.js b/fns/verifyInstancesNotCrashing.js new file mode 100644 index 0000000..d970fcd --- /dev/null +++ b/fns/verifyInstancesNotCrashing.js @@ -0,0 +1,51 @@ +const debug = require('debug')('push2cloud-cf-adapter:verifyInstancesNotCrashing'); +const _ = require('lodash'); + +function allInstancesRunning(instances) { + return _.every(instances, (instance) => instance.state === 'RUNNING'); +} + +module.exports = (api) => { + return (options, callback) => { + options = options || {}; + + options.gracePeriod = options.gracePeriod || 40; + options.interval = options.interval || 4; + options.maxRetries = options.maxRetries || Math.floor(options.gracePeriod / options.interval); + + if (!options.appGuid && options.name && api.actualDeploymentConfig) { + var a = _.find(api.actualDeploymentConfig.apps, { name: options.name }); + if (a) { + options.appGuid = a.guid; + } + } + + if (!options.appGuid) { + return callback(new Error('Please provide an appGuid! \n' + JSON.stringify(options, null, 2))); + } + + var attempt = 0; + + (function retry() { + setTimeout(() => { + api.getAppInstances(options, (err, instances) => { + if (err) return callback(err); + + if (attempt >= options.maxRetries) { + return callback(null); + } + + var running = allInstancesRunning(instances); + + if (running) { + attempt++; + debug(`${attempt}. retry will try ${options.maxRetries} for ${options.name || options.appGuid}`); + return retry(); + } + + callback(new Error(`${options.name || options.appGuid} crashed!`)); + }); + }, options.interval * 1000); + })(); + }; +}; diff --git a/fns/waitForAllInstancesRunning.js b/fns/waitForAllInstancesRunning.js new file mode 100644 index 0000000..c1469f5 --- /dev/null +++ b/fns/waitForAllInstancesRunning.js @@ -0,0 +1,51 @@ +const debug = require('debug')('push2cloud-cf-adapter:waitForAllInstances'); +const _ = require('lodash'); + +function allInstancesRunning(instances) { + return _.every(instances, (instance) => instance.state === 'RUNNING'); +} + +module.exports = (api) => { + return (options, callback) => { + options = options || {}; + + options.timeout = options.timeout || 30; + options.interval = options.interval || 3; + options.maxRetries = options.maxRetries || Math.floor(options.timeout / options.interval); + + if (!options.appGuid && options.name && api.actualDeploymentConfig) { + var a = _.find(api.actualDeploymentConfig.apps, { name: options.name }); + if (a) { + options.appGuid = a.guid; + } + } + + if (!options.appGuid) { + return callback(new Error('Please provide an appGuid! \n' + JSON.stringify(options, null, 2))); + } + + var attempt = 0; + + (function retry() { + setTimeout(() => { + api.getAppInstances(options, (err, instances) => { + if (err) return callback(err); + + if (attempt >= options.maxRetries) { + return callback(new Error(`${options.name || options.appGuid} timeouted!`)); + } + + var running = allInstancesRunning(instances); + + if (!running) { + attempt++; + debug(`${attempt}. retry for ${options.name || options.appGuid}`); + return retry(); + } + + callback(null); + }); + }, options.interval * 1000); + })(); + }; +}; diff --git a/index.js b/index.js new file mode 100644 index 0000000..ad47ecc --- /dev/null +++ b/index.js @@ -0,0 +1,74 @@ +const fs = require('fs'); +const path = require('path'); +const _ = require('lodash'); +const debug = require('debug')('push2cloud-cf-adapter'); + +const functions = []; +const functionsPath = path.join(__dirname, 'fns'); + +fs.readdirSync(functionsPath).forEach((fileName) => { + var joinedPath = path.join(functionsPath, fileName); + + if (path.extname(fileName) === '.js') { + var required = require(joinedPath); + functions.push({ name: path.basename(fileName, '.js'), module: required }); + } +}); + +module.exports = (options) => { + options = options || {}; + + var defaults = { + api: 'https://api.run.pivotal.io', + rejectUnauthorized: true, + maxRetries: 3, + delay: 1000, + delayFactor: 1 + }; + + _.defaults(options, defaults); + + const api = { options }; + + functions.forEach((fn) => { + const orgFn = fn.module(api); + api[fn.name] = function(opts) { + var additionalInfos; + if (opts) { + if (opts.name) additionalInfos = ' ' + opts.name; + if (!additionalInfos && opts.app) additionalInfos = ' ' + opts.app; + if (!additionalInfos && opts.hostname) additionalInfos = ' ' + opts.hostname; + if (!additionalInfos) additionalInfos = ''; + } + + if (arguments.length === 0 || typeof arguments[arguments.length - 1] !== 'function') { + var args = _.toArray(arguments); + // called as promise.. + debug(`called ${fn.name} as promise...${additionalInfos}`); + return new Promise((resolve, reject) => { + args.push((err) => { + var responseArgs = _.toArray(arguments); + responseArgs.shift(); + if (err) { + return reject(err); + } + if (responseArgs.length === 0) { + return resolve(); + } + if (responseArgs.length === 1) { + return resolve(responseArgs[0]); + } + resolve(responseArgs); + }); + orgFn.apply(api, args); + }); + } else { + // called normally... + debug(`called ${fn.name}...${additionalInfos}`); + orgFn.apply(api, arguments); + } + }; + }); + + return api; +}; diff --git a/lib/convertSize.js b/lib/convertSize.js new file mode 100644 index 0000000..54a38a5 --- /dev/null +++ b/lib/convertSize.js @@ -0,0 +1,16 @@ +const _ = require('lodash'); + +module.exports = (input) => { + if (_.isNumber(input)) return input; + if (_.isString(input)) { + var multipliers = { + M: 1, + G: 1024 + }; + var matches = input.toUpperCase().match(/(\d)+([MG]?)B?$/); + var value = parseInt(matches[1]); + var unit = matches[2]; + return value * (multipliers[unit] || 1); + } + return null; +}; diff --git a/lib/parseCFSummaryIntoDeploymentConfig.js b/lib/parseCFSummaryIntoDeploymentConfig.js new file mode 100644 index 0000000..b96ca5b --- /dev/null +++ b/lib/parseCFSummaryIntoDeploymentConfig.js @@ -0,0 +1,83 @@ +const _ = require('lodash'); + +module.exports = (summaryJSON) => { + const actual = { + apps: [], + serviceBindings: [], + services: [], + // domains: {}, + routes: [], + envVars: [] + }; + + _.forEach(summaryJSON.services, (service) => { + actual.services.push({ + name: service.name, + guid: service.guid, + type: service.service_plan.service.label, + plan: service.service_plan.name + }); + }); + + _.forEach(summaryJSON.apps, (app) => { + + var indexOfVersion = app.name.lastIndexOf('-'); + var unversionedName = app.name; + if (indexOfVersion >= 0) unversionedName = app.name.substring(0, indexOfVersion); + + actual.apps.push({ + name: app.name, + unversionedName: unversionedName, + guid: app.guid, + instances: app.instances, + memory: app.memory, + disk: app.disk_quota, + state: app.state, + version: app.name.substring(app.name.lastIndexOf('-') + 1), + package_state: app.package_state + }); + + _.forEach(app.service_names, (service) => { + actual.serviceBindings.push({ + service: service, + serviceInstanceGuid: _.find(actual.services, { name: service }).guid, + app: app.name, + unversionedName: unversionedName, + appGuid: app.guid + }); + }); + + var env = { + env: {}, + name: app.name, + unversionedName: unversionedName + }; + _.forIn(app.environment_json, (value, key) => env.env[key] = value); + actual.envVars.push(env); + + _.forEach(app.routes, (route) => { + var foundRoute = _.findIndex(actual.routes, 'guid', route.guid); + var newRoute = { + guid: route.guid, + domain: route.domain.name, + domainGuid: route.domain.guid, + hostname: route.host, + app: app.name, + unversionedName: unversionedName, + appGuid: app.guid + }; + if (foundRoute >= 0) { + actual.routes[foundRoute] = newRoute; + } else { + actual.routes.push(newRoute); + } + // actual.domains[route.domain.name] = route.domain; + }); + }); + + actual.serviceBindings = _.sortBy(actual.serviceBindings, (serviceBinding) => `${serviceBinding.app}${serviceBinding.service}`); + actual.routes = _.sortBy(actual.routes, (route) => `${route.app}${route.domain}${route.hostname}`); + actual.envVars = _.sortBy(actual.envVars, (envVar) => `${envVar.app}${envVar.key}${envVar.value}`); + + return actual; +}; diff --git a/lib/zipGenerator.js b/lib/zipGenerator.js new file mode 100644 index 0000000..8f2f29f --- /dev/null +++ b/lib/zipGenerator.js @@ -0,0 +1,39 @@ +var fs = require('fs'), + path = require('path'), + parser = require('gitignore-parser'), + archiver = require('archiver'), + filewalker = require('filewalker'); + +module.exports = (appSource, zipTarget, callback) => { + var output = fs.createWriteStream(zipTarget); + + var archive = archiver('zip'); + + fs.access(path.join(appSource, '.cfignore'), fs.R_OK, (err) => { + var cfignore = null; + if (!err) { + cfignore = parser.compile(fs.readFileSync(path.join(appSource, '.cfignore'), 'utf8')); + } + + filewalker(appSource) + .on('file', (p, s) => { + if (p === '.cfignore') return; + if (!cfignore || cfignore.accepts(p)) { + var stat = fs.statSync(path.join(appSource, p)); + archive.append(fs.createReadStream(path.join(appSource, p)), { name: p, mode: stat.mode }); + } + }) + .on('error', (err) => { + callback(err); + }) + .on('done', () => { + archive.pipe(output); + archive.finalize(); + + output.on('close', () => { + callback(null); + }); + }) + .walk(); + }); +}; diff --git a/package.json b/package.json new file mode 100644 index 0000000..e12dd57 --- /dev/null +++ b/package.json @@ -0,0 +1,37 @@ +{ + "name": "push2cloud-cf-adapter", + "version": "1.0.0", + "description": "abstracts cloud foundry api", + "main": "index.js", + "scripts": { + "lint": "eslint .", + "test": "npm run lint && mocha" + }, + "engines": { + "node": ">=4.2.0" + }, + "author": "Adriano Raiano (https://github.com/adrai)", + "contributors": [ + "Adriano Raiano (https://github.com/adrai)", + "Christoph Hermann (http://stoeffel.github.io/)", + "Michael Erne (https://github.com/michaelerne)" + ], + "license": "Apache-2.0", + "repository": "push2cloud/cf-adapter", + "homepage": "http://push2.cloud/", + "dependencies": { + "archiver": "1.0.0", + "async": "2.0.0-rc.3", + "debug": "2.2.0", + "filewalker": "0.1.2", + "gitignore-parser": "0.0.2", + "lodash": "4.11.1", + "request": "2.72.0", + "semver": "5.1.0", + "ws": "1.1.0" + }, + "devDependencies": { + "eslint": "2.8.0", + "expect.js": "0.3.1" + } +} diff --git a/test/can.basically.talk.with.cf.js b/test/can.basically.talk.with.cf.js new file mode 100644 index 0000000..f6e24e5 --- /dev/null +++ b/test/can.basically.talk.with.cf.js @@ -0,0 +1,286 @@ +const expect = require('expect.js'); +const cfAdapter = require('../'); +const join = require('path').join; + +describe('can basically talk with cf', () => { + + var api; + + before(() => { + api = cfAdapter({ + api: 'https://api.lyra-836.appcloud.swisscom.com', + username: process.env.CF_USER, + password: process.env.CF_PWD, + org: process.env.CF_ORG, + space: process.env.CF_SPACE + }); + }); + + describe('getInfo', () => { + + it('should work as expected', (done) => { + + api.getInfo((err, result) => { + expect(err).not.to.be.ok(); + expect(result).to.be.ok(); + done(); + }); + + }); + + }); + + describe('init', () => { + + it('should work as expected', (done) => { + + api.init((err, result) => { + expect(err).not.to.be.ok(); + expect(api.actualDeploymentConfig).to.be.an('object'); + expect(api.inited).to.eql(true); + expect(api.services).to.be.an('array'); + expect(api.services[0].servicePlans).to.be.an('array'); + done(); + }); + + }); + + }); + + describe('inited', () => { + + before((done) => { + api.init(done); + }); + + describe('getSpaceSummary', () => { + + it('should work as expected', (done) => { + + api.getSpaceSummary((err, result) => { + expect(err).not.to.be.ok(); + expect(result.guid).to.eql(api.spaceGuid); + expect(result.name).to.eql(api.options.space); + expect(result.apps).to.be.an('array'); + expect(result.services).to.be.an('array'); + done(); + }); + + }); + + }); + + describe('getActualDeploymentConfig', () => { + + it('should work as expected', (done) => { + + api.getActualDeploymentConfig((err, result) => { + expect(err).not.to.be.ok(); + expect(result.apps).to.be.an('array'); + expect(result.services).to.be.an('array'); + expect(api.actualDeploymentConfig).to.eql(result); + done(); + }); + + }); + + }); + + describe.skip('createServiceInstance and deleteServiceInstance', () => { + + it('should work as expected', (done) => { + + api.createServiceInstance({ name: 'temp-service', type: 'elk', plan: 'beta' }, (err, result) => { + expect(err).not.to.be.ok(); + expect(result.entity.name).to.eql('temp-service'); + + api.deleteServiceInstance({ serviceInstanceGuid: result.metadata.guid }, (err, result) => { + expect(err).not.to.be.ok(); + done(); + }); + }); + + }); + + }); + + describe.skip('pushApp and updateApp and deleteApp', () => { + + it('should work as expected', (done) => { + + api.pushApp({ name: 'temp-app', appPath: join(__dirname, '/sampleApp') }, (err, result) => { + expect(err).not.to.be.ok(); + expect(result.entity.name).to.eql('temp-app'); + + api.updateApp({ appGuid: result.metadata.guid, newName: 'temp-app-modified', instances: 5 }, (err, result) => { + expect(err).not.to.be.ok(); + + api.deleteApp({ appGuid: result.metadata.guid }, (err, result) => { + expect(err).not.to.be.ok(); + done(); + }); + }); + }); + + }); + + }); + + describe.skip('round-trip', () => { + + it('should work as expected', (done) => { + + api.pushApp({ name: 'temp-app', appPath: join(__dirname, '/sampleApp') }, (err, app) => { + expect(err).not.to.be.ok(); + expect(app.entity.name).to.eql('temp-app'); + + api.createServiceInstance({ name: 'temp-service', serviceName: 'elk', planName: 'beta' }, (err, service) => { + expect(err).not.to.be.ok(); + expect(service.entity.name).to.eql('temp-service'); + + api.bindService({ appGuid: app.metadata.guid, serviceInstanceGuid: service.metadata.guid }, (err, binding) => { + expect(err).not.to.be.ok(); + expect(binding.metadata.guid).to.be.ok(); + + api.unbindService({ serviceBindingGuid: binding.metadata.guid }, (err) => { + expect(err).not.to.be.ok(); + + api.deleteApp({ appGuid: app.metadata.guid }, (err, result) => { + expect(err).not.to.be.ok(); + + api.deleteServiceInstance({ serviceInstanceGuid: service.metadata.guid }, (err, result) => { + expect(err).not.to.be.ok(); + done(); + }); + }); + }); + }); + }); + }); + + }); + + }); + + describe('createRoute and deleteRoute', () => { + + it('should work as expected', (done) => { + + api.createRoute({ + hostname: 'tmp-route-for-my-cf-test-space', + domain: 'scapp.io' + }, (err, result) => { + expect(err).not.to.be.ok(); + expect(result.metadata.guid).to.be.ok(); + + api.deleteRoute({ + routeGuid: result.metadata.guid + }, (err) => { + expect(err).not.to.be.ok(); + done(); + }); + }); + + }); + + }); + + describe('route round-trip', () => { + + it('should work as expected', (done) => { + + api.createRoute({ + hostname: 'tmp-route-for-my-cf-test-space', + domain: 'scapp.io' + }, (err, route) => { + expect(err).not.to.be.ok(); + expect(route.metadata.guid).to.be.ok(); + + api.createApp({ name: 'tmp-app' }, (err, app) => { + expect(err).not.to.be.ok(); + + api.associateRoute({ appGuid: app.metadata.guid, routeGuid: route.metadata.guid }, (err) => { + expect(err).not.to.be.ok(); + + api.disassociateRoute({ appGuid: app.metadata.guid, routeGuid: route.metadata.guid }, (err) => { + expect(err).not.to.be.ok(); + + api.deleteRoute({ + routeGuid: route.metadata.guid + }, (err) => { + expect(err).not.to.be.ok(); + + api.deleteApp({ + appGuid: app.metadata.guid + }, (err) => { + expect(err).not.to.be.ok(); + done(); + }); + }); + }); + }); + + }); + + }); + + }); + + }); + + describe.skip('staging round-trip', () => { + + it('should work as expected', (done) => { + + this.timeout(120000); + + api.pushApp({ name: 'temp-app', appPath: join(__dirname, '/sampleApp') }, (err, app) => { + expect(err).not.to.be.ok(); + expect(app.entity.name).to.eql('temp-app'); + + api.stageApp({ appGuid: app.metadata.guid }, (err) => { + expect(err).not.to.be.ok(); + + api.startAppAndWaitForInstances({ appGuid: app.metadata.guid, gracePeriod: 3, interval: 2 }, (err) => { + expect(err).not.to.be.ok(); + + api.deleteApp({ appGuid: app.metadata.guid }, (err) => { + expect(err).not.to.be.ok(); + done(); + }); + }); + }); + }); + + }); + + }); + + describe('setEnv', () => { + + it('should work as expected', (done) => { + + api.createApp({ name: 'tmp-app' }, (err, app) => { + expect(err).not.to.be.ok(); + + api.setEnv({ appGuid: app.metadata.guid, env: { a: 'val 1', b: 'other' } }, (err, res) => { + expect(err).not.to.be.ok(); + expect(res.entity.environment_json.a).to.eql('val 1'); + + api.deleteApp({ + appGuid: app.metadata.guid + }, (err) => { + expect(err).not.to.be.ok(); + done(); + }); + }); + + }); + + }); + + }); + + }); + +}); diff --git a/test/can.be.used.with.promises.js b/test/can.be.used.with.promises.js new file mode 100644 index 0000000..921f15e --- /dev/null +++ b/test/can.be.used.with.promises.js @@ -0,0 +1,42 @@ +const expect = require('expect.js'); +const cfAdapter = require('../'); + +describe('can work with promises too', () => { + + var api; + + before(() => { + api = cfAdapter({ + api: 'https://api.lyra-836.appcloud.swisscom.com', + username: process.env.CF_USER, + password: process.env.CF_PWD, + org: process.env.CF_ORG, + space: process.env.CF_SPACE + }); + }); + + describe('getInfo', () => { + + it('should work as expected', () => { + + return api.getInfo().then((result) => { + expect(result).to.be.ok(); + }); + + }); + + }); + + describe('init', () => { + + it('should work as expected', () => { + + return api.init().then((result) => { + expect(api.inited).to.eql(true); + }); + + }); + + }); + +}); diff --git a/test/can.work.completely.without.guids.js b/test/can.work.completely.without.guids.js new file mode 100644 index 0000000..f509a10 --- /dev/null +++ b/test/can.work.completely.without.guids.js @@ -0,0 +1,327 @@ +const _ = require('lodash'); +const expect = require('expect.js'); +const cfAdapter = require('../'); +const join = require('path').join; + +describe('can work completely without guids', () => { + + var api; + + before(() => { + api = cfAdapter({ + api: 'https://api.lyra-836.appcloud.swisscom.com', + username: process.env.CF_USER, + password: process.env.CF_PWD, + org: process.env.CF_ORG, + space: process.env.CF_SPACE + }); + }); + + describe('getInfo', () => { + + it('should work as expected', (done) => { + + api.getInfo((err, result) => { + expect(err).not.to.be.ok(); + expect(result).to.be.ok(); + done(); + }); + + }); + + }); + + describe('init', () => { + + it('should work as expected', (done) => { + + api.init((err, result) => { + expect(err).not.to.be.ok(); + expect(api.actualDeploymentConfig).to.be.an('object'); + expect(api.inited).to.eql(true); + expect(api.services).to.be.an('array'); + expect(api.services[0].servicePlans).to.be.an('array'); + done(); + }); + + }); + + }); + + describe('inited', () => { + + before((done) => { + api.init(done); + }); + + describe('getSpaceSummary', () => { + + it('should work as expected', (done) => { + + api.getSpaceSummary((err, result) => { + expect(err).not.to.be.ok(); + expect(result.guid).to.eql(api.spaceGuid); + expect(result.name).to.eql(api.options.space); + expect(result.apps).to.be.an('array'); + expect(result.services).to.be.an('array'); + done(); + }); + + }); + + }); + + describe('getActualDeploymentConfig', () => { + + it('should work as expected', (done) => { + + api.getActualDeploymentConfig((err, result) => { + expect(err).not.to.be.ok(); + expect(result.apps).to.be.an('array'); + expect(result.services).to.be.an('array'); + expect(api.actualDeploymentConfig).to.eql(result); + done(); + }); + + }); + + }); + + describe.skip('createServiceInstance and deleteServiceInstance', () => { + + it('should work as expected', (done) => { + + api.createServiceInstance({ name: 'temp-service', type: 'elk', plan: 'beta' }, (err, result) => { + expect(err).not.to.be.ok(); + expect(result.entity.name).to.eql('temp-service'); + + api.deleteServiceInstance({ name: 'temp-service' }, (err, result) => { + expect(err).not.to.be.ok(); + done(); + }); + }); + + }); + + }); + + describe.skip('pushApp and updateApp and deleteApp', () => { + + it('should work as expected', (done) => { + + api.pushApp({ name: 'temp-app', appPath: join(__dirname, '/sampleApp') }, (err, result) => { + expect(err).not.to.be.ok(); + expect(result.entity.name).to.eql('temp-app'); + + api.updateApp({ name: 'temp-app', newName: 'temp-app-modified', instances: 5 }, (err, result) => { + expect(err).not.to.be.ok(); + + api.deleteApp({ name: 'temp-app-modified' }, (err) => { + expect(err).not.to.be.ok(); + done(); + }); + }); + }); + + }); + + }); + + describe.skip('round-trip', () => { + + it('should work as expected', (done) => { + + api.pushApp({ name: 'temp-app', appPath: join(__dirname, '/sampleApp') }, (err, app) => { + expect(err).not.to.be.ok(); + expect(app.entity.name).to.eql('temp-app'); + + api.createServiceInstance({ name: 'temp-service', type: 'elk', plan: 'beta' }, (err, service) => { + expect(err).not.to.be.ok(); + expect(service.entity.name).to.eql('temp-service'); + + api.bindService({ app: 'temp-app', service: 'temp-service' }, (err, binding) => { + expect(err).not.to.be.ok(); + expect(binding.metadata.guid).to.be.ok(); + + api.unbindService({ app: 'temp-app', service: 'temp-service' }, (err) => { + expect(err).not.to.be.ok(); + + api.deleteApp({ name: 'temp-app' }, (err, result) => { + expect(err).not.to.be.ok(); + + api.deleteServiceInstance({ name: 'temp-service' }, (err, result) => { + expect(err).not.to.be.ok(); + done(); + }); + }); + }); + }); + }); + }); + + }); + + }); + + describe('createRoute and deleteRoute', () => { + + it('should work as expected', (done) => { + + api.createRoute({ + host: 'tmp-route-for-my-cf-test-space', + domain: 'scapp.io' + }, (err, result) => { + expect(err).not.to.be.ok(); + expect(result.metadata.guid).to.be.ok(); + + api.deleteRoute({ + host: 'tmp-route-for-my-cf-test-space', + domain: 'scapp.io' + }, (err) => { + expect(err).not.to.be.ok(); + done(); + }); + }); + + }); + + }); + + describe('mapRoute and unmapRoute', () => { + it('should work as expected', (done) => { + + api.createApp({name: 'tmp-app'}, (err, app) => { + expect(err).not.to.be.ok(); + api.mapRoute({ + hostname: 'tmp-route-for-my-cf-test-space' + , domain: 'scapp.io' + , app: 'tmp-app' + }, (err, route) => { + expect(err).not.to.be.ok(); + api.unmapRoute({ + hostname: 'tmp-route-for-my-cf-test-space' + , domain: 'scapp.io' + , app: 'tmp-app' + }, (err, route) => { + expect(err).not.to.be.ok(); + api.deleteApp({ + name: 'tmp-app' + }, (err) => { + expect(err).not.to.be.ok(); + done(); + }); + }); + }); + }); + }); + }); + + describe('route round-trip', () => { + + it('should work as expected', (done) => { + + api.createRoute({ + host: 'tmp-route-for-my-cf-test-space', + domain: 'scapp.io' + }, (err, route) => { + expect(err).not.to.be.ok(); + expect(route.metadata.guid).to.be.ok(); + + api.createApp({ name: 'tmp-app' }, (err, app) => { + expect(err).not.to.be.ok(); + + api.associateRoute({ app: 'tmp-app', route: { + host: 'tmp-route-for-my-cf-test-space', + domain: 'scapp.io' + } }, (err) => { + expect(err).not.to.be.ok(); + + api.disassociateRoute({ app: 'tmp-app', route: { + host: 'tmp-route-for-my-cf-test-space', + domain: 'scapp.io' + } }, (err) => { + expect(err).not.to.be.ok(); + + api.deleteRoute({ + host: 'tmp-route-for-my-cf-test-space', + domain: 'scapp.io' + }, (err) => { + expect(err).not.to.be.ok(); + + api.deleteApp({ + name: 'tmp-app' + }, (err) => { + expect(err).not.to.be.ok(); + done(); + }); + }); + }); + }); + + }); + + }); + + }); + + }); + + describe.skip('staging round-trip', () => { + + it('should work as expected', (done) => { + + this.timeout(120000); + + api.pushApp({ name: 'tmp-app', appPath: join(__dirname, '/sampleApp') }, (err, app) => { + expect(err).not.to.be.ok(); + expect(app.entity.name).to.eql('tmp-app'); + expect(_.find(api.actualDeploymentConfig.apps, { name: 'tmp-app' }).state).to.eql('STOPPED'); + + api.stageApp({ name: 'tmp-app' }, (err) => { + expect(err).not.to.be.ok(); + expect(_.find(api.actualDeploymentConfig.apps, { name: 'tmp-app' }).state).to.eql('STOPPED'); + + api.startAppAndWaitForInstances({ name: 'tmp-app', gracePeriod: 3, interval: 2 }, (err) => { + expect(err).not.to.be.ok(); + expect(_.find(api.actualDeploymentConfig.apps, { name: 'tmp-app' }).state).to.eql('STARTED'); + + api.deleteApp({ name: 'tmp-app' }, (err) => { + expect(err).not.to.be.ok(); + done(); + }); + }); + }); + }); + + }); + + }); + + describe('setEnv', () => { + + it('should work as expected', (done) => { + + api.createApp({ name: 'tmp-app' }, (err, app) => { + expect(err).not.to.be.ok(); + + api.setEnv({ name: 'tmp-app', env: { a: 'val 1', b: 'other' } }, (err, res) => { + expect(err).not.to.be.ok(); + expect(res.entity.environment_json.a).to.eql('val 1'); + + api.deleteApp({ + name: 'tmp-app' + }, (err) => { + expect(err).not.to.be.ok(); + done(); + }); + }); + + }); + + }); + + }); + + }); + +}); diff --git a/test/mocha.opts b/test/mocha.opts new file mode 100644 index 0000000..d05592f --- /dev/null +++ b/test/mocha.opts @@ -0,0 +1 @@ +-R spec -t 30000 diff --git a/test/sampleApp/.cfignore b/test/sampleApp/.cfignore new file mode 100644 index 0000000..9dfd89d --- /dev/null +++ b/test/sampleApp/.cfignore @@ -0,0 +1 @@ +special.file diff --git a/test/sampleApp/package.json b/test/sampleApp/package.json new file mode 100644 index 0000000..06278f5 --- /dev/null +++ b/test/sampleApp/package.json @@ -0,0 +1,11 @@ +{ + "name": "my_super_app", + "version": "0.0.1", + "private": true, + "scripts": { + "start": "node server.js" + }, + "dependencies": { + }, + "devDependencies": {} +} diff --git a/test/sampleApp/server.js b/test/sampleApp/server.js new file mode 100644 index 0000000..6ffb9c9 --- /dev/null +++ b/test/sampleApp/server.js @@ -0,0 +1,5 @@ +const http = require('http'); +http.createServer((req, res) => { + res.writeHead(200, {'Content-Type': 'text/plain'}); + res.end('Just for CF! ;-)\n'); +}).listen(process.env.PORT); diff --git a/test/sampleApp/special.file b/test/sampleApp/special.file new file mode 100644 index 0000000..89e3c6e --- /dev/null +++ b/test/sampleApp/special.file @@ -0,0 +1 @@ +do never push this file! diff --git a/test/sampleApp/test.sh b/test/sampleApp/test.sh new file mode 100755 index 0000000..4fee7d8 --- /dev/null +++ b/test/sampleApp/test.sh @@ -0,0 +1 @@ +echo "hi"