sedouard/azure-arm-validator

View on GitHub
routes/validate.js

Summary

Maintainability
D
1 day
Test Coverage
var express = require('express'),
  router = express.Router(),
  path = require('path'),
  azureTools = require('../modules/azure'),
  paramHelper = require('../modules/param_helper'),
  Guid = require('guid'),
  fs = require('fs'),
  conf = require('../modules/config'),
  RSVP = require('rsvp'),
  githubHelper = require('../modules/github_helper'),
  DelayedResponse = require('http-delayed-response'),
  tempDir = 'temp';

var debug = require('debug')('arm-validator:server');
var parallelDeployLimit = parseInt(conf.get('PARALLEL_DEPLOY_LIMIT') || 20);
debug('parallelDeployLimit: ' + parallelDeployLimit);
var parallelDeploys = 0;

function writeFileHelper(fs, fileName, parametersFileName, template, parameters) {
  var writeFile = RSVP.denodeify(fs.writeFile);
  return writeFile.call(fs, fileName, JSON.stringify(template, null, '\t'))
    .then(function () {
      return writeFile.call(fs, parametersFileName, JSON.stringify(parameters, null, '\t'));
    });
}

// replaces https://raw.githubusercontent.com links to upstream:master to the downstream repo
function replaceRawLinksForPR(template, prNumber) {
  var templateString = JSON.stringify(template);
  // we make the assumption all links target a source on master
  var replaceTarget = 'https://' + path.join('raw.githubusercontent.com/', conf.get('GITHUB_REPO'), '/master');
  debug('replaceTarget: ' + replaceTarget);
  return githubHelper.getPullRequestBaseLink(prNumber)
    .then(link => {
      // replace something like 'https://raw.githubusercontent.com/azure/azure-quickstart-templates/master'
      // with 'https://raw.githubusercontent.com/user/azure-quickstart-templates/sourcebranch'
      return JSON.parse(templateString.replace(new RegExp(replaceTarget, 'g'), link));
    });
}

// replaces
function replaceSpecialParameterPlaceholders(req) {
  req.body.parameters = paramHelper.replaceKeyParameters(req.body.parameters);
  if (req.body.preReqParameters) {
    req.body.preReqParameters = paramHelper.replaceKeyParameters(req.body.preReqParameters);
  }
}
router.post('/validate', function (req, res) {

  var fileName = tempDir + '/' + Guid.raw(),
    parametersFileName = tempDir + '/' + Guid.raw(),
    promise = new RSVP.Promise((resolve) => {
      resolve();
    }),
    preReqFileName,
    preReqParametersFileName;

  replaceSpecialParameterPlaceholders(req);

  debug('pull request number: ' + req.body.pull_request);
  if (req.body.pull_request) {
    promise = promise
      .then(() => {
        return replaceRawLinksForPR(req.body.template, req.body.pull_request);
      })
      .then((modifiedTemplate) => {
        debug('modified template is:');
        debug(modifiedTemplate);
        req.body.template = modifiedTemplate;
      });
  }

  if (req.body.preReqTemplate) {
    promise = promise.then(() => {
      preReqFileName = tempDir + '/' + Guid.raw();
      preReqParametersFileName = tempDir + '/' + Guid.raw();
      return writeFileHelper(fs, preReqFileName, preReqParametersFileName, req.body.preReqTemplate, req.body.preReqParameters);
    });
  }
  
  promise.then(() => {
    writeFileHelper(fs, fileName, parametersFileName, req.body.template, req.body.parameters)
      .then(function() {
                if (preReqFileName) {
                    return azureTools.validateTemplateWithPreReq(fileName, parametersFileName, preReqFileName, preReqParametersFileName);
                } else if (req.body.template_link) {
                    return azureTools.validateTemplate(null, parametersFileName, req.body.template_link);
                } else {
                    return azureTools.validateTemplate(fileName, parametersFileName);
                }
            })
      .then(function () {
        return res.send({
          result: 'Template Valid'
        });
      })
      .catch(function (err) {
        return res.status(400).send({
          error: err.toString()
        });
      })
      .finally(function () {
        if (fs.existsSync(fileName)) {
          fs.unlink(fileName);
        }

        if (fs.existsSync(parametersFileName)) {
          fs.unlink(parametersFileName);
        }
      
        if (fs.existsSync(preReqFileName)) {
          fs.unlink(preReqFileName);
        }
      
        if (fs.existsSync(preReqParametersFileName)) {
          fs.unlink(preReqParametersFileName);
        }
      });
  });
});

router.post('/deploy', function (req, res) {

  var fileName = tempDir + '/' + Guid.raw(),
    rgName = conf.get('RESOURCE_GROUP_NAME_PREFIX') + Guid.raw(),
    parametersFileName = tempDir + '/' + Guid.raw(),
    preReqFileName,
    preReqParametersFileName;

  if (parallelDeploys >= parallelDeployLimit) {
    return res.status(403).send({
      error: 'Server has hit its parallel deployment limit. Try again later'
    });
  }

  var delayed = new DelayedResponse(req, res);
  // shortcut for res.setHeader('Content-Type', 'application/json')
  delayed.json();
  replaceSpecialParameterPlaceholders(req);
  delayed.start();
  var promise = new RSVP.Promise((resolve) => {
    resolve();
  });

  debug('pull request number: ' + req.body.pull_request);
  if (req.body.pull_request) {
    promise = promise
      .then(() => {
        return replaceRawLinksForPR(req.body.template, req.body.pull_request);
      })
      .then((modifiedTemplate) => {
        debug('modified template is:');
        debug(modifiedTemplate);
        req.body.template = modifiedTemplate;
      });
  }

  if (req.body.preReqTemplate) {
    promise = promise.then(() => {
      preReqFileName = tempDir + '/' + Guid.raw();
      preReqParametersFileName = tempDir + '/' + Guid.raw();
      return writeFileHelper(fs, preReqFileName, preReqParametersFileName, req.body.preReqTemplate, req.body.preReqParameters);
    });
  }


  promise.then(() => {
    return writeFileHelper(fs, fileName, parametersFileName, req.body.template, req.body.parameters);
  })
  .then(function () {
    debug('deploying template: ');
    debug(JSON.stringify(req.body.template, null, '\t'));
    debug('with paremeters: ');
    debug(JSON.stringify(req.body.parameters, null, '\t'));
    parallelDeploys += 1;
    debug('parallel deploy count: ' + parallelDeploys);
    if (preReqFileName) {
      return azureTools.testTemplateWithPreReq(rgName, fileName, parametersFileName, preReqFileName, preReqParametersFileName);
    } else if (req.body.template_link) {
      return azureTools.testTemplate(rgName, null, parametersFileName, req.body.template_link);
    } else {
      return azureTools.testTemplate(rgName, fileName, parametersFileName);
    }
  })
  .then(function () {
    debug('Deployment Successful');
    // stop sending long poll bytes
    delayed.stop();
    return res.end(JSON.stringify({
      result: 'Deployment Successful'
    }));
  })
  .catch(function (err) {
    debug(err);
    debug('Deployment not Sucessful');
    // stop sending long poll bytes
    delayed.stop();
    return res.end(JSON.stringify({
      error: err.toString(),
      _rgName: rgName,
      command: 'azure group deployment create --resource-group (your_group_name) --template-file azuredeploy.json --parameters-file azuredeploy.parameters.json',
      parameters: req.body.parameters,
      template: req.body.template
    }));
  })
  .finally(function () {
    parallelDeploys -= 1;
    fs.unlink(fileName);
    fs.unlink(parametersFileName);

    if (fs.existsSync(preReqFileName)) {
      fs.unlink(preReqFileName);
    }

    if (fs.existsSync(preReqParametersFileName)) {
      fs.unlink(preReqParametersFileName);
    }

    azureTools.deleteGroup(rgName)
      .then(() => {
        debug('Sucessfully cleaned up resource group: ' + rgName);
      })
      .catch((err) => {
        console.error('failed to delete resource group: ' + rgName);
        console.error(err);
      });
  });
});

module.exports = router;