diff --git a/lib/deploy/storage/deploy.js b/lib/deploy/storage/deploy.js new file mode 100644 index 00000000000..4e4a3c4edda --- /dev/null +++ b/lib/deploy/storage/deploy.js @@ -0,0 +1,25 @@ +'use strict'; + +var _ = require('lodash'); +var RSVP = require('rsvp'); + +var gcp = require('../../gcp'); +var utils = require('../../utils'); + +module.exports = function(context, options) { + var files = _.get(context, 'storage.rules'); + if (!files) { + return RSVP.resolve(); + } + + return gcp.storage.buckets.getDefault(options.project).then(function(defaultBucket) { + if (!defaultBucket) { + return utils.reject('Unable to locate your default bucket. Please visit the storage panel in the Firebase console to ensure provisioning.'); + } + + context.storage.defaultBucket = defaultBucket; + return gcp.rules.createRuleset(options.project, files); + }).then(function(rulesetName) { + context.storage.rulesetName = rulesetName; + }); +}; diff --git a/lib/deploy/storage/index.js b/lib/deploy/storage/index.js index 0faeede9a37..a832046c82b 100644 --- a/lib/deploy/storage/index.js +++ b/lib/deploy/storage/index.js @@ -1,5 +1,7 @@ 'use strict'; module.exports = { - prepare: require('./prepare') + prepare: require('./prepare'), + deploy: require('./deploy'), + release: require('./release') }; diff --git a/lib/deploy/storage/prepare.js b/lib/deploy/storage/prepare.js index 01987b52371..91ee2fbbe05 100644 --- a/lib/deploy/storage/prepare.js +++ b/lib/deploy/storage/prepare.js @@ -1,29 +1,21 @@ 'use strict'; +var chalk = require('chalk'); var fs = require('fs'); var RSVP = require('rsvp'); -var api = require('../../api'); + +var gcp = require('../../gcp'); var utils = require('../../utils'); -var chalk = require('chalk'); -module.exports = function(context, options, payload) { +module.exports = function(context, options) { var rulesPath = options.config.get('storage.rules'); if (rulesPath) { rulesPath = options.config.path(rulesPath); var src = fs.readFileSync(rulesPath, 'utf8'); + var files = [{name: options.config.get('storage.rules'), content: src}]; + utils.logBullet(chalk.bold.cyan('storage:') + ' checking rules for compilation errors...'); - return api.request('POST', '/v1/projects/' + encodeURIComponent(options.project) + ':test', { - origin: api.rulesOrigin, - data: { - source: { - files: [{ - content: src, - name: 'storage.rules' - }] - } - }, - auth: true - }).then(function(response) { + return gcp.rules.testRuleset(options.project, files).then(function(response) { if (response.body && response.body.issues && response.body.issues.length > 0) { var add = response.body.issues.length === 1 ? '' : 's'; var message = 'Compilation error' + add + ' in ' + chalk.bold(options.config.get('storage.rules')) + ':\n'; @@ -35,9 +27,7 @@ module.exports = function(context, options, payload) { } utils.logSuccess(chalk.bold.green('storage:') + ' rules file compiled successfully'); - payload.storage = {rules: [ - {name: options.config.get('storage.rules'), content: src} - ]}; + context.storage = {rules: files}; return RSVP.resolve(); }); } diff --git a/lib/deploy/storage/release.js b/lib/deploy/storage/release.js new file mode 100644 index 00000000000..551c4ccc473 --- /dev/null +++ b/lib/deploy/storage/release.js @@ -0,0 +1,18 @@ +'use strict'; + +var chalk = require('chalk'); + +var gcp = require('../../gcp'); +var utils = require('../../utils'); + +var STORAGE_RELEASE_NAME = 'firebase.storage'; + +module.exports = function(context, options) { + return gcp.rules.updateOrCreateRelease( + options.project, + context.storage.rulesetName, + [STORAGE_RELEASE_NAME, context.storage.defaultBucket].join('/') + ).then(function() { + utils.logSuccess(chalk.bold.green('storage: ') + 'released rules for bucket ' + context.storage.defaultBucket); + }); +}; diff --git a/lib/gcp/index.js b/lib/gcp/index.js index cd1c7fe986c..2af74e26e7b 100644 --- a/lib/gcp/index.js +++ b/lib/gcp/index.js @@ -1,8 +1,9 @@ 'use strict'; module.exports = { - cloudlogging: require('./cloudlogging'), + apikeys: require('./apikeys'), cloudfunctions: require('./cloudfunctions'), + cloudlogging: require('./cloudlogging'), storage: require('./storage'), - apikeys: require('./apikeys') + rules: require('./rules') }; diff --git a/lib/gcp/rules.js b/lib/gcp/rules.js new file mode 100644 index 00000000000..670747d3f4a --- /dev/null +++ b/lib/gcp/rules.js @@ -0,0 +1,114 @@ +'use strict'; + +var api = require('../api'); +var logger = require('../logger'); +var utils = require('../utils'); + +function _handleErrorResponse(response) { + if (response.body && response.body.error) { + return utils.reject(response.body.error, {code: 2}); + } + + logger.debug('[rules] error:', response.status, response.body); + return utils.reject('Unexpected error encountered deploying rules.', {code: 2}); +} + +/** + * Creates a new ruleset which can then be associated with a release. + * @param {String} projectId Project on which you want to create the ruleset. + * @param {Array} files Array of `{name, content}` for the source files. + */ +function createRuleset(projectId, files) { + var payload = {source: {files: files}}; + + return api.request('POST', '/v1/projects/' + projectId + '/rulesets', { + auth: true, + data: payload, + origin: api.rulesOrigin + }).then(function(response) { + if (response.status === 200) { + logger.debug('[rules] created ruleset', response.body.name); + return response.body.name; + } + + return _handleErrorResponse(response); + }); +} + +/** + * Create a new named release with the specified ruleset. + * @param {String} projectId Project on which you want to create the ruleset. + * @param {String} rulesetName The unique identifier for the ruleset you want to release. + * @param {String} releaseName The name (e.g. `firebase.storage`) of the release you want to create. + */ +function createRelease(projectId, rulesetName, releaseName) { + var payload = { + name: 'projects/' + projectId + '/releases/' + releaseName, + rulesetName: rulesetName + }; + + return api.request('POST', '/v1/projects/' + projectId + '/releases', { + auth: true, + data: payload, + origin: api.rulesOrigin + }).then(function(response) { + if (response.status === 200) { + logger.debug('[rules] created release', response.body.name); + return response.body.name; + } + + return _handleErrorResponse(response); + }); +} + +/** + * Update an existing release with the specified ruleset. + * @param {String} projectId Project on which you want to create the ruleset. + * @param {String} rulesetName The unique identifier for the ruleset you want to release. + * @param {String} releaseName The name (e.g. `firebase.storage`) of the release you want to update. + */ +function updateRelease(projectId, rulesetName, releaseName) { + var payload = { + name: 'projects/' + projectId + '/releases/' + releaseName, + rulesetName: rulesetName + }; + + return api.request('PUT', '/v1/projects/' + projectId + '/releases/' + releaseName, { + auth: true, + data: payload, + origin: api.rulesOrigin + }).then(function(response) { + if (response.status === 200) { + logger.debug('[rules] updated release', response.body.name); + return response.body.name; + } + + return _handleErrorResponse(response); + }); +} + +function updateOrCreateRelease(projectId, rulesetName, releaseName) { + logger.debug('[rules] releasing', releaseName, 'with ruleset', rulesetName); + return updateRelease(projectId, rulesetName, releaseName).catch(function() { + logger.debug('[rules] ruleset update failed, attempting to create instead'); + return createRelease(projectId, rulesetName, releaseName); + }); +} + +function testRuleset(projectId, files) { + return api.request('POST', '/v1/projects/' + encodeURIComponent(projectId) + ':test', { + origin: api.rulesOrigin, + data: { + source: {files: files} + }, + auth: true + }); +} + +module.exports = { + createRuleset: createRuleset, + createRelease: createRelease, + updateRelease: updateRelease, + updateOrCreateRelease: updateOrCreateRelease, + testRuleset: testRuleset +};