├── .babelrc ├── .eslintrc.yml ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── dist ├── bin │ └── meteor-google-cloud.js ├── lib │ ├── bundle.js │ ├── cli.js │ ├── google.js │ ├── helpers.js │ └── validation.js └── main.js ├── examples ├── example_basic │ ├── Dockerfile │ ├── app.yml │ └── settings.json └── example_with_env_in_settings │ ├── Dockerfile │ ├── app.yml │ └── settings.json ├── package-lock.json ├── package.json └── src ├── bin └── meteor-google-cloud.js ├── lib ├── bundle.js ├── cli.js ├── google.js ├── helpers.js └── validation.js └── main.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/env", 5 | { 6 | "useBuiltIns": "usage", 7 | "corejs": 3, 8 | "targets": { 9 | "node": "4.0.0" 10 | }, 11 | } 12 | ] 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | root: true 2 | 3 | extends: airbnb-base 4 | 5 | env: 6 | node: true 7 | 8 | rules: 9 | no-await-in-loop: off 10 | import/prefer-default-export: off 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### WebStorm ### 2 | .idea/ 3 | 4 | ### Linux ### 5 | *~ 6 | # temporary files which can be created if a process still has a handle open of a deleted file 7 | .fuse_hidden* 8 | # KDE directory preferences 9 | .directory 10 | # Linux trash folder which might appear on any partition or disk 11 | .Trash-* 12 | # .nfs files are created when an open file is removed but is still being accessed 13 | .nfs* 14 | 15 | ### macOS ### 16 | *.DS_Store 17 | .AppleDouble 18 | .LSOverride 19 | # Icon must end with two \r 20 | Icon 21 | # Thumbnails 22 | ._* 23 | # Files that might appear in the root of a volume 24 | .DocumentRevisions-V100 25 | .fseventsd 26 | .Spotlight-V100 27 | .TemporaryItems 28 | .Trashes 29 | .VolumeIcon.icns 30 | .com.apple.timemachine.donotpresent 31 | # Directories potentially created on remote AFP share 32 | .AppleDB 33 | .AppleDesktop 34 | Network Trash Folder 35 | Temporary Items 36 | .apdisk 37 | 38 | ### Node ### 39 | # Logs 40 | logs 41 | *.log 42 | npm-debug.log* 43 | yarn-debug.log* 44 | yarn-error.log* 45 | # Runtime data 46 | pids 47 | *.pid 48 | *.seed 49 | *.pid.lock 50 | # Directory for instrumented libs generated by jscoverage/JSCover 51 | lib-cov 52 | # Coverage directory used by tools like istanbul 53 | coverage 54 | # nyc test coverage 55 | .nyc_output 56 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 57 | .grunt 58 | # Bower dependency directory (https://bower.io/) 59 | bower_components 60 | # node-waf configuration 61 | .lock-wscript 62 | # Compiled binary addons (http://nodejs.org/api/addons.html) 63 | build/Release 64 | # Dependency directories 65 | node_modules/ 66 | jspm_packages/ 67 | # Typescript v1 declaration files 68 | typings/ 69 | # Optional npm cache directory 70 | .npm 71 | # Optional eslint cache 72 | .eslintcache 73 | # Optional REPL history 74 | .node_repl_history 75 | # Output of 'npm pack' 76 | *.tgz 77 | # Yarn Integrity file 78 | .yarn-integrity 79 | # dotenv environment variables file 80 | .env 81 | 82 | 83 | ### SublimeText ### 84 | # cache files for sublime text 85 | *.tmlanguage.cache 86 | *.tmPreferences.cache 87 | *.stTheme.cache 88 | # workspace files are user-specific 89 | *.sublime-workspace 90 | # project files should be checked into the repository, unless a significant 91 | # proportion of contributors will probably not be using SublimeText 92 | # *.sublime-project 93 | # sftp configuration file 94 | sftp-config.json 95 | # Package control specific files 96 | Package Control.last-run 97 | Package Control.ca-list 98 | Package Control.ca-bundle 99 | Package Control.system-ca-bundle 100 | Package Control.cache/ 101 | Package Control.ca-certs/ 102 | Package Control.merged-ca-bundle 103 | Package Control.user-ca-bundle 104 | oscrypto-ca-bundle.crt 105 | bh_unicode_properties.cache 106 | # Sublime-github package stores a github token in this file 107 | # https://packagecontrol.io/packages/sublime-github 108 | GitHub.sublime-settings 109 | 110 | ### Windows ### 111 | # Windows thumbnail cache files 112 | Thumbs.db 113 | ehthumbs.db 114 | ehthumbs_vista.db 115 | # Folder config file 116 | Desktop.ini 117 | # Recycle Bin used on file shares 118 | $RECYCLE.BIN/ 119 | # Windows Installer files 120 | *.cab 121 | *.msi 122 | *.msm 123 | *.msp 124 | # Windows shortcuts 125 | *.lnk -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | ### Babel ### 2 | source/ 3 | 4 | ### RTD Documentation ### 5 | documentation/ 6 | 7 | ### WebStorm ### 8 | .idea/ 9 | 10 | ### Linux ### 11 | *~ 12 | # temporary files which can be created if a process still has a handle open of a deleted file 13 | .fuse_hidden* 14 | # KDE directory preferences 15 | .directory 16 | # Linux trash folder which might appear on any partition or disk 17 | .Trash-* 18 | # .nfs files are created when an open file is removed but is still being accessed 19 | .nfs* 20 | 21 | ### macOS ### 22 | *.DS_Store 23 | .AppleDouble 24 | .LSOverride 25 | # Icon must end with two \r 26 | Icon 27 | # Thumbnails 28 | ._* 29 | # Files that might appear in the root of a volume 30 | .DocumentRevisions-V100 31 | .fseventsd 32 | .Spotlight-V100 33 | .TemporaryItems 34 | .Trashes 35 | .VolumeIcon.icns 36 | .com.apple.timemachine.donotpresent 37 | # Directories potentially created on remote AFP share 38 | .AppleDB 39 | .AppleDesktop 40 | Network Trash Folder 41 | Temporary Items 42 | .apdisk 43 | 44 | ### Node ### 45 | # Logs 46 | logs 47 | *.log 48 | npm-debug.log* 49 | yarn-debug.log* 50 | yarn-error.log* 51 | # Runtime data 52 | pids 53 | *.pid 54 | *.seed 55 | *.pid.lock 56 | # Directory for instrumented libs generated by jscoverage/JSCover 57 | lib-cov 58 | # Coverage directory used by tools like istanbul 59 | coverage 60 | # nyc test coverage 61 | .nyc_output 62 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 63 | .grunt 64 | # Bower dependency directory (https://bower.io/) 65 | bower_components 66 | # node-waf configuration 67 | .lock-wscript 68 | # Compiled binary addons (http://nodejs.org/api/addons.html) 69 | build/Release 70 | # Dependency directories 71 | node_modules/ 72 | jspm_packages/ 73 | # Typescript v1 declaration files 74 | typings/ 75 | # Optional npm cache directory 76 | .npm 77 | # Optional eslint cache 78 | .eslintcache 79 | # Optional REPL history 80 | .node_repl_history 81 | # Output of 'npm pack' 82 | *.tgz 83 | # Yarn Integrity file 84 | .yarn-integrity 85 | # dotenv environment variables file 86 | .env 87 | 88 | 89 | ### SublimeText ### 90 | # cache files for sublime text 91 | *.tmlanguage.cache 92 | *.tmPreferences.cache 93 | *.stTheme.cache 94 | # workspace files are user-specific 95 | *.sublime-workspace 96 | # project files should be checked into the repository, unless a significant 97 | # proportion of contributors will probably not be using SublimeText 98 | # *.sublime-project 99 | # sftp configuration file 100 | sftp-config.json 101 | # Package control specific files 102 | Package Control.last-run 103 | Package Control.ca-list 104 | Package Control.ca-bundle 105 | Package Control.system-ca-bundle 106 | Package Control.cache/ 107 | Package Control.ca-certs/ 108 | Package Control.merged-ca-bundle 109 | Package Control.user-ca-bundle 110 | oscrypto-ca-bundle.crt 111 | bh_unicode_properties.cache 112 | # Sublime-github package stores a github token in this file 113 | # https://packagecontrol.io/packages/sublime-github 114 | GitHub.sublime-settings 115 | 116 | ### Windows ### 117 | # Windows thumbnail cache files 118 | Thumbs.db 119 | ehthumbs.db 120 | ehthumbs_vista.db 121 | # Folder config file 122 | Desktop.ini 123 | # Recycle Bin used on file shares 124 | $RECYCLE.BIN/ 125 | # Windows Installer files 126 | *.cab 127 | *.msi 128 | *.msm 129 | *.msp 130 | # Windows shortcuts 131 | *.lnk 132 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 EducationLink 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Meteor Google Cloud 2 | 3 | [![Project Status: Active – The project has reached a stable, usable state and is being actively developed.](https://www.repostatus.org/badges/latest/active.svg)](https://www.repostatus.org/#active) 4 | 5 | A command line tool for deploying Meteor applications on Google Cloud App Engine Flexible. 6 | 7 | ## What is Google Cloud App Engine Flexible? 8 | 9 | App Engine allows developers to focus on doing what they do best, writing code. Based on Google Compute Engine, the App Engine flexible environment automatically scales your app up and down while balancing the load. 10 | 11 | *Meteor needs to run on App Engine Flexible, not Standard.* 12 | 13 | App Engine manages your virtual machines, ensuring that: 14 | 15 | - Instances are health-checked, healed as necessary, and co-located with other services within the project. 16 | Critical, backwards compatible updates are automatically applied to the underlying operating system. 17 | - VM instances are automatically located by geographical region according to the settings in your project. Google's management services ensure that all of a project's VM instances are co-located for optimal performance. 18 | - VM instances are restarted on a weekly basis. During restarts Google's management services will apply any necessary operating system and security updates. 19 | - You always have root access to Compute Engine VM instances. SSH access to VM instances in the flexible environment is disabled by default. If you choose, you can enable root access to your app's VM instances. 20 | 21 | For more information, check: [App Engine Flexible Environment's page](https://cloud.google.com/appengine/docs/flexible/). 22 | 23 | ## App Engine Flexible Pricing 24 | 25 | Because we run Meteor on the Flexible environment you may not be able to use the free tier of App Engine Standard. For the first year you may have $300 in credit per month, but be aware of the costs: 26 | 27 | - [Pricing calculator.](https://cloud.google.com/products/calculator/#id=126a7009-debc-49e7-8e36-f7d5574ecfc1) 28 | - [More info on App Engine billing.](https://stackoverflow.com/questions/47125661/pricing-of-google-app-engine-flexible-env-a-500-lesson) 29 | 30 | ## Installation 31 | 32 | ```bash 33 | npm install meteor-google-cloud -g 34 | ``` 35 | 36 | ## Deploying 37 | 38 | To deploy to App Engine Flexible, follow the steps bellow: 39 | 40 | ### 1. Install gcloud CLI 41 | 42 | ```bash 43 | Follow the guide on: https://cloud.google.com/sdk/install 44 | ``` 45 | 46 | ### 2. Init Meteor Google Cloud 47 | 48 | If this is the first time you deploy, you will need some specific files on your repo, run the command below to get them automatically generated. 49 | 50 | ```bash 51 | meteor-google-cloud --init 52 | ``` 53 | 54 | ### 3. Set your App Engine Flexible settings 55 | 56 | ```bash 57 | cd ./deploy 58 | ls 59 | Dockerfile app.yml settings.json 60 | ``` 61 | 62 | - Dockerfile: you can customize your Docker image, if you don't need to or don't know how to, you can either delete this fle or leave iit as is. 63 | - app.yml: The settings and preferences of your App Engine service goes in here, check [Google's app.yml documentation](https://cloud.google.com/appengine/docs/standard/nodejs/config/appref) for full options. 64 | - settings.json: This is your normal Meteor settings file, you will need to have the special key `meteor-google-cloud` for the deployment settings. 65 | - Required keys: 66 | - `project`: The project name of the project on Google Cloud to use. 67 | - Other keys: You can add any option you would like here, and they will be passed to `gcloud deploy app` command, for the full list, check [Google's gcloud deploy documentation](https://cloud.google.com/sdk/gcloud/reference/app/deploy). 68 | 69 | ### 4. Deploy 70 | 71 | ```bash 72 | meteor-google-cloud --settings deploy/settings.json --app deploy/app.yml --docker deploy/Dockerfile 73 | ``` 74 | 75 | P.S: It may take a few minutes to build your app, which may appear to be unresponsive, but it's not, just wait. 76 | 77 | ## CLI options 78 | 79 | The Meteor Google Cloud CLI supports the following options: 80 | 81 | ```bash 82 | -v, --version output the version number 83 | -i, --init init necessary files on your repo 84 | -b, --build-only build only, without deploying to gcp 85 | -s, --settings path to settings file (settings.json) 86 | -c, --app path to app.yaml config file 87 | -d, --docker path to Dockerfile file 88 | -p, --project path of the directory of your Meteor project 89 | -o, --output-dir path of the output directory of build files 90 | -v, --verbose enable verbose mode 91 | -q, --quiet enable quite mode 92 | -ci, --ci add --allow-superuser flag in meteor commands for running in CI 93 | -h, --help output usage information 94 | --node-version set custom node version 95 | --npm-version set custom npm version 96 | ``` 97 | 98 | ## FAQ 99 | **1. Does App Engine supports websockets?** 100 | Yes, announced in February 5, 2019, [more info](https://cloud.google.com/blog/products/application-development/introducing-websockets-support-for-app-engine-flexible-environment). 101 | 102 | **2. Does App Engine supports session affinity?** Yes. 103 | 104 | **3. Do I get auto scaling?** Yes. 105 | 106 | **4. Do I get auto healing?** Yes. 107 | 108 | **5. Can I add the environment variables to the `settings.json?`** Yes. Just create add a property `env_variables` to `meteor-google-cloud`. It will prefer those over the ones in your `app.yaml`. 109 | ## Support 110 | 111 | We welcome any questions, contributions or bug reports in the GitHub [issue tracker](https://github.com/EducationLink/meteor-google-cloud/issues). 112 | 113 | ## Meteor Azure 114 | 115 | This package was heavily inspired on `meteor-azure`, a deployment packge for Meteor applicatons on Microsoft Azure, [click here](https://github.com/fractal-code/meteor-azure) for more information. 116 | 117 | ## License 118 | 119 | [MIT](https://github.com/EducationLink/meteor-google-cloud/blob/master/LICENSE) 120 | -------------------------------------------------------------------------------- /dist/bin/meteor-google-cloud.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | "use strict"; 3 | 4 | require("../main"); -------------------------------------------------------------------------------- /dist/lib/bundle.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.default = compileBundle; 7 | 8 | var _tmp = _interopRequireDefault(require("tmp")); 9 | 10 | var _path = _interopRequireDefault(require("path")); 11 | 12 | var _shelljs = _interopRequireDefault(require("shelljs")); 13 | 14 | var _winston = _interopRequireDefault(require("winston")); 15 | 16 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 17 | 18 | // Bundle compilation method 19 | function compileBundle() { 20 | var _ref = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}, 21 | dir = _ref.dir, 22 | _ref$workingDir = _ref.workingDir, 23 | workingDir = _ref$workingDir === void 0 ? _tmp.default.dirSync().name : _ref$workingDir, 24 | ci = _ref.ci, 25 | keep = _ref.keep; 26 | 27 | var customMeteorProjectDirShellEx = `cd ${dir} &&`; 28 | 29 | _winston.default.info('Compiling application bundle'); // Generate Meteor build 30 | 31 | 32 | _winston.default.debug(`generate meteor build at ${workingDir}`); 33 | 34 | if (!keep) { 35 | _winston.default.debug(`removing ${workingDir}`); 36 | 37 | _shelljs.default.exec(`rm -rf ${workingDir}`); 38 | } else { 39 | _winston.default.debug(`keeping ${workingDir}, if it exists`); 40 | } 41 | 42 | _shelljs.default.exec(`${dir ? customMeteorProjectDirShellEx : ''} meteor build ${workingDir} ${ci ? '--allow-superuser' : ''} --directory --server-only --architecture os.linux.x86_64`); // Cleanup broken symlinks 43 | 44 | 45 | _winston.default.debug('checking for broken symlinks'); 46 | 47 | _shelljs.default.find(_path.default.join(workingDir, 'bundle')).forEach(function (symlinkPath) { 48 | // Matches symlinks that do not exist 49 | if (_shelljs.default.test('-L', symlinkPath) && !_shelljs.default.test('-e', symlinkPath)) { 50 | // Delete file 51 | _shelljs.default.rm('-f', symlinkPath); 52 | 53 | _winston.default.debug(`deleted symlink at '${symlinkPath}'`); 54 | } 55 | }); 56 | 57 | return { 58 | workingDir 59 | }; 60 | } -------------------------------------------------------------------------------- /dist/lib/cli.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | require("core-js/modules/es.symbol"); 4 | 5 | require("core-js/modules/es.symbol.description"); 6 | 7 | require("core-js/modules/es.object.to-string"); 8 | 9 | require("core-js/modules/es.promise"); 10 | 11 | Object.defineProperty(exports, "__esModule", { 12 | value: true 13 | }); 14 | exports.default = startup; 15 | 16 | require("regenerator-runtime/runtime"); 17 | 18 | var _commander = _interopRequireDefault(require("commander")); 19 | 20 | var _shelljs = _interopRequireDefault(require("shelljs")); 21 | 22 | var _tmp = _interopRequireDefault(require("tmp")); 23 | 24 | var _updateNotifier = _interopRequireDefault(require("update-notifier")); 25 | 26 | var _winston = _interopRequireDefault(require("winston")); 27 | 28 | var _package = _interopRequireDefault(require("../../package.json")); 29 | 30 | var _validation = require("./validation"); 31 | 32 | var _bundle = _interopRequireDefault(require("./bundle")); 33 | 34 | var _google = _interopRequireDefault(require("./google")); 35 | 36 | var _helpers = require("./helpers"); 37 | 38 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 39 | 40 | function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { Promise.resolve(value).then(_next, _throw); } } 41 | 42 | function _asyncToGenerator(fn) { return function () { var self = this, args = arguments; return new Promise(function (resolve, reject) { var gen = fn.apply(self, args); function _next(value) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value); } function _throw(err) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err); } _next(undefined); }); }; } 43 | 44 | // Notify user of available updates 45 | (0, _updateNotifier.default)({ 46 | pkg: _package.default 47 | }).notify(); // Configure CLI 48 | 49 | _commander.default.description(_package.default.description).version(`v${_package.default.version}`, '-v, --version').option('-i, --init', 'init necessary files on your repo').option('-b, --build-only', 'build bundle only').option('-s, --settings ', 'path to settings file (settings.json)').option('-c, --app ', 'path to app.yaml config file').option('-d, --docker ', 'path to Dockerfile file').option('-p, --project ', 'path of the directory of your Meteor project').option('-v, --verbose', 'enable verbose mode').option('-q, --quiet', 'enable quite mode').option('-ci, --ci', 'add --allow-superuser flag in meteor commands for running in CI').option('-o, --output-dir ', 'build files output directory').option('-k, --keep-output-dir', 'do not remove the output directory before start').option('--node-version ', 'set custom node version').option('--npm-version ${this.workingDir}/bundle/app.yaml`); 87 | 88 | _shelljs.default.sed('-i', '{{ METEOR_SETTINGS }}', `'${compactSettings}'`, `${this.workingDir}/bundle/app.yaml`); 89 | 90 | var resultAppYaml = _jsYaml.default.safeLoad(_fs.default.readFileSync(`${this.workingDir}/bundle/app.yaml`)); 91 | 92 | resultAppYaml.env_variables.MONGO_URL = '*****'; 93 | resultAppYaml.env_variables.MONGO_OPLOG_URL = '*****'; 94 | 95 | _winston.default.debug(`the following app.yaml will be used:\n${JSON.stringify(resultAppYaml)}`); 96 | 97 | var nodeVersion = this.nodeVersion || _shelljs.default.exec(`meteor node -v ${this.ci ? '--allow-superuser' : ''}`, { 98 | silent: true 99 | }).stdout.trim(); 100 | 101 | var npmVersion = this.npmVersion || _shelljs.default.exec(`meteor npm -v ${this.ci ? '--allow-superuser' : ''}`, { 102 | silent: true 103 | }).stdout.trim(); 104 | 105 | _winston.default.debug(`set Node to ${nodeVersion}`); 106 | 107 | _winston.default.debug(`set NPM to ${npmVersion}`); // Create Dockerfile 108 | 109 | 110 | var docker = this.dockerFile.replace('{{ nodeVersion }}', nodeVersion).replace('{{ npmVersion }}', npmVersion); 111 | 112 | _shelljs.default.exec(`echo '${docker}' >${this.workingDir}/bundle/Dockerfile`); 113 | 114 | _winston.default.debug(`the following Dockerfile will be used:\n${JSON.stringify(_jsYaml.default.safeLoad(_fs.default.readFileSync(`${this.workingDir}/bundle/Dockerfile`)))}`); 115 | } 116 | }, { 117 | key: "deployBundle", 118 | value: function () { 119 | var _deployBundle = _asyncToGenerator( 120 | /*#__PURE__*/ 121 | regeneratorRuntime.mark(function _callee() { 122 | var settings, flags; 123 | return regeneratorRuntime.wrap(function _callee$(_context) { 124 | while (1) { 125 | switch (_context.prev = _context.next) { 126 | case 0: 127 | _winston.default.debug('deploy to App Engine'); // Allow users to pass any option to gcloud app deploy 128 | 129 | 130 | settings = this.googleCloudSettings; 131 | flags = Object.keys(settings).map(function (key) { 132 | if (key !== 'env_variables') { 133 | var value = settings[key]; // Only some flags actually require a value (e.g. stop-previous-version) 134 | 135 | if (value) { 136 | return `--${key}=${settings[key]}`; 137 | } 138 | 139 | return `--${key}`; 140 | } 141 | }).join(' '); 142 | 143 | _winston.default.debug(`set flags for deploy: ${flags}`); 144 | 145 | _shelljs.default.exec(`cd ${this.workingDir}/bundle && gcloud app deploy -q ${flags}`); 146 | 147 | case 5: 148 | case "end": 149 | return _context.stop(); 150 | } 151 | } 152 | }, _callee, this); 153 | })); 154 | 155 | function deployBundle() { 156 | return _deployBundle.apply(this, arguments); 157 | } 158 | 159 | return deployBundle; 160 | }() 161 | }]); 162 | 163 | return AppEngineInstance; 164 | }(); 165 | 166 | exports.default = AppEngineInstance; -------------------------------------------------------------------------------- /dist/lib/helpers.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | require("core-js/modules/es.string.replace"); 4 | 5 | Object.defineProperty(exports, "__esModule", { 6 | value: true 7 | }); 8 | exports.initRepo = initRepo; 9 | 10 | var _fs = _interopRequireDefault(require("fs")); 11 | 12 | var _shelljs = _interopRequireDefault(require("shelljs")); 13 | 14 | var _winston = _interopRequireDefault(require("winston")); 15 | 16 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 17 | 18 | // Helper functions 19 | function initRepo() { 20 | var dirName = __dirname; 21 | var path = dirName.replace('/dist/lib', '/examples/'); 22 | var dockerFile = 'Dockerfile'; 23 | var app = 'app.yml'; 24 | var settings = 'settings.json'; 25 | 26 | _shelljs.default.exec('mkdir -p deploy'); 27 | 28 | var newFolderPath = `${process.cwd()}/deploy`; // Init app.yaml 29 | 30 | if (!_fs.default.existsSync('./deploy/app.yml')) { 31 | _shelljs.default.cp('-R', `${path}${app}`, newFolderPath); 32 | 33 | _winston.default.info('app.yml created on deploy/ it has the default/minimum settings you need'); 34 | } else { 35 | _winston.default.error('app.yml already exists on deploy/'); 36 | } // Init Dockerfile 37 | 38 | 39 | if (!_fs.default.existsSync('./deploy/Dockerfile')) { 40 | _shelljs.default.cp('-R', `${path}${dockerFile}`, newFolderPath); 41 | 42 | _winston.default.info('Dockerfile created on deploy/'); 43 | } else { 44 | _winston.default.error('Dockerfile already exists on deploy/'); 45 | } // Init settings 46 | 47 | 48 | if (!_fs.default.existsSync('./deploy/settings.json')) { 49 | _shelljs.default.cp('-R', `${path}${settings}`, newFolderPath); 50 | 51 | _winston.default.info('Meteor settings file created on deploy/'); 52 | } else { 53 | _winston.default.error('settings.json already exists on deploy/'); 54 | } 55 | } -------------------------------------------------------------------------------- /dist/lib/validation.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | require("core-js/modules/es.object.assign"); 4 | 5 | require("core-js/modules/es.string.replace"); 6 | 7 | require("core-js/modules/es.string.split"); 8 | 9 | Object.defineProperty(exports, "__esModule", { 10 | value: true 11 | }); 12 | exports.validateGCloud = validateGCloud; 13 | exports.validateMeteor = validateMeteor; 14 | exports.validateSettings = validateSettings; 15 | exports.validateApp = validateApp; 16 | exports.getDocker = getDocker; 17 | exports.validateEnv = validateEnv; 18 | 19 | var _fs = _interopRequireDefault(require("fs")); 20 | 21 | var _jsonfile = _interopRequireDefault(require("jsonfile")); 22 | 23 | var _jsYaml = _interopRequireDefault(require("js-yaml")); 24 | 25 | var _winston = _interopRequireDefault(require("winston")); 26 | 27 | var _lodash = _interopRequireDefault(require("lodash.nth")); 28 | 29 | var _lodash2 = _interopRequireDefault(require("lodash.dropright")); 30 | 31 | var _joi = _interopRequireDefault(require("@hapi/joi")); 32 | 33 | var _commandExists = _interopRequireDefault(require("command-exists")); 34 | 35 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 36 | 37 | // Validation methods 38 | function validateGCloud() { 39 | // Ensure gcloud CLI is installed 40 | _winston.default.debug('check gcloud is installed'); 41 | 42 | if (_commandExists.default.sync('gcloud') === false) { 43 | throw new Error('gcloud is not installed'); 44 | } 45 | } 46 | 47 | function validateMeteor() { 48 | var release; // Ensure Meteor CLI is installed 49 | 50 | _winston.default.debug('check Meteor is installed'); 51 | 52 | if (_commandExists.default.sync('meteor') === false) { 53 | throw new Error('Meteor is not installed'); 54 | } // Determine current release/packages from '.meteor' directory 55 | 56 | 57 | try { 58 | release = _fs.default.readFileSync('.meteor/release', 'utf8'); 59 | } catch (error) { 60 | /* Abort the program if files are not found, this is a strong 61 | indication we may not be in the root project directory */ 62 | throw new Error('You must be in a Meteor project directory'); 63 | } // Determine major/minor version numbers by stripping non-numeric characters from release 64 | 65 | 66 | var versionNumbers = release.replace(/[^0-9.]/g, '').split('.'); 67 | var majorVersion = Number.parseInt(versionNumbers[0], 10); 68 | var minorVersion = Number.parseInt(versionNumbers[1], 10); // Ensure current Meteor release is >= 1.4 69 | 70 | _winston.default.debug('check current Meteor release >= 1.4'); 71 | 72 | if (majorVersion < 1 || majorVersion === 1 && minorVersion < 4) { 73 | throw new Error('Meteor version must be >= 1.4'); 74 | } 75 | } 76 | 77 | function validateSettings(filePath) { 78 | var settingsFile; 79 | 80 | _winston.default.info(`Validating settings file (${filePath})`); // Ensure valid json exists 81 | 82 | 83 | _winston.default.debug('check valid json exists'); 84 | 85 | try { 86 | settingsFile = _jsonfile.default.readFileSync(filePath); 87 | } catch (error) { 88 | throw new Error(`Could not read settings file at '${filePath}'`); 89 | } // Define schema 90 | 91 | 92 | var meteorGoogleCloudConfig = _joi.default.object({ 93 | project: _joi.default.string() 94 | }).unknown(true); 95 | 96 | var schema = _joi.default.object({ 97 | 'meteor-google-cloud': meteorGoogleCloudConfig 98 | }).unknown(true); // Ensure settings data follows schema 99 | 100 | 101 | _winston.default.debug('check data follows schema'); 102 | 103 | _joi.default.validate(settingsFile, schema, { 104 | presence: 'required' 105 | }, function (error) { 106 | if (error) { 107 | // Pull error from bottom of stack to get most specific/useful details 108 | var lastError = (0, _lodash.default)(error.details, -1); // Locate parent of non-compliant field, or otherwise mark as top level 109 | 110 | var pathToParent = 'top level'; 111 | 112 | if (lastError.path.length > 1) { 113 | pathToParent = `"${(0, _lodash2.default)(lastError.path).join('.')}"`; 114 | } // Report user-friendly error with relevant complaint/context to errors 115 | 116 | 117 | throw new Error(`Settings file (${filePath}): ${lastError.message} in ${pathToParent}`); 118 | } 119 | }); 120 | 121 | return settingsFile; 122 | } 123 | 124 | function validateApp(filePath) { 125 | var appFile; 126 | 127 | _winston.default.info(`Validating app.yml file (${filePath})`); // Ensure valid json exists 128 | 129 | 130 | _winston.default.debug('check app yaml exists'); 131 | 132 | try { 133 | appFile = _jsYaml.default.safeLoad(_fs.default.readFileSync(filePath)); 134 | } catch (error) { 135 | throw new Error(`Could not read app.yml file at '${filePath}'`); 136 | } // Define schema 137 | 138 | 139 | var schema = _joi.default.object({ 140 | service: _joi.default.string(), 141 | runtime: _joi.default.string(), 142 | env: _joi.default.string(), 143 | threadsafe: _joi.default.boolean(), 144 | automatic_scaling: _joi.default.object({ 145 | max_num_instances: _joi.default.number().min(1) 146 | }).optional().unknown(true), 147 | resources: _joi.default.object({ 148 | cpu: _joi.default.number().min(1), 149 | memory_gb: _joi.default.number(), 150 | disk_size_gb: _joi.default.number().min(10) 151 | }).optional().unknown(true), 152 | network: _joi.default.object({ 153 | session_affinity: _joi.default.boolean() 154 | }) 155 | }).unknown(true); // allow unknown keys (at the top level) for extra settings 156 | // (https://cloud.google.com/appengine/docs/admin-api/reference/rest/v1/apps.services.versions) 157 | // Ensure settings app yaml follows schema 158 | 159 | 160 | _winston.default.debug('check app yaml follows schema'); 161 | 162 | _joi.default.validate(appFile, schema, { 163 | presence: 'required' 164 | }, function (error) { 165 | if (error) { 166 | // Pull error from bottom of stack to get most specific/useful details 167 | var lastError = (0, _lodash.default)(error.details, -1); // Locate parent of non-compliant field, or otherwise mark as top level 168 | 169 | var pathToParent = 'top level'; 170 | 171 | if (lastError.path.length > 1) { 172 | pathToParent = `"${(0, _lodash2.default)(lastError.path).join('.')}"`; 173 | } // Report user-friendly error with relevant complaint/context to errors 174 | 175 | 176 | throw new Error(`App.yaml file (${filePath}): ${lastError.message} in ${pathToParent}`); 177 | } 178 | }); // Make sure threadsafe is always true otherwise Meteor will not work properly 179 | 180 | 181 | if (!appFile.threadsafe) { 182 | _winston.default.debug('found threadsafe false, change threadsafe to true'); 183 | 184 | Object.assign(appFile, { 185 | threadsafe: true 186 | }); 187 | } 188 | 189 | return appFile; 190 | } 191 | 192 | function getDocker(filePath) { 193 | var dockerFile; 194 | 195 | _winston.default.info(`Reading Dockerfile (${filePath})`); // Ensure file exists 196 | 197 | 198 | _winston.default.debug('check dockerfile exists'); 199 | 200 | try { 201 | dockerFile = _fs.default.readFileSync(filePath, 'utf8'); 202 | } catch (error) { 203 | throw new Error(`Could not read Dockerfile at '${filePath}'`); 204 | } 205 | 206 | return dockerFile; 207 | } 208 | 209 | function validateEnv(settings, app) { 210 | _winston.default.debug('check either settings.json or app.yaml contain the required env'); 211 | 212 | var appSchema = _joi.default.object({ 213 | env_variables: _joi.default.object({ 214 | ROOT_URL: _joi.default.string(), 215 | MONGO_URL: _joi.default.string() 216 | }).unknown(true) 217 | }).unknown(true); 218 | 219 | var settingsValidation = _joi.default.validate(settings, _joi.default.object({ 220 | 'meteor-google-cloud': appSchema 221 | }).unknown(true), { 222 | presence: 'required' 223 | }); 224 | 225 | var appValidation = _joi.default.validate(app, appSchema, { 226 | presence: 'required' 227 | }); 228 | 229 | if (settingsValidation.error === null) { 230 | return settings['meteor-google-cloud'].env_variables; 231 | } 232 | 233 | if (appValidation.error === null) { 234 | return app.env_variables; 235 | } 236 | 237 | throw new Error('neither app.yaml, nor settings.json did contain the env_variables'); 238 | } -------------------------------------------------------------------------------- /dist/main.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _cli = _interopRequireDefault(require("./lib/cli")); 4 | 5 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 6 | 7 | // Entry point 8 | (0, _cli.default)(); -------------------------------------------------------------------------------- /examples/example_basic/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM gcr.io/google_appengine/nodejs 2 | RUN install_node {{ nodeVersion }} 3 | RUN npm install npm@{{ npmVersion }} 4 | RUN node -v 5 | RUN npm -v 6 | COPY . /app/ 7 | RUN (cd programs/server && npm install --unsafe-perm) 8 | CMD node main.js 9 | -------------------------------------------------------------------------------- /examples/example_basic/app.yml: -------------------------------------------------------------------------------- 1 | runtime: custom 2 | service: my-service-name 3 | env: flex 4 | threadsafe: true 5 | zones: 6 | - us-east1-b 7 | - us-east1-c 8 | resources: 9 | cpu: 1 10 | memory_gb: 0.5 11 | disk_size_gb: 10 12 | network: 13 | session_affinity: true 14 | automatic_scaling: 15 | max_num_instances: 2 16 | env_variables: 17 | ROOT_URL: 18 | MONGO_URL: 19 | MAIL_URL: 20 | skip_files: 21 | - ^(.*/)?\.dockerignore$ 22 | - ^(.*/)?\yarn-error.log$ 23 | - ^(.*/)?\.git$ 24 | - ^(.*/)?\.hg$ 25 | - ^(.*/)?\.svn$ 26 | 27 | -------------------------------------------------------------------------------- /examples/example_basic/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "public": {}, 3 | "private": {}, 4 | "meteor-google-cloud": { 5 | "project": "", 6 | "stop-previous-version": "" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /examples/example_with_env_in_settings/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM gcr.io/google_appengine/nodejs 2 | RUN install_node {{ nodeVersion }} 3 | RUN npm install npm@{{ npmVersion }} 4 | RUN node -v 5 | RUN npm -v 6 | COPY . /app/ 7 | RUN (cd programs/server && npm install --unsafe-perm) 8 | CMD node main.js 9 | -------------------------------------------------------------------------------- /examples/example_with_env_in_settings/app.yml: -------------------------------------------------------------------------------- 1 | runtime: custom 2 | service: my-service-name 3 | env: flex 4 | threadsafe: true 5 | zones: 6 | - us-east1-b 7 | - us-east1-c 8 | resources: 9 | cpu: 1 10 | memory_gb: 0.5 11 | disk_size_gb: 10 12 | network: 13 | session_affinity: true 14 | automatic_scaling: 15 | max_num_instances: 2 16 | skip_files: 17 | - ^(.*/)?\.dockerignore$ 18 | - ^(.*/)?\yarn-error.log$ 19 | - ^(.*/)?\.git$ 20 | - ^(.*/)?\.hg$ 21 | - ^(.*/)?\.svn$ 22 | 23 | -------------------------------------------------------------------------------- /examples/example_with_env_in_settings/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "public": {}, 3 | "private": {}, 4 | "meteor-google-cloud": { 5 | "project": "", 6 | "stop-previous-version": "", 7 | "env_variables": { 8 | "MONGO_URL": "mongodb://user:pw@bla.com", 9 | "ROOT_URL": "https://example.de" 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "meteor-google-cloud", 3 | "version": "1.2.1", 4 | "description": "Automate Meteor deployments on Google Cloud App Service Flexible", 5 | "main": "dist/main.js", 6 | "bin": { 7 | "meteor-google-cloud": "dist/bin/meteor-google-cloud.js" 8 | }, 9 | "watch": { 10 | "build": "src" 11 | }, 12 | "scripts": { 13 | "build": "babel src --out-dir dist", 14 | "dev": "npm-watch build", 15 | "lint": "eslint source" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/EducationLink/meteor-google-cloud.git" 20 | }, 21 | "keywords": [ 22 | "meteor", 23 | "deployment", 24 | "hosting", 25 | "google", 26 | "gcp", 27 | "app-engine" 28 | ], 29 | "directories": { 30 | "examples": "examples" 31 | }, 32 | "author": "Raphael Arias ", 33 | "license": "MIT", 34 | "bugs": "https://github.com/EducationLink/meteor-google-cloud/issues", 35 | "homepage": "https://github.com/EducationLink/meteor-google-cloud", 36 | "devDependencies": { 37 | "@babel/cli": "^7.2.3", 38 | "@babel/core": "^7.4.0", 39 | "@babel/preset-env": "^7.4.2", 40 | "eslint": "^5.15.3", 41 | "eslint-config-airbnb-base": "^13.1.0", 42 | "eslint-plugin-import": "^2.16.0", 43 | "npm-watch": "^0.6.0" 44 | }, 45 | "dependencies": { 46 | "@hapi/joi": "^15.0.0", 47 | "axios": "^0.18.0", 48 | "command-exists": "^1.2.8", 49 | "commander": "^2.19.0", 50 | "core-js": "^3.0.0", 51 | "delay": "^4.1.0", 52 | "js-yaml": "^3.13.1", 53 | "jsesc": "^2.5.2", 54 | "jsonfile": "^5.0.0", 55 | "lodash.defaultto": "^4.14.0", 56 | "lodash.dropright": "^4.1.1", 57 | "lodash.nth": "^4.11.2", 58 | "lodash.omit": "^4.5.0", 59 | "p-iteration": "^1.1.8", 60 | "regenerator-runtime": "^0.13.2", 61 | "shelljs": "^0.8.3", 62 | "tar": "^4.4.8", 63 | "tmp": "^0.1.0", 64 | "update-notifier": "^2.1.0", 65 | "winston": "^2.4.4" 66 | }, 67 | "engines": { 68 | "node": ">=4" 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/bin/meteor-google-cloud.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import '../main'; 4 | -------------------------------------------------------------------------------- /src/lib/bundle.js: -------------------------------------------------------------------------------- 1 | // Bundle compilation method 2 | 3 | import tmp from 'tmp'; 4 | import path from 'path'; 5 | import shell from 'shelljs'; 6 | import winston from 'winston'; 7 | 8 | export default function compileBundle({ 9 | dir, workingDir = tmp.dirSync().name, ci, keep, 10 | } = {}) { 11 | const customMeteorProjectDirShellEx = `cd ${dir} &&`; 12 | 13 | winston.info('Compiling application bundle'); 14 | 15 | // Generate Meteor build 16 | winston.debug(`generate meteor build at ${workingDir}`); 17 | if (!keep) { 18 | winston.debug(`removing ${workingDir}`); 19 | shell.exec(`rm -rf ${workingDir}`); 20 | } else { 21 | winston.debug(`keeping ${workingDir}, if it exists`); 22 | } 23 | shell.exec(`${dir ? customMeteorProjectDirShellEx : ''} meteor build ${workingDir} ${ci ? '--allow-superuser' : ''} --directory --server-only --architecture os.linux.x86_64`); 24 | 25 | // Cleanup broken symlinks 26 | winston.debug('checking for broken symlinks'); 27 | shell.find(path.join(workingDir, 'bundle')).forEach((symlinkPath) => { 28 | // Matches symlinks that do not exist 29 | if (shell.test('-L', symlinkPath) && !shell.test('-e', symlinkPath)) { 30 | // Delete file 31 | shell.rm('-f', symlinkPath); 32 | winston.debug(`deleted symlink at '${symlinkPath}'`); 33 | } 34 | }); 35 | return { workingDir }; 36 | } 37 | -------------------------------------------------------------------------------- /src/lib/cli.js: -------------------------------------------------------------------------------- 1 | // CLI setup 2 | 3 | import program from 'commander'; 4 | import shell from 'shelljs'; 5 | import tmp from 'tmp'; 6 | import updateNotifier from 'update-notifier'; 7 | import winston from 'winston'; 8 | import pkg from '../../package.json'; 9 | import { 10 | validateGCloud, validateSettings, validateMeteor, validateApp, getDocker, validateEnv, 11 | } from './validation'; 12 | import compileBundle from './bundle'; 13 | import AppEngineInstance from './google'; 14 | import { initRepo } from './helpers'; 15 | 16 | // Notify user of available updates 17 | updateNotifier({ pkg }).notify(); 18 | 19 | // Configure CLI 20 | program 21 | .description(pkg.description) 22 | .version(`v${pkg.version}`, '-v, --version') 23 | .option('-i, --init', 'init necessary files on your repo') 24 | .option('-b, --build-only', 'build bundle only') 25 | .option('-s, --settings ', 'path to settings file (settings.json)') 26 | .option('-c, --app ', 'path to app.yaml config file') 27 | .option('-d, --docker ', 'path to Dockerfile file') 28 | .option('-p, --project ', 'path of the directory of your Meteor project') 29 | .option('-v, --verbose', 'enable verbose mode') 30 | .option('-q, --quiet', 'enable quite mode') 31 | .option('-ci, --ci', 'add --allow-superuser flag in meteor commands for running in CI') 32 | .option('-o, --output-dir ', 'build files output directory') 33 | .option('-k, --keep-output-dir', 'do not remove the output directory before start') 34 | .option('--node-version ', 'set custom node version') 35 | .option('--npm-version ${this.workingDir}/bundle/app.yaml`); 50 | shell.sed('-i', '{{ METEOR_SETTINGS }}', `'${compactSettings}'`, `${this.workingDir}/bundle/app.yaml`); 51 | const resultAppYaml = yaml.safeLoad(fs.readFileSync(`${this.workingDir}/bundle/app.yaml`)); 52 | resultAppYaml.env_variables.MONGO_URL = '*****'; 53 | resultAppYaml.env_variables.MONGO_OPLOG_URL = '*****'; 54 | winston.debug(`the following app.yaml will be used:\n${JSON.stringify(resultAppYaml)}`); 55 | const nodeVersion = this.nodeVersion || shell.exec( 56 | `meteor node -v ${this.ci ? '--allow-superuser' : ''}`, 57 | { silent: true }, 58 | ).stdout.trim(); 59 | const npmVersion = this.npmVersion || shell.exec( 60 | `meteor npm -v ${this.ci ? '--allow-superuser' : ''}`, 61 | { silent: true }, 62 | ).stdout.trim(); 63 | winston.debug(`set Node to ${nodeVersion}`); 64 | winston.debug(`set NPM to ${npmVersion}`); 65 | 66 | // Create Dockerfile 67 | const docker = this.dockerFile 68 | .replace('{{ nodeVersion }}', nodeVersion) 69 | .replace('{{ npmVersion }}', npmVersion); 70 | 71 | shell.exec(`echo '${docker}' >${this.workingDir}/bundle/Dockerfile`); 72 | winston.debug(`the following Dockerfile will be used:\n${JSON.stringify(yaml.safeLoad( 73 | fs.readFileSync(`${this.workingDir}/bundle/Dockerfile`), 74 | ))}`); 75 | } 76 | 77 | async deployBundle() { 78 | winston.debug('deploy to App Engine'); 79 | 80 | // Allow users to pass any option to gcloud app deploy 81 | const settings = this.googleCloudSettings; 82 | const flags = Object.keys(settings).map((key) => { 83 | if (key !== 'env_variables') { 84 | const value = settings[key]; 85 | 86 | // Only some flags actually require a value (e.g. stop-previous-version) 87 | if (value) { 88 | return `--${key}=${settings[key]}`; 89 | } 90 | 91 | return `--${key}`; 92 | } 93 | }).join(' '); 94 | 95 | winston.debug(`set flags for deploy: ${flags}`); 96 | shell.exec(`cd ${this.workingDir}/bundle && gcloud app deploy -q ${flags}`); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/lib/helpers.js: -------------------------------------------------------------------------------- 1 | // Helper functions 2 | 3 | import fs from 'fs'; 4 | import shell from 'shelljs'; 5 | import winston from 'winston'; 6 | 7 | export function initRepo() { 8 | const dirName = __dirname; 9 | 10 | const path = dirName.replace('/dist/lib', '/examples/'); 11 | const dockerFile = 'Dockerfile'; 12 | const app = 'app.yml'; 13 | const settings = 'settings.json'; 14 | 15 | shell.exec('mkdir -p deploy'); 16 | const newFolderPath = `${process.cwd()}/deploy`; 17 | 18 | // Init app.yaml 19 | if (!fs.existsSync('./deploy/app.yml')) { 20 | shell.cp('-R', `${path}${app}`, newFolderPath); 21 | 22 | winston.info('app.yml created on deploy/ it has the default/minimum settings you need'); 23 | } else { 24 | winston.error('app.yml already exists on deploy/'); 25 | } 26 | 27 | // Init Dockerfile 28 | if (!fs.existsSync('./deploy/Dockerfile')) { 29 | shell.cp('-R', `${path}${dockerFile}`, newFolderPath); 30 | 31 | winston.info('Dockerfile created on deploy/'); 32 | } else { 33 | winston.error('Dockerfile already exists on deploy/'); 34 | } 35 | 36 | // Init settings 37 | if (!fs.existsSync('./deploy/settings.json')) { 38 | shell.cp('-R', `${path}${settings}`, newFolderPath); 39 | 40 | winston.info('Meteor settings file created on deploy/'); 41 | } else { 42 | winston.error('settings.json already exists on deploy/'); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/lib/validation.js: -------------------------------------------------------------------------------- 1 | // Validation methods 2 | 3 | import fs from 'fs'; 4 | import jsonfile from 'jsonfile'; 5 | import yaml from 'js-yaml'; 6 | import winston from 'winston'; 7 | import nth from 'lodash.nth'; 8 | import dropRight from 'lodash.dropright'; 9 | import Joi from '@hapi/joi'; 10 | import commandExists from 'command-exists'; 11 | 12 | export function validateGCloud() { 13 | // Ensure gcloud CLI is installed 14 | winston.debug('check gcloud is installed'); 15 | if (commandExists.sync('gcloud') === false) { 16 | throw new Error('gcloud is not installed'); 17 | } 18 | } 19 | 20 | export function validateMeteor() { 21 | let release; 22 | 23 | // Ensure Meteor CLI is installed 24 | winston.debug('check Meteor is installed'); 25 | if (commandExists.sync('meteor') === false) { 26 | throw new Error('Meteor is not installed'); 27 | } 28 | 29 | // Determine current release/packages from '.meteor' directory 30 | try { 31 | release = fs.readFileSync('.meteor/release', 'utf8'); 32 | } catch (error) { 33 | /* Abort the program if files are not found, this is a strong 34 | indication we may not be in the root project directory */ 35 | throw new Error('You must be in a Meteor project directory'); 36 | } 37 | 38 | // Determine major/minor version numbers by stripping non-numeric characters from release 39 | const versionNumbers = release.replace(/[^0-9.]/g, '').split('.'); 40 | const majorVersion = Number.parseInt(versionNumbers[0], 10); 41 | const minorVersion = Number.parseInt(versionNumbers[1], 10); 42 | 43 | // Ensure current Meteor release is >= 1.4 44 | winston.debug('check current Meteor release >= 1.4'); 45 | if (majorVersion < 1 || (majorVersion === 1 && minorVersion < 4)) { 46 | throw new Error('Meteor version must be >= 1.4'); 47 | } 48 | } 49 | 50 | export function validateSettings(filePath) { 51 | let settingsFile; 52 | 53 | winston.info(`Validating settings file (${filePath})`); 54 | 55 | // Ensure valid json exists 56 | winston.debug('check valid json exists'); 57 | try { 58 | settingsFile = jsonfile.readFileSync(filePath); 59 | } catch (error) { 60 | throw new Error(`Could not read settings file at '${filePath}'`); 61 | } 62 | 63 | // Define schema 64 | const meteorGoogleCloudConfig = Joi.object({ 65 | project: Joi.string(), 66 | }).unknown(true); 67 | const schema = Joi.object({ 68 | 'meteor-google-cloud': meteorGoogleCloudConfig, 69 | }).unknown(true); 70 | 71 | // Ensure settings data follows schema 72 | winston.debug('check data follows schema'); 73 | Joi.validate(settingsFile, schema, { presence: 'required' }, (error) => { 74 | if (error) { 75 | // Pull error from bottom of stack to get most specific/useful details 76 | const lastError = nth(error.details, -1); 77 | 78 | // Locate parent of non-compliant field, or otherwise mark as top level 79 | let pathToParent = 'top level'; 80 | if (lastError.path.length > 1) { 81 | pathToParent = `"${dropRight(lastError.path).join('.')}"`; 82 | } 83 | 84 | // Report user-friendly error with relevant complaint/context to errors 85 | throw new Error(`Settings file (${filePath}): ${lastError.message} in ${pathToParent}`); 86 | } 87 | }); 88 | 89 | return settingsFile; 90 | } 91 | 92 | export function validateApp(filePath) { 93 | let appFile; 94 | 95 | winston.info(`Validating app.yml file (${filePath})`); 96 | 97 | // Ensure valid json exists 98 | winston.debug('check app yaml exists'); 99 | try { 100 | appFile = yaml.safeLoad(fs.readFileSync(filePath)); 101 | } catch (error) { 102 | throw new Error(`Could not read app.yml file at '${filePath}'`); 103 | } 104 | 105 | // Define schema 106 | const schema = Joi.object({ 107 | service: Joi.string(), 108 | runtime: Joi.string(), 109 | env: Joi.string(), 110 | threadsafe: Joi.boolean(), 111 | automatic_scaling: Joi.object({ 112 | max_num_instances: Joi.number().min(1), 113 | }).optional().unknown(true), 114 | resources: Joi.object({ 115 | cpu: Joi.number().min(1), 116 | memory_gb: Joi.number(), 117 | disk_size_gb: Joi.number().min(10), 118 | }).optional().unknown(true), 119 | network: Joi.object({ 120 | session_affinity: Joi.boolean(), 121 | }), 122 | }).unknown(true); 123 | // allow unknown keys (at the top level) for extra settings 124 | // (https://cloud.google.com/appengine/docs/admin-api/reference/rest/v1/apps.services.versions) 125 | 126 | // Ensure settings app yaml follows schema 127 | winston.debug('check app yaml follows schema'); 128 | Joi.validate(appFile, schema, { presence: 'required' }, (error) => { 129 | if (error) { 130 | // Pull error from bottom of stack to get most specific/useful details 131 | const lastError = nth(error.details, -1); 132 | 133 | // Locate parent of non-compliant field, or otherwise mark as top level 134 | let pathToParent = 'top level'; 135 | if (lastError.path.length > 1) { 136 | pathToParent = `"${dropRight(lastError.path).join('.')}"`; 137 | } 138 | 139 | // Report user-friendly error with relevant complaint/context to errors 140 | throw new Error(`App.yaml file (${filePath}): ${lastError.message} in ${pathToParent}`); 141 | } 142 | }); 143 | 144 | // Make sure threadsafe is always true otherwise Meteor will not work properly 145 | if (!appFile.threadsafe) { 146 | winston.debug('found threadsafe false, change threadsafe to true'); 147 | 148 | Object.assign(appFile, { 149 | threadsafe: true, 150 | }); 151 | } 152 | 153 | return appFile; 154 | } 155 | 156 | export function getDocker(filePath) { 157 | let dockerFile; 158 | 159 | winston.info(`Reading Dockerfile (${filePath})`); 160 | 161 | // Ensure file exists 162 | winston.debug('check dockerfile exists'); 163 | try { 164 | dockerFile = fs.readFileSync(filePath, 'utf8'); 165 | } catch (error) { 166 | throw new Error(`Could not read Dockerfile at '${filePath}'`); 167 | } 168 | 169 | return dockerFile; 170 | } 171 | 172 | export function validateEnv(settings, app) { 173 | winston.debug('check either settings.json or app.yaml contain the required env'); 174 | const appSchema = Joi.object({ 175 | env_variables: Joi.object({ 176 | ROOT_URL: Joi.string(), 177 | MONGO_URL: Joi.string(), 178 | }).unknown(true), 179 | }).unknown(true); 180 | const settingsValidation = Joi.validate(settings, Joi.object({ 181 | 'meteor-google-cloud': appSchema, 182 | }).unknown(true), { presence: 'required' }); 183 | const appValidation = Joi.validate(app, appSchema, { presence: 'required' }); 184 | if (settingsValidation.error === null) { 185 | return settings['meteor-google-cloud'].env_variables; 186 | } 187 | if (appValidation.error === null) { 188 | return app.env_variables; 189 | } 190 | throw new Error('neither app.yaml, nor settings.json did contain the env_variables'); 191 | } 192 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | // Entry point 2 | 3 | import startup from './lib/cli'; 4 | 5 | startup(); 6 | --------------------------------------------------------------------------------