tools/scheduled-deployments/functions/schedule.js (183 lines of code) (raw):

/** * @fileoverview Helper functions to implement the Scheduling stage of * Scheduled Deployments. */ 'use strict'; const base64 = require('js-base64'); const constants = require('./constants'); const datastore = require('./datastore'); const logging = require('./logging'); /** * Extract details of a Scheduled Deployment from a JSON payload and * record details as a new Datastore entity. * * @param {string} name - Name of the Scheduled Deployment to create. * @param {!Object} deploymentDetails - Deployment details from HTTP request. * @param {!Object} res - HTTP response context. */ exports.create = (name, deploymentDetails, res) => { // validate format of JSON payload const requiredFields = ['user', 'description', 'triggers']; const missingFields = datastore.findMissingFields(requiredFields, deploymentDetails); if (missingFields.length > 0) { res.status(400).send( 'Missing required fields in Scheduled Deployment config: ' + missingFields.join()); return; } if (!Array.isArray(deploymentDetails.triggers)) { res.status(400).send( 'Scheduled Deployment triggers must be input as an array.'); return; } if (deploymentDetails.triggers.length === 0) { res.status(400).send( 'A Scheduled Deployment must have at least one trigger.'); return; } const parentKey = datastore.createEntityKey(constants.KIND); const parentData = [ {name: 'name', value: name}, {name: 'description', value: deploymentDetails.description}, {name: 'user', value: deploymentDetails.user}, {name: 'submitted', value: new Date()} ]; // ensure no entity exists in Datastore with the same name exports.enforceUniqueDeploymentName(name) .then(() => { return datastore.insertEntity(parentKey, parentData); }) .then(() => { return exports.putTriggerEntities( name, parentKey, deploymentDetails.triggers); }) .then(() => { logging.logMessage(`Recorded the Scheduled Deployment '${name}'.`); res.status(200).end(`Recorded the Scheduled Deployment '${name}'.`); }) .catch((err) => { logging.logError( `Unable to record Scheduled Deployment '${name}'.`, err); res.status(400).end(`Unable to record Scheduled Deployment '${name}'.`); }); }; /** * Return details of a Scheduled Deployment in the HTTP response. * * @param {string} name - Name of the Scheduled Deployment. * @param {!Object} entity - Scheduled Deployment entity from Datastore. * @param {!Object} res - HTTP response context. */ exports.read = (name, entity, res) => { logging.logWithJson(`The Scheduled Deployment ${name} is `, entity); res.json(entity); res.status(200).json(); }; /** * Update a Scheduled Deployment by inserting the new deployment details. * * @param {string} name - Name of the Scheduled Deployment. * @param {!Object} entity - Scheduled Deployment entity from Datastore. * @param {!Object} deploymentDetails - Deployment details from HTTP request. * @param {!Object} res - HTTP response context. */ exports.update = (name, entity, deploymentDetails, res) => { // delete existing Trigger entities const parentKey = datastore.getEntityKey(entity); datastore.deleteChildrenOfEntity(parentKey.path, constants.TRIGGER_KIND) .then(() => { // update Scheduled Deployment entity const data = [ {name: 'name', value: name}, {name: 'description', value: deploymentDetails.description}, {name: 'user', value: deploymentDetails.user}, {name: 'submitted', value: new Date()} ]; return datastore.updateEntity(parentKey, data); }) .then(() => { // insert new Trigger entities exports.putTriggerEntities(name, parentKey, deploymentDetails.triggers); logging.logMessage(`Updated the Scheduled Deployment '${name}'.`); res.status(200).end(); }) .catch((err) => { logging.logError( `Unable to update Scheduled Deployment '${name}'. `, err); res.status(500).end(); }); }; /** * Delete a Scheduled Deployment by removing its details from Datastore. * * @param {string} name - Name of the Scheduled Deployment. * @param {!Object} entity - Scheduled Deployment entity from Datastore. * @param {!Object} res - HTTP response context. */ exports.delete = (name, entity, res) => { const key = datastore.getEntityKey(entity); // delete corresponding triggers and operations datastore.deleteChildrenOfEntity(key.path, constants.TRIGGER_KIND); datastore.deleteChildrenOfEntity(key.path, constants.OPERATION_KIND); datastore.deleteEntity(key) .then((data) => { logging.logMessage(`Deleted the Scheduled Deployment '${name}'.`); const apiResponse = data[0]; res.json(apiResponse); res.status(200).end(); }) .catch((err) => { logging.logError( `Unable to delete Scheduled Deployment '${name}'.`, err); res.status(500).end(); return; }); }; /** * Ensure the name of a new Scheduled Deployment does not conflict with * any existing entities in Datastore. * * @param {string} name - Proposed Scheduled Deployment name. * @returns {Promise<null>} */ exports.enforceUniqueDeploymentName = (name) => { return new Promise(function(resolve, reject) { datastore.getEntitiesWithFilter(constants.KIND, 'name', name) .then((results) => { if (results[0].length > 0) { reject(`A Scheduled Deployment already exists with the name '${ name }'.`); } resolve(); }) .catch((err) => { reject(err); }); }); }; /** * Extract details of a Scheduled Deployment trigger from a JSON payload * and record details as a new Datastore entity. * * @param {string} parentName - Name of Scheduled Deployment parent. * @param {{kind: string, id: number}} parentKey - Identifiers of the parent * entity in Datastore. * @param {!Array<!Object>} triggers - Trigger entities from Datastore. * @returns {Promise<null>} */ exports.putTriggerEntities = (parentName, parentKey, triggers) => { return new Promise(function(resolve, reject) { for (let trigger of triggers) { exports.isTriggerFormatValid(trigger, parentName) .then(() => { const triggerPath = [parentKey.kind, parentKey.id, constants.TRIGGER_KIND]; const triggerKey = datastore.createEntityKey(triggerPath); const triggerData = [ {name: 'name', value: trigger.name}, {name: 'type', value: trigger.type}, {name: 'time', value: trigger.time}, {name: 'action', value: trigger.action}, {name: 'description', value: trigger.description}, {name: 'submitted', value: (new Date()).toISOString()} ]; if (trigger['action'] === 'CREATE_OR_UPDATE') { // these fields are not necessary for DELETE triggerData.push({name: 'config', value: trigger.config}); if ('importName' in trigger && 'importContent' in trigger) { triggerData.push( {name: 'importName', value: trigger.importName}); triggerData.push( {name: 'importContent', value: trigger.importContent}); } else if ( 'importName' in trigger || 'importContent' in trigger) { reject(new Error( `Both 'importName' and 'importContent' are required to ` + 'import a template in a configuration.')); } } datastore.insertEntity(triggerKey, triggerData).catch((err) => { reject(err); }); }) .catch((err) => { reject(err); }); } resolve(); }); }; /** * Ensure a Trigger entity in Datastore has the correct format. * * @param {!Object<string>} trigger - Entity from Datastore. * @param {string} parentName - Name of Scheduled Deployment parent. * @returns {Promise<boolean>} Whether entity has the correct format. */ exports.isTriggerFormatValid = (trigger, parentName) => { return new Promise((resolve, reject) => { const requiredFields = ['action', 'name', 'type', 'time']; const missingFields = datastore.findMissingFields(requiredFields, trigger); if (missingFields.length > 0) { if ('name' in missingFields) { reject(new Error( 'Missing required fields in Trigger entity of Scheduled ' + `Deployment ${parentName}': ` + missingFields.join())); } else { reject(new Error( `Missing required fields in Trigger entity '${trigger.name}' of` + `Scheduled Deployment '${parentName}': ` + missingFields.join())); } } if (trigger.action != 'DELETE' && trigger.action != 'CREATE_OR_UPDATE') { reject(new Error( 'Trigger action must be either CREATE_OR_UPDATE or DELETE.')); } resolve(true); }); }; /** * Extract Scheduled Deployment name from provided URL path or query. The name * is provided as a query (?name=...) in the POST method and as a path (/...) * otherwise. * * @param {string} url - URL fragment of request as query or path. * @param {string} method - HTTP request method. * @returns {string} Name from URL fragment. */ exports.parseNameFromUrl = (url, method) => { if (method == 'POST') { const parsed = url.split('='); return parsed.slice(-1)[0]; } else { return url.substr(1); } };