diff --git a/Gruntfile.js b/Gruntfile.js index c81bffa5c..d98b8a089 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -66,7 +66,8 @@ module.exports = function (grunt) { mochaTest: { test: { src: [ - 'test/*.js' + 'test/*.js', + '!test/cf-test-creation.js' ], options: { reporter: 'spec', diff --git a/app/templates/Gruntfile.js b/app/templates/Gruntfile.js index 6755db958..0a7a18e2f 100644 --- a/app/templates/Gruntfile.js +++ b/app/templates/Gruntfile.js @@ -216,7 +216,9 @@ module.exports = function (grunt) { '<%%= yeoman.dist %>/*', '!<%%= yeoman.dist %>/.git*', '!<%%= yeoman.dist %>/.openshift', - '!<%%= yeoman.dist %>/Procfile' + '!<%%= yeoman.dist %>/.cfignore', + '!<%%= yeoman.dist %>/Procfile', + '!<%%= yeoman.dist %>/manifest.yml' ] }] }, diff --git a/app/templates/server/config/environment/index.js b/app/templates/server/config/environment/index.js index 2476a7e8c..384197dba 100644 --- a/app/templates/server/config/environment/index.js +++ b/app/templates/server/config/environment/index.js @@ -19,7 +19,7 @@ var all = { root: path.normalize(__dirname + '/../../..'), // Server port - port: process.env.PORT || 9000, + port: process.env.PORT || process.env.VCAP_APP_PORT || 9000, // Should we populate the DB with sample data? seedDB: false, diff --git a/app/templates/server/config/environment/production.js b/app/templates/server/config/environment/production.js index e0b77bf97..80595dc9c 100644 --- a/app/templates/server/config/environment/production.js +++ b/app/templates/server/config/environment/production.js @@ -2,6 +2,17 @@ // Production specific configuration // ================================= + +// cloud foundry +var getCfMongo = function() { + var vcapServices = JSON.parse(process.env.vcapServices); + var mongoUri; + if (vcapServices.mongolab && vcapServices.mongolab.length > 0) { + mongoUri = vcapServices.mongolab[0].credentials.uri; + } + return mongoUri; +}; + module.exports = { // Server IP ip: process.env.OPENSHIFT_NODEJS_IP || @@ -11,6 +22,7 @@ module.exports = { // Server port port: process.env.OPENSHIFT_NODEJS_PORT || process.env.PORT || + process.env.VCAP_APP_PORT || 8080, // MongoDB connection options @@ -19,6 +31,7 @@ module.exports = { process.env.MONGOHQ_URL || process.env.OPENSHIFT_MONGODB_DB_URL + process.env.OPENSHIFT_APP_NAME || + getCfMongo() || 'mongodb://localhost/<%= _.slugify(appname) %>' } }; diff --git a/cloudfoundry/USAGE b/cloudfoundry/USAGE new file mode 100644 index 000000000..be7892e48 --- /dev/null +++ b/cloudfoundry/USAGE @@ -0,0 +1,8 @@ +Description: + Initalizes a cloud foundr app and generates a `dist` folder which is ready to push to cloud foundry. + +Example: + yo angular-fullstack:cloudfoundry + + This will create: + a dist folder and initialize a cloud foundry app diff --git a/cloudfoundry/index.js b/cloudfoundry/index.js new file mode 100644 index 000000000..1047dd9df --- /dev/null +++ b/cloudfoundry/index.js @@ -0,0 +1,209 @@ +'use strict'; +var util = require('util'); +var genUtils = require('../util.js'); +var yeoman = require('yeoman-generator'); +var exec = require('child_process').exec; +var chalk = require('chalk'); +var path = require('path'); + +var Generator = module.exports = function Generator() { + yeoman.generators.Base.apply(this, arguments); + this.sourceRoot(path.join(__dirname, './templates')); + + try { + this.appname = require(path.join(process.cwd(), 'bower.json')).name; + } catch (e) { + this.appname = path.basename(process.cwd()); + } + this.appname = this._.slugify(this.appname); + this.filters = this.config.get('filters') || {}; +}; + +util.inherits(Generator, yeoman.generators.NamedBase); + +Generator.prototype.askForRoute = function askForRoute() { + var done = this.async(); + + var prompts = [ + { + name: 'routeName', + message: 'Name of route to deploy (Leave blank for a random route name):' + } + ]; + + this.prompt(prompts, function(props) { + this.routeName = this._.slugify(props.routeName); + done(); + }.bind(this)); +}; + +Generator.prototype.checkInstallation = function checkInstallation() { + if (this.abort) return; + var done = this.async(); + + exec('cf --version', function(err) { + if (err) { + this.log.error('You don\'t have the Cloud Foundry CLI installed. ' + + 'Grab it from https://github.com/cloudfoundry/cli'); + this.abort = true; + } + done(); + }.bind(this)); +}; + +Generator.prototype.askForApiEndpoint = function askForApiEndpoint() { + if (this.abort) return; + var done = this.async(); + + var prompts = [ + { + name: 'apiEndpoint', + default: 'api.run.pivotal.io', + message: 'What api endpoint will you be using for Cloud Foundry?:' + } + ]; + + this.prompt(prompts, function(props) { + this.apiEndpoint = props.apiEndpoint; + done(); + }.bind(this)); +}; + +Generator.prototype.cfInit = function cfInit() { + if (this.abort) return; + var done = this.async(); + + this.log(chalk.bold('Setting Cloud Foundry api endpoint')); + this.mkdir('dist'); + var child = exec('cf api ' + this.apiEndpoint, { cwd: 'dist' }, function(err, stdout, stderr) { + if (err) { + this.abort = true; + this.log.error(err); + } else { + if (stdout.indexOf('Not logged in.') !== -1) { + this.log.error('You don\'t appear to be logged. Please login and try again.'); + this.abort = true; + } else { + this.log('stdout: ' + stdout); + } + + } + done(); + }.bind(this)); + + child.stdout.on('data', function(data) { + this.log(this._.trim(data.toString(), "\n\r")); + }.bind(this)); +} + +Generator.prototype.copyProcfile = function copyProcfile() { + if (this.abort) return; + var done = this.async(); + this.log(chalk.bold('Creating Procfile and manifest.yml')); + genUtils.processDirectory(this, '.', './dist'); + this.conflicter.resolve(function(err) { + done(); + }); +}; + +Generator.prototype.gruntBuild = function gruntBuild() { + if (this.abort) return; + var done = this.async(); + + this.log(chalk.bold('\nBuilding dist folder, please wait...')); + var child = exec('grunt build', function(err, stdout) { + done(); + }.bind(this)); + child.stdout.on('data', function(data) { + this.log(data.toString()); + }.bind(this)); +}; + +Generator.prototype.cfPush = function cfPush() { + if (this.abort) return; + var done = this.async(); + + this.log(chalk.bold("\nUploading your initial application code.\n This may take " + chalk.cyan('several minutes') + " depending on your connection speed...")); + + var randomRoute = this.routeName === '' ? '--random-route' : ''; + var child = exec(['cf push', this.appname, randomRoute, ' --no-start'].join(' '), { cwd: 'dist' }, function(err, stdout, stderr) { + if (err) { + this.abort = true; + this.log.error(err); + } else { + this.log('stdout: ' + stdout); + } + done(); + }.bind(this)); + child.stdout.on('data', function(data) { + this.log(this._.trim(data.toString(), "\n\r")); + }.bind(this)); +}; + +Generator.prototype.cfSetEnvVars = function cfSetEnvVars() { + if (this.abort) return; + var done = this.async(); + + var child = exec('cf set-env ' + this.appname + ' NODE_ENV production', { cwd: 'dist' }, function(err, stdout, stderr) { + if (err) { + this.abort = true; + this.log.error(err); + } + done(); + + }.bind(this)); + child.stdout.on('data', function(data) { + this.log(this._.trim(data.toString(), "\n\r")); + }.bind(this)); +}; + +Generator.prototype.cfStart = function cfStart() { + if (this.abort) return; + var done = this.async(); + + var child = exec('cf start ' + this.appname, { cwd: 'dist' }, function(err, stdout, stderr) { + if (err) { + this.abort = true; + this.log.error(err); + } else { + var hasWarning = false; + + if (this.filters.mongoose) { + this.log(chalk.yellow('\nBecause you\'re using mongoose, you must add mongoDB to your cloud foundry app.\n\t' + 'from `/dist`: ' + chalk.bold('cf create-service mongolab sandbox my-mongo') + '\n')); + hasWarning = true; + } + + if (this.filters.facebookAuth) { + this.log(chalk.yellow('You will need to set environment variables for facebook auth. From `/dist`:\n\t' + + chalk.bold('cf set-env ' + this.appname + ' FACEBOOK_ID appId\n\t') + + chalk.bold('cf set-env ' + this.appname + ' FACEBOOK_SECRET secret\n'))); + hasWarning = true; + } + if (this.filters.googleAuth) { + this.log(chalk.yellow('You will need to set environment variables for google auth. From `/dist`:\n\t' + + chalk.bold('cf set-env ' + this.appname + ' GOOGLE_ID appId\n\t') + + chalk.bold('cf set-env ' + this.appname + ' GOOGLE_SECRET secret\n'))); + hasWarning = true; + } + if (this.filters.twitterAuth) { + this.log(chalk.yellow('You will need to set environment variables for twitter auth. From `/dist`:\n\t' + + chalk.bold('cf set-env ' + this.appname + ' TWITTER_ID appId\n\t') + + chalk.bold('cf set-env ' + this.appname + ' TWITTER_SECRET secret\n'))); + hasWarning = true; + } + + this.log(chalk.green('\nYour app should now be live.')); + if (hasWarning) { + this.log(chalk.green('\nYou may need to address the issues mentioned above and restart the server for the app to work correctly.')); + } + this.log(chalk.yellow('After app modification run\n\t' + chalk.bold('grunt build') + + '\nThen deploy (from dist directory ) with\n\t' + chalk.bold('cf push'))); + } + done(); + + }.bind(this)); + child.stdout.on('data', function(data) { + this.log(this._.trim(data.toString(), "\n\r")); + }.bind(this)); + +}; \ No newline at end of file diff --git a/cloudfoundry/templates/Procfile b/cloudfoundry/templates/Procfile new file mode 100644 index 000000000..528737e6f --- /dev/null +++ b/cloudfoundry/templates/Procfile @@ -0,0 +1 @@ +web: node server/app.js diff --git a/cloudfoundry/templates/manifest.yml b/cloudfoundry/templates/manifest.yml new file mode 100644 index 000000000..9a9829d89 --- /dev/null +++ b/cloudfoundry/templates/manifest.yml @@ -0,0 +1,5 @@ +--- +applications: +- name: <%= appname %> + buildpack: https://github.com/cloudfoundry/heroku-buildpack-nodejs.git + command: node server/app.js \ No newline at end of file diff --git a/readme.md b/readme.md index 31836c2a2..73b77ab9c 100644 --- a/readme.md +++ b/readme.md @@ -78,6 +78,7 @@ Available generators: * Deployment - [angular-fullstack:openshift](#openshift) - [angular-fullstack:heroku](#heroku) + - [angular-fullstack:cloudfoundry](#cloudfoundry) ### App Sets up a new AngularJS + Express app, generating all the boilerplate you need to get started. @@ -291,6 +292,36 @@ Commit and push the resulting build, located in your dist folder: grunt buildcontrol:heroku +### Cloud Foundry / Pivotal Web Services + +Deploying to Cloud Foundry can be done with these steps. + + yo angular-fullstack:cloudfoundry + +To work with your new Cloud Foundry app using the command line, you will need to run any `cf` commands from the `dist` folder. + + +If you're using mongoDB you will need to create a service (MongoLab) and bind it to your app: + + cf create-service mongolab sandbox my-mongo + cf bind-service my-app my-mongo + +> +> If you're using any oAuth strategies, you must set environment variables for your selected oAuth. For example, if we're using **Facebook** oAuth we would do this : +> +> cf set-env my-app FACEBOOK_ID id +> cf set-env my-app FACEBOOK_SECRET secret +> + +#### Pushing Updates + + grunt + +Deploy the resulting build, from your `dist` folder: + + cf push + + ## Bower Components The following packages are always installed by the [app](#app) generator: diff --git a/test/cf-test-creation.js b/test/cf-test-creation.js new file mode 100644 index 000000000..1e6e8ebe5 --- /dev/null +++ b/test/cf-test-creation.js @@ -0,0 +1,157 @@ +/*global describe, beforeEach, it */ +'use strict'; +var path = require('path'); +var helpers = require('yeoman-generator').test; +var chai = require('chai'); +var expect = chai.expect; +var fs = require('fs-extra'); +var exec = require('child_process').exec; + +describe('angular-fullstack:cloudfoundry', function () { + var gen, defaultOptions = { + script: 'js', + markup: 'html', + stylesheet: 'sass', + router: 'uirouter', + bootstrap: true, + uibootstrap: false, + mongoose: false, + auth: false, + oauth: [], + socketio: false + }; + + + function generatorTest(generatorType, name, mockPrompt, callback) { + gen.run({}, function () { + var afGenerator; + var deps = [path.join('../..', generatorType)]; + afGenerator = helpers.createGenerator('angular-fullstack:' + generatorType, deps, [name]); + + helpers.mockPrompt(afGenerator, mockPrompt); + afGenerator.run([], function () { + callback(); + }); + }); + } + + beforeEach(function (done) { + this.timeout(10000); + this.appname = 'testapp' + Math.floor(Math.random()*100000000).toString(); + var deps = [ + '../../app', + [ + helpers.createDummyGenerator(), + 'ng-component:app' + ] + ]; + + helpers.testDirectory(path.join(__dirname, this.appname), function (err) { + if (err) { + return done(err); + } + + gen = helpers.createGenerator('angular-fullstack:app', deps); + gen.options['skip-install'] = true; + + fs.mkdirSync(__dirname + '/' + this.appname + '/client'); + fs.symlinkSync(__dirname + '/fixtures/node_modules', __dirname + '/' + this.appname + '/node_modules'); + fs.symlinkSync(__dirname +'/fixtures/bower_components', __dirname + '/' + this.appname + '/client/bower_components'); + + helpers.mockPrompt(gen, defaultOptions); + this.timeout(60000); + + done(); + }.bind(this)); + }); + + afterEach(function (done) { + exec('cf delete ' + this.appname + " -r -f", function (error, stdout, stderr) { + gen.log("Deleting cloudfoundry app instance for " + this.appname + "..."); + if (error) { + gen.log(error); + } + done(); + }.bind(this)); + + }); + + afterEach(function (done) { + exec('rm -rf ' + this.appname, { cwd: '..' }, function (error, stdout, stderr) { + gen.log("Deleting local app instance for " + this.appname + "..."); + if (error) { + gen.log(error); + } + done(); + }.bind(this)); + + }); + + it('copies procfile and manifest files with named route', function (done) { + var mockPromptOptions = { + routeName: Math.floor(Math.random()*100000000).toString(), + apiEndpoint: '' + }; + generatorTest('cloudfoundry', 'cf-test', mockPromptOptions, function () { + helpers.assertFile([ + 'dist/Procfile', + 'dist/manifest.yml' + ]); + exec('cf app ' + this.appname, function (error, stdout, stderr) { + if (error) { + console.log(error); + console.log(stderr); + } + + gen.log(stdout); + expect(stdout, 'App failed to start: \n' + stdout).to.contain('running'); + done(); + }); + }.bind(this)); + }); + + it('copies procfile and manifest files with blank route name', function (done) { + var mockPromptOptions = { + routeName: '', + apiEndpoint: '' + }; + generatorTest('cloudfoundry', 'cf-test', mockPromptOptions, function () { + helpers.assertFile([ + 'dist/Procfile', + 'dist/manifest.yml' + ]); + exec('cf app ' + this.appname, function (error, stdout, stderr) { + if (error) { + console.log(error); + console.log(stderr); + } + gen.log(stdout); + expect(stdout, 'App failed to start: \n' + stdout).to.contain('running'); + done(); + }); + }.bind(this)); + }); + + it('copies procfile and manifest files with specific apiendpoint', function (done) { + var mockPromptOptions = { + routeName: '', + apiEndpoint: 'api.run.pivotal.io' + }; + generatorTest('cloudfoundry', 'cf-test', mockPromptOptions, function () { + helpers.assertFile([ + 'dist/Procfile', + 'dist/manifest.yml' + ]); + exec('cf app ' + this.appname, function (error, stdout, stderr) { + if (error) { + console.log(error); + console.log(stderr); + } + gen.log(stdout); + expect(stdout, 'App failed to start: \n' + stdout).to.contain('running'); + done(); + }); + }.bind(this)); + }); + +});