├── validate-test.js ├── temp └── temp.MD ├── views ├── index.jade ├── error.jade └── layout.jade ├── archive ├── dokku-deploy.enc ├── dokku_git_push.exp └── deploy.sh ├── .gitignore ├── public └── stylesheets │ └── style.css ├── modules ├── config.js ├── mongo_helper.js ├── github_helper.js ├── param_helper.js └── azure.js ├── .beautifyrc ├── test ├── assets │ ├── dokku-vm │ │ ├── metadata.json │ │ ├── azuredeploy.parameters.json │ │ ├── azuredeploy.parameters.gen_unique_var.json │ │ ├── README.md │ │ └── azuredeploy.json │ └── githubresponse.json ├── helpers │ └── setup_env.js ├── modules │ ├── github_helper.js │ ├── azure.js │ └── param_helper.js ├── mocks │ ├── mongodb.js │ ├── unirest.js │ └── azure.js └── routes │ └── validate.js ├── azure-arm-validator.service ├── .travis.yml ├── deployment-template ├── azuredeploy.parameters.a.json ├── azuredeploy.parameters.b.json ├── configure-bot.sh └── azuredeploy.json ├── service-start.sh ├── .jshintrc ├── Gruntfile.js ├── nginx.conf.template ├── LICENSE ├── package.json ├── CONTRIBUTING.md ├── bin └── www ├── app.js ├── .example-config.json ├── .jscsrc ├── .example-config.new.json ├── az-group-deploy.sh ├── routes └── validate.js ├── README.md └── Deploy-AzureResourceGroup.ps1 /validate-test.js: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /temp/temp.MD: -------------------------------------------------------------------------------- 1 | Directory used for storing temp files 2 | -------------------------------------------------------------------------------- /views/index.jade: -------------------------------------------------------------------------------- 1 | extends layout 2 | 3 | block content 4 | h1= title 5 | p Welcome to #{title} 6 | -------------------------------------------------------------------------------- /archive/dokku-deploy.enc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure/azure-arm-validator/HEAD/archive/dokku-deploy.enc -------------------------------------------------------------------------------- /views/error.jade: -------------------------------------------------------------------------------- 1 | extends layout 2 | 3 | block content 4 | h1= message 5 | h2= error.status 6 | pre #{error.stack} 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_STORE 2 | node_modules 3 | **/.config.json 4 | **/.config.*.json 5 | *.bak 6 | deployment-template/upload-config.sh 7 | -------------------------------------------------------------------------------- /public/stylesheets/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding: 50px; 3 | font: 14px "Lucida Grande", Helvetica, Arial, sans-serif; 4 | } 5 | 6 | a { 7 | color: #00B7FF; 8 | } 9 | -------------------------------------------------------------------------------- /views/layout.jade: -------------------------------------------------------------------------------- 1 | doctype html 2 | html 3 | head 4 | title= title 5 | link(rel='stylesheet', href='/stylesheets/style.css') 6 | body 7 | block content 8 | -------------------------------------------------------------------------------- /modules/config.js: -------------------------------------------------------------------------------- 1 | var nconf = require('nconf'); 2 | 3 | nconf.argv() 4 | .env() 5 | .file({ 6 | file: '.config.json' 7 | }); 8 | 9 | module.exports = nconf; 10 | -------------------------------------------------------------------------------- /.beautifyrc: -------------------------------------------------------------------------------- 1 | { 2 | "js": { 3 | "indentChar": " ", 4 | "indentLevel": 0, 5 | "indentSize": 2, 6 | "indentWithTabs": false, 7 | "space_after_anon_function": "true" 8 | } 9 | } 10 | 11 | -------------------------------------------------------------------------------- /archive/dokku_git_push.exp: -------------------------------------------------------------------------------- 1 | #!/usr/bin/expect -f 2 | 3 | expect << EOF 4 | set timeout -1 5 | spawn git push dokku master 6 | expect "Are you sure you want to continue connecting (yes/no)?" 7 | send "yes\r" 8 | expect -exact "remote: =====> Application deployed:" 9 | expect eof 10 | EOF 11 | 12 | -------------------------------------------------------------------------------- /test/assets/dokku-vm/metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "itemDisplayName": "Dokku Instance", 3 | "description": "Dokku is a mini-heroku-style PaaS on a single VM.", 4 | "summary": "Dokku is a mini-heroku-style PaaS on a single VM. You can easily git-push any Heroku buildpack or Dockerfile app and it'll be hosted on this VM.", 5 | "githubUsername": "sedouard", 6 | "dateUpdated": "2015-10-26" 7 | } 8 | -------------------------------------------------------------------------------- /azure-arm-validator.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=azure-arm-validator 3 | After=network.target 4 | 5 | [Service] 6 | ExecStart=/var/www/azure-arm-validator/app.js 7 | Restart=always 8 | User=ubuntu 9 | Environment=PATH=/usr/bin:/usr/local/bin 10 | Environment=NODE_ENV=production 11 | WorkingDirectory=/var/www/azure-arm-validator 12 | 13 | [Install] 14 | WantedBy=multi-user.target 15 | -------------------------------------------------------------------------------- /test/helpers/setup_env.js: -------------------------------------------------------------------------------- 1 | // helper to inject fake conifugration settings to environment 2 | var fs = require('fs'); 3 | var configString = fs.readFileSync('./.example-config.json', { 4 | encoding: 'utf8' 5 | }).trim(); 6 | var configData = JSON.parse(configString); 7 | 8 | for (var key in configData) { 9 | if (typeof key === 'string') { 10 | process.env[key] = configData[key]; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | before_script: 3 | - npm install -g grunt-cli 4 | - npm install -g mocha 5 | node_js: 6 | - 4.1.2 7 | deploy: 8 | skip_cleanup: true 9 | provider: script 10 | script: bash ./deploy.sh 11 | on: 12 | branch: master 13 | before_install: 14 | - openssl aes-256-cbc -K $encrypted_6de54f97b202_key -iv $encrypted_6de54f97b202_iv 15 | -in dokku-deploy.enc -out dokku-deploy -d 16 | -------------------------------------------------------------------------------- /deployment-template/azuredeploy.parameters.a.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "adminUsername": { 6 | "value": "armvalidatoradmin" 7 | }, 8 | "publicIpAddressName": { 9 | "value": "template-bot-a-publicIp" 10 | }, 11 | "publicIpAddressResourceGroup": { 12 | "value": "template-bot-public-ips" 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /deployment-template/azuredeploy.parameters.b.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "adminUsername": { 6 | "value": "armvalidatoradmin" 7 | }, 8 | "publicIpAddressName": { 9 | "value": "template-bot-b-publicIp" 10 | }, 11 | "publicIpAddressResourceGroup": { 12 | "value": "template-bot-public-ips" 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /service-start.sh: -------------------------------------------------------------------------------- 1 | # 2 | # this script is used to set the app up as a service to run in the bg 3 | sudo forever-service install armvalidator –-script ./bin/www 4 | 5 | #After service installation, you can start service by running following command. 6 | sudo service armvalidator start 7 | 8 | #Check status if app is running or stopped 9 | sudo service armvalidator status 10 | 11 | #You can use other following commands for start/stop/ operations. 12 | #sudo service armvalidator stop #to stop app 13 | #sudo service armvalidator start # 14 | -------------------------------------------------------------------------------- /modules/mongo_helper.js: -------------------------------------------------------------------------------- 1 | var MongoClient = require('mongodb').MongoClient, 2 | conf = require('./config'), 3 | denodeify = require('es6-denodeify')(Promise); 4 | 5 | var connect = function () { 6 | // Connection URL 7 | var url = conf.get('MONGO_URL'); 8 | var connectPromise = denodeify(MongoClient.connect); 9 | 10 | return connectPromise.call(MongoClient, url) 11 | .catch(err => { 12 | console.error('Failed to conenct to: ' + url); 13 | console.error(err); 14 | }); 15 | }; 16 | 17 | module.exports = { 18 | connect: connect 19 | }; 20 | -------------------------------------------------------------------------------- /test/assets/dokku-vm/azuredeploy.parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "newStorageAccountName":{ 6 | "value": "GEN-UNIQUE" 7 | }, 8 | "location": { 9 | "value": "West US" 10 | }, 11 | "adminUsername": { 12 | "value": "sedouard" 13 | }, 14 | "sshKeyData": { 15 | "value": "GEN-SSH-PUB-KEY" 16 | }, 17 | "dnsNameForPublicIP": { 18 | "value": "GEN-UNIQUE" 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /test/assets/dokku-vm/azuredeploy.parameters.gen_unique_var.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "newStorageAccountName":{ 6 | "value": "GEN-UNIQUE-8" 7 | }, 8 | "location": { 9 | "value": "West US" 10 | }, 11 | "adminUsername": { 12 | "value": "GEN-UNIQUE-8" 13 | }, 14 | "sshKeyData": { 15 | "value": "GEN-SSH-PUB-KEY" 16 | }, 17 | "dnsNameForPublicIP": { 18 | "value": "GEN-UNIQUE-24" 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "esnext": true, 3 | "node": true, 4 | "browser": true, 5 | "nomen": false, 6 | "bitwise": true, 7 | "eqeqeq": true, 8 | "forin": true, 9 | "immed": true, 10 | "latedef": true, 11 | "newcap": true, 12 | "noarg": true, 13 | "noempty": true, 14 | "nonew": true, 15 | "plusplus": true, 16 | "regexp": true, 17 | "undef": true, 18 | "unused": true, 19 | "trailing": true, 20 | "indent": 4, 21 | "esnext": true, 22 | "onevar": true, 23 | "white": true, 24 | "quotmark": "single", 25 | "predef": { 26 | } 27 | } 28 | 29 | -------------------------------------------------------------------------------- /archive/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | git init 4 | git config user.name "Travis CI" 5 | git config user.email "sedouard@microsoft.com" 6 | 7 | echo 'Adding files to local repo ' 8 | ls -ltr 9 | git add . 10 | git commit -m "Deploy" 11 | 12 | GIT_USERNAME="dokku" 13 | GIT_TARGET_URL="${GIT_USERNAME}@${AZURE_WA_GIT_TARGET}:${DOKKU_APPNAME}" 14 | 15 | eval "$(ssh-agent -s)" 16 | ssh-agent -s 17 | 18 | # write the trusted host to the known hosts file 19 | # to avoid a prompt when connecting to it via ssh 20 | echo $DOKKU_SSH_PUBLIC_KNOWN_HOST > ~/.ssh/known_hosts 21 | 22 | chmod 600 ./dokku-deploy 23 | ssh-add ./dokku-deploy 24 | 25 | echo 'Private keys added. Starting Dokku Deployment' 26 | git remote add $GIT_USERNAME $GIT_TARGET_URL 27 | 28 | git push dokku master 29 | 30 | echo 'Deployed Latest Version of Arm Validator' 31 | -------------------------------------------------------------------------------- /test/assets/dokku-vm/README.md: -------------------------------------------------------------------------------- 1 | # Deploy a Dokku Instance 2 | 3 | [![Deploy to Azure](http://azuredeploy.net/deploybutton.png)](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2FAzure%2Fazure-quickstart-templates%2Fmaster%2F101-dokku%2Fazuredeploy.json) 4 | 5 | [Dokku](http://progrium.viewdocs.io/dokku/) is the smallest PaaS you've ever seen. It's a mini-heroku-style PaaS on a single VM. You can git-push any heroku-buildpack compatible app or Dockerfile - including ruby, go, python and php apps. 6 | 7 | Use the **Deploy to Azure** button above to get started. All you need to do is specify a unique name for the storage account and the sub domain name for the VM's public IP address. 8 | 9 | Checkout Dokku's [official documentation](http://progrium.viewdocs.io/dokku/application-deployment/) to learn how to deploy apps. -------------------------------------------------------------------------------- /test/modules/github_helper.js: -------------------------------------------------------------------------------- 1 | /*global describe, it*/ 2 | var assert = require('assert'); 3 | require('../helpers/setup_env'); 4 | 5 | var mockery = require('mockery'); 6 | mockery.registerSubstitute('unirest', '../test/mocks/unirest'); 7 | mockery.enable({ 8 | warnOnUnregistered: false 9 | }); 10 | var githubHelper = require('../../modules/github_helper'); 11 | 12 | describe('Github Helper Tests', () => { 13 | 14 | it('Gets the PR head repo link', () => { 15 | return githubHelper.getPullRequestBaseLink('44') 16 | .then(link => { 17 | // validation is also done by mock 18 | assert.ok(link, 'Expected link to not be null'); 19 | // should be pointing to this repo specified by ./tests/assets/githubresponse.json 20 | assert.equal(link, 'https://raw.githubusercontent.com/sedouard/testtemplate/test_branch_name', 'Expected link: ' + link + ' to contain sedouard/testtemplate'); 21 | }); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | var grunt = require('grunt'); 2 | require('load-grunt-tasks')(grunt); 3 | 4 | var files = ['modules/**/*.js', 'public/javascripts/**/*.js', 'routes/**/*.js', 'test/**/*.js']; 5 | 6 | grunt.initConfig({ 7 | mochacli: { 8 | options: { 9 | reporter: 'spec', 10 | bail: false 11 | }, 12 | all: ['test/modules/*.js', 'test/routes/*.js'] 13 | }, 14 | jshint: { 15 | files: files, 16 | options: { 17 | jshintrc: '.jshintrc' 18 | } 19 | }, 20 | jscs: { 21 | files: { 22 | src: files 23 | }, 24 | options: { 25 | config: '.jscsrc', 26 | esnext: true 27 | } 28 | }, 29 | jsbeautifier: { 30 | write: { 31 | files: { 32 | src: files 33 | }, 34 | options: { 35 | config: '.beautifyrc' 36 | } 37 | } 38 | } 39 | }); 40 | grunt.registerTask('test', ['jshint', 'jscs', 'mochacli']); 41 | 42 | -------------------------------------------------------------------------------- /nginx.conf.template: -------------------------------------------------------------------------------- 1 | server { 2 | listen [::]:80; 3 | listen 80; 4 | server_name $NOSSL_SERVER_NAME; 5 | access_log /var/log/nginx/${APP}-access.log; 6 | error_log /var/log/nginx/${APP}-error.log; 7 | keepalive_timeout 3600000; 8 | proxy_connect_timeout 3600000; 9 | proxy_send_timeout 3600000; 10 | proxy_read_timeout 3600000; 11 | send_timeout 3600000; 12 | # set a custom header for requests 13 | add_header X-Served-By www-ec2-01; 14 | 15 | location / { 16 | proxy_pass http://$APP; 17 | proxy_http_version 1.1; 18 | proxy_set_header Upgrade \$http_upgrade; 19 | proxy_set_header Connection "upgrade"; 20 | proxy_set_header Host \$http_host; 21 | proxy_set_header X-Forwarded-Proto \$scheme; 22 | proxy_set_header X-Forwarded-For \$remote_addr; 23 | proxy_set_header X-Forwarded-Port \$server_port; 24 | proxy_set_header X-Request-Start \$msec; 25 | } 26 | include $DOKKU_ROOT/$APP/nginx.conf.d/*.conf; 27 | } 28 | 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Microsoft and Steven Edouard 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /modules/github_helper.js: -------------------------------------------------------------------------------- 1 | var unirest = require('unirest'); 2 | var path = require('path'); 3 | var conf = require('./config'); 4 | var RSVP = require('rsvp'); 5 | var debug = require('debug')('arm-validator:github'); 6 | 7 | exports.getPullRequestBaseLink = function (prNumber) { 8 | 9 | return new RSVP.Promise((resolve, reject) => { 10 | var githubPath = 'https://' + path.join('api.github.com/repos/', conf.get('GITHUB_REPO'), '/pulls/', prNumber.toString()); 11 | githubPath += '?client_id=' + conf.get('GITHUB_CLIENT_ID') + '&client_secret=' + conf.get('GITHUB_CLIENT_SECRET'); 12 | debug('making github request: GET - ' + githubPath); 13 | unirest.get(githubPath) 14 | .headers({ 15 | 'User-Agent': 'Mozilla/5.0' 16 | }) 17 | .end(response => { 18 | 19 | if (response.error) { 20 | return reject(response.error); 21 | } 22 | debug('Rate Limit Remaining: ' + response.headers['x-ratelimit-remaining']); 23 | return resolve(response.body); 24 | }); 25 | }) 26 | .then(body => { 27 | return 'https://' + path.join('raw.githubusercontent.com', body.head.repo.full_name, body.head.ref); 28 | }); 29 | }; 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "arm-validator", 3 | "version": "0.0.1", 4 | "private": true, 5 | "scripts": { 6 | "start": "node ./bin/www", 7 | "test" : "grunt test" 8 | }, 9 | "description": "A tiny server which will validate Azure Resource Templates", 10 | "dependencies": { 11 | "azure-arm-resource": "^0.10.6", 12 | "azure-common": "^0.9.16", 13 | "azure-scripty-cli2": "git+https://github.com/SpektraSystems/azure-scripty-cli2.git", 14 | "body-parser": "~1.13.2", 15 | "cookie-parser": "~1.3.5", 16 | "debug": ">=2.6.9", 17 | "es6-denodeify": "^0.1.5", 18 | "express": "~4.13.1", 19 | "guid": "0.0.12", 20 | "http-delayed-response": "0.0.4", 21 | "jade": "~1.11.0", 22 | "mongodb": "^2.0.48", 23 | "morgan": "~1.6.1", 24 | "nconf": "^0.8.2", 25 | "rsvp": "^3.1.0", 26 | "serve-favicon": "~2.3.0", 27 | "unirest": "^0.4.2" 28 | }, 29 | "devDependencies": { 30 | "grunt": "^0.4.5", 31 | "grunt-contrib-jshint": "^0.11.3", 32 | "grunt-jsbeautifier": "^0.2.10", 33 | "grunt-jscs": "^2.5.0", 34 | "grunt-mocha-cli": "^2.0.0", 35 | "load-grunt-tasks": "^3.3.0", 36 | "mocha": "^2.3.3", 37 | "mockery": "^1.4.0", 38 | "supertest": "^1.1.0" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /test/mocks/mongodb.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var Db = function () { 3 | 4 | }; 5 | var Collection = function () { 6 | 7 | }; 8 | var MongoClient = function () { 9 | 10 | }; 11 | Collection.prototype.insert = function (object, cb) { 12 | assert(typeof object === 'object', 'Expected collection.insert.object parameter 1 to be of type object'); 13 | return cb(null, { 14 | ops: { 15 | length: 1 16 | } 17 | }); 18 | }; 19 | Collection.prototype.deleteOne = function (object, cb) { 20 | assert(typeof object === 'object', 'Expected collection.insert.object parameter 1 to be of type object'); 21 | return cb(null, { 22 | ops: { 23 | length: 1 24 | } 25 | }); 26 | }; 27 | Db.prototype.collection = function (collectionName) { 28 | assert.ok(collectionName, 'Expected collectionName parameter for db.collection to be non-empty string'); 29 | assert(typeof collectionName === 'string', 'Expected collectionName parameter for db.collection to be a string'); 30 | return new Collection(); 31 | }; 32 | MongoClient.prototype.connect = function (url, cb) { 33 | assert.ok(url, 'Expected url parameter for MongoClient.connect to be non-empty string'); 34 | assert(typeof url === 'string', 'Expected url parameter for MongoClient.connect to be a string'); 35 | cb(null, new Db()); 36 | }; 37 | 38 | exports.MongoClient = new MongoClient(); 39 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are always welcome! Be sure to follow the [github workflow](https://guides.github.com/introduction/flow/) when contributing to this project: 4 | 5 | * Create an issue, or comment on an issue to indicate what you are working on. This avoids work duplication. 6 | * Fork the repository and clone to your local machine 7 | * You should already be on the default branch `master` - if not, check it out (`git checkout master`) 8 | * Create a new branch for your feature/fix `git checkout -b my-new-feature`) 9 | * Write your feature/fix 10 | * Stage the changed files for a commit (`git add .`) 11 | * Commit your files with a *useful* commit message ([example](https://github.com/Azure/azure-quickstart-templates/commit/53699fed9983d4adead63d9182566dec4b8430d4)) (`git commit`) 12 | * Push your new branch to your GitHub Fork (`git push origin my-new-feature`) 13 | * Visit this repository in GitHub and create a Pull Request. 14 | 15 | # Running the Tests 16 | 17 | Tests are written in the [`./test`](./test) folder, with mocks for Azure and MongoDB in [`./test/mocks`](./test/mocks). To run the tests you'll need to install [`grunt-cli`](https://npmjs.org/grunt-cli) and [`mocha-cli`](https://npmjs.org/mocha). After running `npm install` on this repository, run: 18 | 19 | ``` 20 | grunt test 21 | ``` 22 | 23 | These are the same tests that are ran in Travis CI. Happy coding! -------------------------------------------------------------------------------- /test/mocks/unirest.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var assert = require('assert'); 3 | var targetUrl = ''; 4 | exports.get = function (url) { 5 | assert.ok(url, 'Expected unirest.get url parameter to be non empty string'); 6 | assert(typeof url === 'string', 'Expected unirest.get url parameter to be of type string'); 7 | targetUrl = url; 8 | return exports; 9 | }; 10 | exports.end = function (cb) { 11 | // the repo Azure/azure-quickstart-templates is specified in .example-config.json 12 | if (targetUrl.indexOf('https://api.github.com/repos/Azure/azure-quickstart-templates/pulls/44' > -1)) { 13 | var jsonString = fs.readFileSync('./test/assets/githubresponse.json', { 14 | encoding: 'utf8' 15 | }).trim(); 16 | var jsonData = JSON.parse(jsonString); 17 | return cb({ 18 | headers: { 19 | 'x-ratelimit-remaining': 4999 20 | }, 21 | body: jsonData 22 | }); 23 | } 24 | }; 25 | exports.headers = function (headers) { 26 | assert.ok(headers, 'Expected unirest.headers to be non-null object'); 27 | assert(typeof headers === 'object', 'Expected unirest.headers headers object to be of type object'); 28 | 29 | for (var key in headers) { 30 | if (typeof key === 'string') { 31 | assert(typeof headers[key] === 'string', 'Expected header value: ' + headers[key] + ' to be of type string'); 32 | } else { 33 | assert.fail('string', typeof key, 'expected header key: ' + key + ' to be of type string'); 34 | } 35 | } 36 | return exports; 37 | }; 38 | -------------------------------------------------------------------------------- /test/modules/azure.js: -------------------------------------------------------------------------------- 1 | /*global describe, it*/ 2 | var assert = require('assert'); 3 | require('../helpers/setup_env'); 4 | 5 | var mockery = require('mockery'); 6 | mockery.registerSubstitute('azure-scripty', '../test/mocks/azure'); 7 | mockery.registerSubstitute('mongodb', '../test/mocks/mongodb'); 8 | mockery.enable({ 9 | warnOnUnregistered: false 10 | }); 11 | var azureTools = require('../../modules/azure'); 12 | 13 | describe('Azure Tools Tests', () => { 14 | 15 | it('logs in', () => { 16 | return azureTools.login() 17 | .then(() => { 18 | // validation is done by mock 19 | assert.ok(true); 20 | }); 21 | }); 22 | 23 | it('Validates Template', () => { 24 | return azureTools.validateTemplate('./test/assets/dokku-vm/azuredeploy.json', 25 | './test/assets/dokku-vm/azuredeploy.parameters.json') 26 | .then(() => { 27 | // validation is done by mock 28 | assert.ok(true); 29 | }); 30 | }); 31 | 32 | it('Deletes a group', () => { 33 | return azureTools.deleteGroup('testgroupname') 34 | .then(() => { 35 | // validation is done by mock 36 | assert.ok(true); 37 | }); 38 | }); 39 | 40 | it('It deploys a template', () => { 41 | return azureTools.testTemplate('./test/assets/dokku-vm/azuredeploy.json', 42 | './test/assets/dokku-vm/azuredeploy.parameters.json', 'testgroupname') 43 | .then(() => { 44 | // validation is done by mock 45 | assert.ok(true); 46 | }); 47 | }); 48 | 49 | }); 50 | -------------------------------------------------------------------------------- /bin/www: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | 7 | var app = require('../app'); 8 | var debug = require('debug')('arm-validator:server'); 9 | var http = require('http'); 10 | 11 | /** 12 | * Get port from environment and store in Express. 13 | */ 14 | 15 | var port = normalizePort(process.env.PORT || '3000'); 16 | app.set('port', port); 17 | 18 | /** 19 | * Create HTTP server. 20 | */ 21 | 22 | var server = http.createServer(app); 23 | 24 | /** 25 | * Listen on provided port, on all network interfaces. 26 | */ 27 | 28 | server.listen(port); 29 | server.on('error', onError); 30 | server.on('listening', onListening); 31 | server.on('connection', function (socket) { 32 | console.log("A new connection was made by a client."); 33 | // templates take a while to deploy, have a long timeout 34 | socket.setTimeout(3600 * 1000); 35 | }); 36 | /** 37 | * Normalize a port into a number, string, or false. 38 | */ 39 | 40 | function normalizePort(val) { 41 | var port = parseInt(val, 10); 42 | 43 | if (isNaN(port)) { 44 | // named pipe 45 | return val; 46 | } 47 | 48 | if (port >= 0) { 49 | // port number 50 | return port; 51 | } 52 | 53 | return false; 54 | } 55 | 56 | /** 57 | * Event listener for HTTP server "error" event. 58 | */ 59 | 60 | function onError(error) { 61 | if (error.syscall !== 'listen') { 62 | throw error; 63 | } 64 | 65 | var bind = typeof port === 'string' 66 | ? 'Pipe ' + port 67 | : 'Port ' + port; 68 | 69 | // handle specific listen errors with friendly messages 70 | switch (error.code) { 71 | case 'EACCES': 72 | console.error(bind + ' requires elevated privileges'); 73 | process.exit(1); 74 | break; 75 | case 'EADDRINUSE': 76 | console.error(bind + ' is already in use'); 77 | process.exit(1); 78 | break; 79 | default: 80 | throw error; 81 | } 82 | } 83 | 84 | /** 85 | * Event listener for HTTP server "listening" event. 86 | */ 87 | 88 | function onListening() { 89 | var addr = server.address(); 90 | var bind = typeof addr === 'string' 91 | ? 'pipe ' + addr 92 | : 'port ' + addr.port; 93 | debug('Listening on ' + bind); 94 | } 95 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var express = require('express'); 4 | var path = require('path'); 5 | var favicon = require('serve-favicon'); 6 | var logger = require('morgan'); 7 | var cookieParser = require('cookie-parser'); 8 | var bodyParser = require('body-parser'); 9 | var azureTools = require('./modules/azure'); 10 | var validate = require('./routes/validate'); 11 | var debug = require('debug')('arm-validator:server'); 12 | var app = express(); 13 | 14 | // view engine setup 15 | app.set('views', path.join(__dirname, 'views')); 16 | app.set('view engine', 'jade'); 17 | 18 | // uncomment after placing your favicon in /public 19 | //app.use(favicon(path.join(__dirname, 'public', 'favicon.ico'))); 20 | app.use(logger('dev')); 21 | app.use(bodyParser.json()); 22 | app.use(bodyParser.urlencoded({ extended: false })); 23 | app.use(cookieParser()); 24 | app.use(express.static(path.join(__dirname, 'public'))); 25 | 26 | app.use('/', validate); 27 | // catch 404 and forward to error handler 28 | app.use(function(req, res, next) { 29 | var err = new Error('Not Found'); 30 | err.status = 404; 31 | next(err); 32 | }); 33 | 34 | // error handlers 35 | 36 | // development error handler 37 | // will print stacktrace 38 | if (app.get('env') === 'development') { 39 | app.use(function(err, req, res, next) { 40 | res.status(err.status || 500); 41 | res.render('error', { 42 | message: err.message, 43 | error: err 44 | }); 45 | }); 46 | } 47 | 48 | // production error handler 49 | // no stacktraces leaked to user 50 | app.use(function(err, req, res, next) { 51 | res.status(err.status || 500); 52 | res.render('error', { 53 | message: err.message, 54 | error: {} 55 | }); 56 | }); 57 | 58 | // init 59 | azureTools.login() 60 | .then(function () { 61 | debug('Sucessfully logged into azure subscription'); 62 | // clear any existing groups that may have been left 63 | // behind possilby due to a restart 64 | return azureTools.deleteExistingGroups(); 65 | }) 66 | .catch(function (err) { 67 | debug('Failed to Initialize Properly'); 68 | debug('This can be either to unable to login or a problem with'); 69 | debug('matching resource groups to those stored in the database:'); 70 | debug(err); 71 | }); 72 | 73 | module.exports = app; 74 | -------------------------------------------------------------------------------- /.example-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "comment": "You can either set these values as environment variables or to a file called '.config.json' at the root of the repo", 3 | "AZURE_CLIENT_ID": "00000000-0000-0000-0000-000000000000", 4 | "AZURE_TENANT_ID": "00000000-0000-0000-0000-000000000000", 5 | "AZURE_CLIENT_SECRET": "00000000-0000-0000-0000-000000000000", 6 | "AZURE_REGION": "westus", 7 | "TEST_RESOURCE_GROUP_NAME": "azure_test_rg", 8 | "RESOURCE_GROUP_NAME_PREFIX": "qstci-", 9 | "MONGO_URL": "mongodb://localhost:27017/arm-validator", 10 | "PARAM_REPLACE_INDICATOR": "GEN-UNIQUE", 11 | "SSH_KEY_REPLACE_INDICATOR": "GEN-SSH-PUB-KEY", 12 | "SSH_PUBLIC_KEY": "ssh-rsa create an ssh public key using ssh-keygen", 13 | 14 | "GUID_REPLACE_INDICATOR": "GEN-GUID", 15 | "PASSWORD_REPLACE_INDICATOR": "GEN-PASSWORD", 16 | "ENV_PREFIX_REPLACE_INDICATOR": "GEN-", 17 | 18 | "GITHUB_REPO": "Azure/azure-quickstart-templates", 19 | "GITHUB_CLIENT_ID": "00000000-0000-0000-0000-000000000000", 20 | "GITHUB_CLIENT_SECRET": "00000000-0000-0000-0000-000000000000", 21 | 22 | "SSH-PUB-KEY": "ssh-rsa create an ssh public key using ssh-keygen", 23 | 24 | "VHDRESOURCEGROUP-NAME": "Existing resource group with pre-created resources", 25 | "VHDSTORAGEACCOUNT-NAME" : "Existing storage account with in the resource group", 26 | "SPECIALIZED-WINVHD-URI": "Specialized Windows VHD URI. This should be in same region as of deployments.", 27 | "GENERALIZED-WINVHD-URI": "Generalized Windows VHD URI. This should be in same region as of deployments.", 28 | "DATAVHD-URI": "Data disk VHD URI. This should be in same region as of deployments.", 29 | 30 | "KEYVAULT-NAME" : "Name of key vault", 31 | "KEYVAULT-FQDN-URI" : "https://keyvalutname.keyvault.azure.net", 32 | "KEYVAULT-RESOURCE-ID" : "ARM resource id for key vault", 33 | "KEYVAULT-SSL-SECRET-NAME" : "Secret name for SSL certificate in key vault", 34 | "KEYVAULT-SSL-SECRET-URI" : "URI for SSL certificate secret in key vault", 35 | 36 | "CUSTOM-FQDN-NAME" : "Fully qualified custom domain name, properly configured with CNAME mapping", 37 | "CUSTOM-WEBAPP-NAME": "First part of custom fqdn name", 38 | "CUSTOM-DOMAIN-SSLCERT-PASSWORD": "SSL certificate password", 39 | "CUSTOM-DOMAIN-SSLCERT-THUMBPRINT": "SSL certificate thumbprint", 40 | "CUSTOM-DOMAIN-SSLCERT-PFXDATA": "SSL certificate PFX data in base 64 encoded format" 41 | 42 | } 43 | -------------------------------------------------------------------------------- /.jscsrc: -------------------------------------------------------------------------------- 1 | { 2 | "disallowEmptyBlocks": true, 3 | "disallowKeywords": [ 4 | "with" 5 | ], 6 | "disallowKeywordsOnNewLine": [ 7 | "else" 8 | ], 9 | "disallowMixedSpacesAndTabs": true, 10 | "disallowMultipleLineBreaks": true, 11 | "disallowNewlineBeforeBlockStatements": true, 12 | "disallowPaddingNewlinesInBlocks": false, 13 | "disallowQuotedKeysInObjects": true, 14 | "disallowSpaceAfterObjectKeys": true, 15 | "disallowSpaceAfterPrefixUnaryOperators": true, 16 | "disallowSpaceBeforeBinaryOperators": [ 17 | ], 18 | "disallowSpaceBeforePostfixUnaryOperators": true, 19 | "disallowSpacesInFunctionDeclaration": { 20 | "beforeOpeningRoundBrace": true 21 | }, 22 | "disallowSpacesInNamedFunctionExpression": { 23 | "beforeOpeningRoundBrace": true 24 | }, 25 | "disallowSpacesInsideArrayBrackets": true, 26 | "disallowSpacesInsideObjectBrackets": "all", 27 | "disallowSpacesInsideParentheses": true, 28 | "disallowTrailingComma": true, 29 | "disallowTrailingWhitespace": true, 30 | "disallowYodaConditions": true, 31 | "requireBlocksOnNewline": 1, 32 | "requireCamelCaseOrUpperCaseIdentifiers": "ignoreProperties", 33 | "requireCapitalizedConstructors": true, 34 | "requireCommaBeforeLineBreak": true, 35 | "requireCurlyBraces": [ 36 | "if", 37 | "else", 38 | "for", 39 | "while", 40 | "do", 41 | "try", 42 | "catch" 43 | ], 44 | "requireDotNotation": true, 45 | "requireLineFeedAtFileEnd": true, 46 | "requireParenthesesAroundIIFE": true, 47 | "requireSpaceAfterBinaryOperators": true, 48 | "requireSpaceAfterKeywords": [ 49 | "else", 50 | "for", 51 | "while", 52 | "do", 53 | "switch", 54 | "case", 55 | "return", 56 | "try", 57 | "function", 58 | "typeof" 59 | ], 60 | "requireSpaceAfterLineComment": "allowSlash", 61 | "requireSpaceBeforeBinaryOperators": true, 62 | "requireSpaceBeforeBlockStatements": true, 63 | "requireSpacesInAnonymousFunctionExpression": { 64 | "beforeOpeningRoundBrace": true 65 | }, 66 | "requireSpacesInConditionalExpression": true, 67 | "safeContextKeyword": [ 68 | "self" 69 | ], 70 | "validateIndentation": 2, 71 | "validateQuoteMarks": "'", 72 | "esnext": true 73 | } 74 | 75 | -------------------------------------------------------------------------------- /test/mocks/azure.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var fs = require('fs'); 3 | 4 | exports.invoke = function (cmd, cb) { 5 | switch (cmd.command) { 6 | case 'login --service-principal': 7 | assert.ok(cmd.command, 'Expected command.username to be a non-empty string'); 8 | assert(typeof cmd.username === 'string', 'Expected command.username to be a string'); 9 | assert.ok(cmd.password, 'Expected command.password to be a non-empty string'); 10 | assert(typeof cmd.password === 'string', 'Expected command.password to be a string'); 11 | assert.ok(cmd.tenant, 'Expected command.tenant to be a non-empty string'); 12 | assert(typeof cmd.tenant === 'string', 'Expected command.tenant to be a string'); 13 | break; 14 | case 'config mode arm': 15 | break; 16 | case 'group deployment create': 17 | case 'group template validate': 18 | assert.ok(cmd['resource-group'], 'Expected command.resource-group to be a non-empty string'); 19 | assert(fs.existsSync(cmd['template-file']), 'Expected command.template-file to point to an existing file path'); 20 | assert.ok(cmd['parameters-file'], 'Expected command.parameters-file to be a non-empty string'); 21 | assert(typeof cmd['parameters-file'] === 'string', 'Expected command.parameters-file to be a string'); 22 | assert(fs.existsSync(cmd['parameters-file']), 'Expected command.parameters-file to point to an existing file path'); 23 | break; 24 | case 'group create': 25 | assert.equal(cmd.positional.length, 2, 'Expected group create command to have 2 parameters'); 26 | assert.ok(typeof cmd.positional[0], 'Expected group create command paramter 1 to be string'); 27 | assert.equal(typeof cmd.positional[0], 'string', 'Expected group create command paramter 1 to be string'); 28 | assert.ok(typeof cmd.positional[1], 'Expected group create command paramter 2 to be string'); 29 | assert.equal(typeof cmd.positional[1], 'string', 'Expected group create command paramter 2 to be string'); 30 | break; 31 | case 'group delete': 32 | assert.equal(cmd.positional.length, 1, 'Expected group delete command to have 1 parameters'); 33 | assert.ok(typeof cmd.positional[0], 'Expected group delete command paramter 1 to be string'); 34 | assert.equal(typeof cmd.positional[0], 'string', 'Expected group delete command paramter 1 to be string'); 35 | break; 36 | default: 37 | throw 'Mock Azure cli interface doesnt have an implementation for command: ' + cmd.command + ' please create one'; 38 | } 39 | // execute callback 40 | return cb(null); 41 | }; 42 | -------------------------------------------------------------------------------- /modules/param_helper.js: -------------------------------------------------------------------------------- 1 | var conf = require('./config'); 2 | var Guid = require('guid'); 3 | var debug = require('debug')('arm-validator:param_helper'); 4 | var assert = require('assert'); 5 | 6 | exports.replaceKeyParameters = function (parameters) { 7 | var parametersString = JSON.stringify(parameters); 8 | // for unique parameters replace each with a guid 9 | var matches = parametersString.match(new RegExp(conf.get('PARAM_REPLACE_INDICATOR') + '-\\d+', 'g')); 10 | if (matches) { 11 | matches.forEach(match => { 12 | var splitPoint = match.indexOf('-', conf.get('PARAM_REPLACE_INDICATOR').length); 13 | var length = parseInt(match.substring(splitPoint + 1, match.length)); 14 | assert(typeof length === 'number', 'Variable length unique params must be appened by an integer'); 15 | assert(length >= 3 && length <= 32, 'Variable length unique params must specify a length between 3 and 32'); 16 | var replaceValue = 'ci' + Guid.raw().replace(/-/g, '').substring(0, length - 2); 17 | debug('replacing: ' + match); 18 | debug('with:' + replaceValue); 19 | parametersString = parametersString.replace(match, replaceValue); 20 | }); 21 | } 22 | 23 | matches = parametersString.match(new RegExp(conf.get('PARAM_REPLACE_INDICATOR'), 'g')); 24 | if (matches) { 25 | matches.forEach(match => { 26 | parametersString = parametersString.replace(match, 'ci' + Guid.raw().replace(/-/g, '').substring(0, 16)); 27 | }); 28 | } 29 | 30 | parametersString = parametersString.replace(new RegExp(conf.get('SSH_KEY_REPLACE_INDICATOR'), 'g'), conf.get('SSH_PUBLIC_KEY')); 31 | parametersString = parametersString.replace(new RegExp(conf.get('PASSWORD_REPLACE_INDICATOR'), 'g'), 'ciP$ss' + Guid.raw().replace(/-/g, '').substring(0, 16)); 32 | parametersString = parametersString.replace(new RegExp(conf.get('GUID_REPLACE_INDICATOR'), 'g'), Guid.raw()); 33 | 34 | // This check should be done at the end, after replacing GEN-GUID, GEN-UNIQUE, GEN-UNIQUE-n etc. 35 | matches = parametersString.match(new RegExp(conf.get('ENV_PREFIX_REPLACE_INDICATOR') + '[a-zA-Z0-9\\-]+', 'g')); 36 | if (matches) { 37 | matches.forEach(match => { 38 | var splitPoint = conf.get('ENV_PREFIX_REPLACE_INDICATOR').length; 39 | var envKeyName = match.substring(splitPoint, match.length); 40 | var replaceValue = conf.get(envKeyName); 41 | debug('replacing: ' + match); 42 | debug('with:' + replaceValue); 43 | parametersString = parametersString.replace(match, replaceValue); 44 | }); 45 | } 46 | 47 | debug('rendered parameters string: '); 48 | debug(parametersString); 49 | return JSON.parse(parametersString); 50 | }; 51 | -------------------------------------------------------------------------------- /test/routes/validate.js: -------------------------------------------------------------------------------- 1 | /*global describe, it, beforeEach*/ 2 | var request = require('supertest'); 3 | var express = require('express'); 4 | var RSVP = require('rsvp'); 5 | var fs = require('fs'); 6 | var assert = require('assert'); 7 | var configString = fs.readFileSync('./.example-config.json', { 8 | encoding: 'utf8' 9 | }).trim(); 10 | var configData = JSON.parse(configString); 11 | 12 | for (var key in configData) { 13 | if (typeof key === 'string') { 14 | process.env[key] = configData[key]; 15 | } 16 | } 17 | 18 | var mockery = require('mockery'); 19 | mockery.registerSubstitute('azure-scripty', '../test/mocks/azure'); 20 | mockery.registerSubstitute('mongodb', '../test/mocks/mongodb'); 21 | mockery.registerSubstitute('unirest', '../test/mocks/unirest'); 22 | mockery.enable({ 23 | warnOnUnregistered: false 24 | }); 25 | var validate = require('../../routes/validate'); 26 | var bodyParser = require('body-parser'); 27 | 28 | function setupRequestBody() { 29 | var templateString = fs.readFileSync('./test/assets/dokku-vm/azuredeploy.json', { 30 | encoding: 'utf8' 31 | }).trim(); 32 | var templateObject = JSON.parse(templateString); 33 | var parametersString = fs.readFileSync('./test/assets/dokku-vm/azuredeploy.parameters.json', { 34 | encoding: 'utf8' 35 | }).trim(); 36 | var parametersObject = JSON.parse(parametersString); 37 | var requestBody = { 38 | template: templateObject, 39 | parameters: parametersObject, 40 | pull_request: 44 41 | }; 42 | 43 | return requestBody; 44 | } 45 | describe('Validate Route Tests', () => { 46 | var app; 47 | beforeEach(() => { 48 | app = express(); 49 | app.use(bodyParser.json()); 50 | app.use('/', validate); 51 | }); 52 | 53 | it('Validates the Template', () => { 54 | 55 | var requestBody = setupRequestBody(); 56 | 57 | return new RSVP.Promise((resolve, reject) => { 58 | request(app) 59 | .post('/validate') 60 | .send(requestBody) 61 | .expect(200) 62 | .end((err, res) => { 63 | if (err) { 64 | assert.fail(null, err, 'Unexpected error ' + err); 65 | return reject(err); 66 | } 67 | assert.equal(res.body.result, 'Template Valid'); 68 | return resolve(res.body); 69 | }); 70 | }); 71 | 72 | }); 73 | 74 | it('Deploys the Template', () => { 75 | 76 | var requestBody = setupRequestBody(); 77 | 78 | return new RSVP.Promise((resolve, reject) => { 79 | request(app) 80 | .post('/deploy') 81 | .send(requestBody) 82 | .expect(202) 83 | .end((err, res) => { 84 | if (err) { 85 | assert.fail(null, err, 'Unexpected error ' + err); 86 | return reject(err); 87 | } 88 | 89 | if (res.body.result !== 'Deployment Successful') { 90 | return reject(res.body.result); 91 | } 92 | 93 | assert.equal(res.body.result, 'Deployment Successful'); 94 | return resolve(res.body); 95 | }); 96 | }); 97 | }); 98 | }); 99 | -------------------------------------------------------------------------------- /deployment-template/configure-bot.sh: -------------------------------------------------------------------------------- 1 | # this scripts expects the following args 2 | userName="$1" #user name which will be used as the directory to clone the repo - usually the admin 3 | artifactsLocation="$2" 4 | artifactsLocationSasToken="$3" 5 | 6 | #appPath='/var/www/azure-arm-validator' 7 | appPath="/home/$userName/azure-arm-validator" 8 | sudo mkdir $appPath 9 | 10 | # install azure cli - signing key has been changed - this will be broken 11 | #echo "deb [arch=amd64] https://packages.microsoft.com/repos/azure-cli/ wheezy main" | sudo tee /etc/apt/sources.list.d/azure-cli.list 12 | #sudo apt-key adv --keyserver packages.microsoft.com --recv-keys 417A0893 13 | #sudo apt-get install apt-transport-https 14 | #sudo apt-get update && sudo apt-get install azure-cli 15 | 16 | # here is the fix for CLI install 17 | AZ_REPO=$(lsb_release -cs) 18 | echo "deb [arch=amd64] https://packages.microsoft.com/repos/azure-cli/ $AZ_REPO main" | sudo tee /etc/apt/sources.list.d/azure-cli.list 19 | curl -L https://packages.microsoft.com/keys/microsoft.asc | sudo apt-key add - 20 | sudo apt-get install apt-transport-https 21 | sudo apt-get update && sudo apt-get install azure-cli 22 | 23 | 24 | #install git 25 | sudo apt-get update 26 | sudo apt-get install git 27 | 28 | #install npm 29 | sudo apt-get install npm --assume-yes 30 | sudo apt-get install nodejs-legacy 31 | sudo npm install forever -g 32 | sudo npm install -g forever-service -g 33 | 34 | #Import the public key used by the package management system. 35 | sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv EA312927 36 | 37 | #Create a list file for MongoDB 38 | echo "deb http://repo.mongodb.org/apt/ubuntu xenial/mongodb-org/3.2 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb-org-3.2.list 39 | 40 | #Reload local package database. 41 | sudo apt-get update 42 | 43 | #Install the MongoDB packages. 44 | sudo apt-get install -y mongodb-org 45 | 46 | #Start MongoDB 47 | sudo service mongod start 48 | 49 | #Configure MongoDB to run on system startup automatically by running this command 50 | sudo systemctl enable mongod.service 51 | 52 | #install bits for the app 53 | #git clone https://github.com/Azure/azure-arm-validator "/home/$userName/azure-arm-validator" #should probably pass this in, instead of hardcoding it 54 | git clone https://github.com/Azure/azure-arm-validator $appPath #should probably pass this in, instead of hardcoding it 55 | 56 | #Create a database where ARM validator will store its information. 57 | #We need to have at least one document inserted in the db. Use following command to insert document 58 | mongo 'arm-validator' --eval 'db["arm-validator"].armvalidator.insert({"name":"arm-validator-db"})' 59 | 60 | #enable monog service 61 | sudo systemctl enable mongod.service 62 | 63 | #try to download the config file if it was staged. 64 | echo "$artifactsLocation.config.json$artifactsLocationSasToken" 65 | curl -v -f -o "$appPath/.config.json" "$artifactsLocation.config.json$artifactsLocationSasToken" 66 | rc=$? 67 | if test "$rc" != "0"; then 68 | echo "curl failed with: $rc" 69 | else #start the app if the config file was staged 70 | echo "start the service if the config file was installed" 71 | sudo cd "$appPath" 72 | #sudo npm install 73 | 74 | # copy the service file - not sure which one we need and this isn't working right not anyway 75 | #sudo cp "$appPath/azure-arm-validator.service" '/etc/systemd/system' 76 | #sudo cp "$appPath/azure-arm-validator.service" '/lib/systemd/system' 77 | #sudo chmod +x "$appPath/app.js" 78 | #reset the service daemon and start the service 79 | #sudo systemctl daemon-reload 80 | #sudo systemctl start azure-arm-validator 81 | 82 | #setup the forever service 83 | #----------------------------------------------------------------- 84 | # currently this isn't working for some reason, need to run this interactively after deployment finishes 85 | npm install 86 | sudo forever-service install armvalidator --script ./bin/www 87 | sudo service armvalidator start 88 | 89 | fi 90 | 91 | #change ownership on the app, just to make life easier 92 | sudo chown -hR $userName "$appPath" 93 | 94 | 95 | 96 | -------------------------------------------------------------------------------- /.example-config.new.json: -------------------------------------------------------------------------------- 1 | { 2 | "comment1": "You can either set these values as environment variables or to a file called '.config.json' at the root of the repo", 3 | "commnet2": "Others constant values can be added without code changes just by adding them here", 4 | "AZURE_CLIENT_ID": "00000000-0000-0000-0000-000000000000", //the clientId of the SPN used for deployments in Azure 5 | "AZURE_TENANT_ID": "00000000-0000-0000-0000-000000000000", // the tenantId of the SPN used for deployments in Azure 6 | "AZURE_CLIENT_SECRET": "00000000-0000-0000-0000-000000000000", //the password for the SPN used for deployments in Azure 7 | "AZURE_SUBSCRIPTION_ID": "00000000-0000-0000-0000-000000000000",//subscription to use for deployments 8 | "AZURE_REGION": "westus", // this the region to use for template deployments 9 | "TEST_RESOURCE_GROUP_NAME": "template-bot-rg-for-validate", //this is the resourceGroup used to call the /validate api - it must already exist (we should probab fix this to use the created one) 10 | "RESOURCE_GROUP_NAME_PREFIX": "qstci-", // the prefix used for naming resourceGroups created during CI test deployments 11 | "MONGO_URL": "mongodb://localhost:27017/arm-validator", // the database used to store metadata about deployments created during CI 12 | 13 | "GITHUB_REPO": "Azure/azure-quickstart-templates", // the repo being used for grabbing PRs for CI 14 | "GITHUB_CLIENT_ID": "00000000-0000-0000-0000-000000000000", // clientId for the repo (if it's not public) 15 | "GITHUB_CLIENT_SECRET": "00000000-0000-0000-0000-000000000000", // client secret (if it's not public) 16 | 17 | // Below here are the tokens used to generate parameter values during CI 18 | "PARAM_REPLACE_INDICATOR": "GEN-UNIQUE", // generates a unique string 19 | "GUID_REPLACE_INDICATOR": "GEN-GUID", // generates a GUID 20 | "PASSWORD_REPLACE_INDICATOR": "GEN-PASSWORD", // generates a complex password 21 | "ENV_PREFIX_REPLACE_INDICATOR": "GEN-", // this prefix precedes all the others below and is used to indicate that a parameter value should be generated by CI 22 | "SSH_KEY_REPLACE_INDICATOR": "GEN-SSH-PUB-KEY", // keyword to use to generate an ssh public key 23 | "SSH_PUBLIC_KEY": "ssh-rsa create an ssh public key using ssh-keygen", // the SSH key generated, we really don't generate a key for some reason - we pull it from here 24 | 25 | "VHDRESOURCEGROUP-NAME": "Existing resource group with pre-created resources", // resource group that contains VHD prereqs - other resources are also stored here 26 | "VHDSTORAGEACCOUNT-NAME" : "Existing storage account with in the resource group", // the storage account that contains VHDs used for CI 27 | "SPECIALIZED-WINVHD-URI": "Specialized Windows VHD URI. This should be in same region as of deployments.", // specialized VHDs used for deployment - this MUST reside in the AZURE_REGION above 28 | "GENERALIZED-WINVHD-URI": "Generalized Windows VHD URI. This should be in same region as of deployments.", // generalized VHDs used for deployment - this MUST reside in the AZURE_REGION above 29 | "DATAVHD-URI": "Data disk VHD URI. This should be in same region as of deployments.", // datadisk VHDs used for deployment - this MUST reside in the AZURE_REGION above 30 | 31 | // KeyVault params - all of these refer to the same keyvault and MUST reside in AZURE_REGION above 32 | "KEYVAULT-NAME" : "Name of key vault", //resource name 33 | "KEYVAULT-FQDN-URI" : "the uri of the keyvault used by CI https://keyvalutname.keyvault.azure.net", //URI of the vault 34 | "KEYVAULT-RESOURCE-ID" : "ARM resourceId for key vault", //resourceId of the value 35 | "KEYVAULT-SSL-SECRET-NAME" : "Secret name for SSL certificate in key vault", //name of a secret in the vault that contains an SSL cert to use for test deployments 36 | "KEYVAULT-SSL-SECRET-URI" : "URI for SSL certificate secret in key vault", //uri of a secret in the vault that contains an SSL cert 37 | 38 | "CUSTOM-FQDN-NAME" : "Fully qualified custom domain name, properly configured with CNAME mapping", // ###AM:armvalidator17384.spektrademos.com, CNAME created already in public DNS mapping it to armvalidator17384.azurewebsites.net 39 | "CUSTOM-WEBAPP-NAME": "First part of custom fqdn name", // example? ###AM:armvalidator17384 - this will become webapp name, to support above value. 40 | "CUSTOM-DOMAIN-SSLCERT-PASSWORD": "SSL certificate password", 41 | "CUSTOM-DOMAIN-SSLCERT-THUMBPRINT": "SSL certificate thumbprint", 42 | "CUSTOM-DOMAIN-SSLCERT-PFXDATA": "SSL certificate PFX data in base 64 encoded format" 43 | 44 | } -------------------------------------------------------------------------------- /test/modules/param_helper.js: -------------------------------------------------------------------------------- 1 | /*global describe, it*/ 2 | /*jshint multistr: true */ 3 | var fs = require('fs'); 4 | var assert = require('assert'); 5 | require('../helpers/setup_env'); 6 | var conf = require('../../modules/config'); 7 | 8 | describe('Paramter Helper Tests', () => { 9 | 10 | it('Should replace ' + conf.get('PARAM_REPLACE_INDICATOR') + ' placeholder with a unqiue 16 character parameter', () => { 11 | // first read the sample template 12 | var paramHelper = require('../../modules/param_helper'); 13 | var parameterString = fs.readFileSync('./test/assets/dokku-vm/azuredeploy.parameters.json', { 14 | encoding: 'utf8' 15 | }).trim(); 16 | 17 | var placeholder = conf.get('PARAM_REPLACE_INDICATOR'); 18 | assert(parameterString.match(new RegExp(placeholder, 'g')).length > 0, 19 | 'In ./test/assets/dokku-vm/azuredeploy.parameters.json \ 20 | Expected ./test/assets/dokku-vm/azuredeploy.parameters.json to have GEN-UNIQUE placeholders'); 21 | var parameters = JSON.parse(parameterString); 22 | 23 | parameters = paramHelper.replaceKeyParameters(parameters); 24 | 25 | assert.equal(parameters.parameters.dnsNameForPublicIP.value.length, 18, 26 | 'In ./test/assets/dokku-vm/azuredeploy.parameters.gen_unique_var.json Expected parameters.parameters.dnsNameForPublicIP.length to be 18.'); 27 | assert.equal(parameters.parameters.newStorageAccountName.value.length, 18, 28 | 'In ./test/assets/dokku-vm/azuredeploy.parameters.gen_unique_var.json Expected parameters.parameters.newStorageAccountName.length to be 18.'); 29 | assert.notEqual(parameters.parameters.dnsNameForPublicIP.value, parameters.parameters.newStorageAccountName.value, 30 | 'In ./test/assets/dokku-vm/azuredeploy.parameters.gen_unique_var.json Expected parameters.parameters.newStorageAccountName and parameters.paramters.dnsNameForPublicIP to not be equal.'); 31 | 32 | parameterString = JSON.stringify(parameters); 33 | 34 | assert.equal(parameterString.match(new RegExp(placeholder, 'g')), null, 'In \ 35 | ./test/assets/dokku-vm/azuredeploy.parameters.json \ 36 | Expected all GEN-UNIQUE parameters to be replaced'); 37 | }); 38 | 39 | it('Should replace ' + conf.get('PARAM_REPLACE_INDICATOR') + '-[N] placeholder with a unqiue [N] character parameter', () => { 40 | // first read the sample template 41 | var paramHelper = require('../../modules/param_helper'); 42 | var parameterString = fs.readFileSync('./test/assets/dokku-vm/azuredeploy.parameters.gen_unique_var.json', { 43 | encoding: 'utf8' 44 | }).trim(); 45 | 46 | var placeholder = conf.get('PARAM_REPLACE_INDICATOR'); 47 | 48 | assert(parameterString.match(new RegExp(placeholder + '-\\d+', 'g')).length > 0, 49 | 'In ./test/assets/dokku-vm/azuredeploy.parameters.gen_unique_var.json \ 50 | Expected ./test/assets/dokku-vm/azuredeploy.parameters.gen_unique_var.json to have GEN-UNIQUE placeholders'); 51 | var parameters = JSON.parse(parameterString); 52 | 53 | parameters = paramHelper.replaceKeyParameters(parameters); 54 | 55 | assert.equal(parameters.parameters.dnsNameForPublicIP.value.length, 24, 56 | 'In ./test/assets/dokku-vm/azuredeploy.parameters.gen_unique_var.json Expected parameters.parameters.dnsNameForPublicIP.length to be 24.'); 57 | assert.equal(parameters.parameters.adminUsername.value.length, 8, 58 | 'In ./test/assets/dokku-vm/azuredeploy.parameters.gen_unique_var.json Expected parameters.parameters.adminUsername.length to be 8.'); 59 | assert.equal(parameters.parameters.newStorageAccountName.value.length, 8, 60 | 'In ./test/assets/dokku-vm/azuredeploy.parameters.gen_unique_var.json Expected parameters.parameters.newStorageAccountName.length to be 8.'); 61 | 62 | parameterString = JSON.stringify(parameters); 63 | 64 | // check all placeholders are gone 65 | assert.equal(parameterString.match(new RegExp(placeholder + '-\\d+'), null, 'Expected all ' + placeholder + '-[N] parameters to be replaced')); 66 | }); 67 | 68 | it('Should fail to parse ' + conf.get('PARAM_REPLACE_INDICATOR') + '-[N] placeholders with invalid lengths', () => { 69 | // first read the sample template 70 | var paramHelper = require('../../modules/param_helper'); 71 | var parameterString = fs.readFileSync('./test/assets/dokku-vm/azuredeploy.parameters.gen_unique_var.json', { 72 | encoding: 'utf8' 73 | }).trim(); 74 | 75 | var placeholder = conf.get('PARAM_REPLACE_INDICATOR'); 76 | 77 | assert(parameterString.match(new RegExp(placeholder + '-\\d+', 'g')).length > 0, 78 | 'In ./test/assets/dokku-vm/azuredeploy.parameters.gen_unique_var.json \ 79 | Expected to have GEN-UNIQUE placeholders'); 80 | var parameters = JSON.parse(parameterString); 81 | // inject bad parameter 82 | parameters.parameters.adminUsername = conf.get('PARAM_REPLACE_INDICATOR') + '-33'; 83 | 84 | assert.throws(() => { 85 | paramHelper.replaceKeyParameters(parameters); 86 | }); 87 | // inject bad parameter 88 | parameters.parameters.adminUsername = conf.get('PARAM_REPLACE_INDICATOR') + '-1'; 89 | 90 | assert.throws(() => { 91 | paramHelper.replaceKeyParameters(parameters); 92 | }); 93 | 94 | // inject bad parameter 95 | parameters.parameters.adminUsername = conf.get('PARAM_REPLACE_INDICATOR') + '-2'; 96 | 97 | assert.throws(() => { 98 | paramHelper.replaceKeyParameters(parameters); 99 | }); 100 | 101 | // inject good parameter 102 | parameters.parameters.adminUsername = conf.get('PARAM_REPLACE_INDICATOR') + '-12'; 103 | 104 | assert.doesNotThrow(() => { 105 | paramHelper.replaceKeyParameters(parameters); 106 | }); 107 | }); 108 | 109 | }); 110 | -------------------------------------------------------------------------------- /az-group-deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | while getopts "a:l:g:s:f:e:uv" opt; do 3 | case $opt in 4 | a) 5 | artifactsStagingDirectory=$OPTARG #the folder or sample to deploy 6 | ;; 7 | l) 8 | location=$OPTARG #location for the deployed resource group 9 | ;; 10 | g) 11 | resourceGroupName=$OPTARG 12 | ;; 13 | u) 14 | uploadArtifacts='true' #set this switch to upload/stage artifacs 15 | ;; 16 | s) 17 | storageAccountName=$OPTARG #storage account to use for staging, if not supplied one will be created and reused 18 | ;; 19 | f) 20 | templateFile=$OPTARG 21 | ;; 22 | e) 23 | parametersFile=$OPTARG 24 | ;; 25 | v) 26 | validateOnly='true' 27 | ;; 28 | esac 29 | done 30 | 31 | [[ $# -eq 0 || -z $artifactsStagingDirectory || -z $location ]] && { echo "Usage: $0 <-a foldername> <-l location> [-e parameters-file] [-g resource-group-name] [-u] [-s storageAccountName] [-v]"; exit 1; } 32 | 33 | if [[ -z $templateFile ]] 34 | then 35 | templateFile="$artifactsStagingDirectory/azuredeploy.json" 36 | fi 37 | if [[ -z $parametersFile ]] 38 | then 39 | parametersFile="$artifactsStagingDirectory/azuredeploy.parameters.json" 40 | fi 41 | 42 | templateName="$( basename "${templateFile%.*}" )" 43 | templateDirectory="$( dirname "$templateFile")" 44 | 45 | if [[ -z $resourceGroupName ]] 46 | then 47 | resourceGroupName=${artifactsStagingDirectory} 48 | fi 49 | 50 | parameterJson=$( cat "$parametersFile" | jq '.parameters' ) 51 | 52 | if [[ $uploadArtifacts ]] 53 | then 54 | 55 | if [[ -z $storageAccountName ]] 56 | then 57 | 58 | subscriptionId=$( az account show -o json | jq -r '.id' ) 59 | subscriptionId="${subscriptionId//-/}" 60 | subscriptionId="${subscriptionId:0:19}" 61 | artifactsStorageAccountName="stage$subscriptionId" 62 | artifactsResourceGroupName="ARM_Deploy_Staging" 63 | 64 | if [[ -z $( az storage account list -o json | jq -r '.[].name | select(. == '\"$artifactsStorageAccountName\"')' ) ]] 65 | then 66 | az group create -n "$artifactsResourceGroupName" -l "$location" 67 | az storage account create -l "$location" --sku "Standard_LRS" -g "$artifactsResourceGroupName" -n "$artifactsStorageAccountName" 2>/dev/null 68 | fi 69 | else 70 | artifactsResourceGroupName=$( az storage account list -o json | jq -r '.[] | select(.name == '\"$s\"') .resourceGroup' ) 71 | if [[ -z $artifactsResourceGroupName ]] 72 | then 73 | echo "Cannot find storageAccount: "$storageAccountName 74 | fi 75 | fi 76 | 77 | artifactsStorageContainerName=${resourceGroupName}"-stageartifacts" 78 | artifactsStorageContainerName=$( echo "$artifactsStorageContainerName" | awk '{print tolower($0)}') 79 | 80 | artifactsStorageAccountKey=$( az storage account keys list -g "$artifactsResourceGroupName" -n "$artifactsStorageAccountName" -o json | jq -r '.[0].value' ) 81 | az storage container create -n "$artifactsStorageContainerName" --account-name "$artifactsStorageAccountName" --account-key "$artifactsStorageAccountKey" >/dev/null 2>&1 82 | 83 | # Get a 4-hour SAS Token for the artifacts container. Fall back to OSX date syntax if Linux syntax fails. 84 | plusFourHoursUtc=$(date -u -v+4H +%Y-%m-%dT%H:%MZ 2>/dev/null) || plusFourHoursUtc=$(date -u --date "$dte 4 hour" +%Y-%m-%dT%H:%MZ) 85 | 86 | sasToken=$( az storage container generate-sas -n "$artifactsStorageContainerName" --permissions r --expiry "$plusFourHoursUtc" --account-name "$artifactsStorageAccountName" --account-key "$artifactsStorageAccountKey" -o json | sed 's/"//g') 87 | 88 | blobEndpoint=$( az storage account show -n "$artifactsStorageAccountName" -g "$artifactsResourceGroupName" -o json | jq -r '.primaryEndpoints.blob' ) 89 | 90 | parameterJson=$( echo "$parameterJson" | jq "{_artifactsLocation: {value: "\"$blobEndpoint$artifactsStorageContainerName"\"}, _artifactsLocationSasToken: {value: \"?"$sasToken"\"}} + ." ) 91 | 92 | artifactsStagingDirectory=$( echo "$artifactsStagingDirectory" | sed 's/\/*$//') 93 | artifactsStagingDirectoryLen=$((${#artifactsStagingDirectory} + 1)) 94 | 95 | for filepath in $( find "$artifactsStagingDirectory" -type f ) 96 | do 97 | relFilePath=${filepath:$artifactsStagingDirectoryLen} 98 | echo "Uploading file $relFilePath..." 99 | az storage blob upload -f $filepath --container $artifactsStorageContainerName -n $relFilePath --account-name "$artifactsStorageAccountName" --account-key "$artifactsStorageAccountKey" --verbose 100 | done 101 | 102 | templateUri=$blobEndpoint$artifactsStorageContainerName/$(basename $templateFile)?$sasToken 103 | 104 | fi 105 | 106 | az group create -n "$resourceGroupName" -l "$location" 107 | 108 | # Remove line endings from parameter JSON so it can be passed in to the CLI as a single line 109 | parameterJson=$( echo "$parameterJson" | jq -c '.' ) 110 | 111 | if [[ $validateOnly ]] 112 | then 113 | if [[ $uploadArtifacts ]] 114 | then 115 | az group deployment validate -g "$resourceGroupName" --template-uri $templateUri --parameters "$parameterJson" --verbose 116 | else 117 | az group deployment validate -g "$resourceGroupName" --template-file $templateFile --parameters "$parameterJson" --verbose 118 | fi 119 | else 120 | if [[ $uploadArtifacts ]] 121 | then 122 | az group deployment create -g "$resourceGroupName" -n AzureRMSamples --template-uri $templateUri --parameters "$parameterJson" --verbose 123 | else 124 | az group deployment create -g "$resourceGroupName" -n AzureRMSamples --template-file $templateFile --parameters "$parameterJson" --verbose 125 | fi 126 | fi 127 | -------------------------------------------------------------------------------- /deployment-template/azuredeploy.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "location": { 6 | "type": "string", 7 | "defaultValue": "centralus", 8 | "metadata": { 9 | "description": "Location for the resources, default to westus2 to match the VM size requested." 10 | } 11 | }, 12 | "adminUsername": { 13 | "type": "string", 14 | "metadata": { 15 | "description": "User name for the Virtual Machine." 16 | } 17 | }, 18 | "adminPassword": { 19 | "type": "securestring", 20 | "metadata": { 21 | "description": "Password for the Virtual Machine." 22 | } 23 | }, 24 | "publicIpAddressName": { 25 | "type": "string", 26 | "metadata": { 27 | "description": "Name of the PublicIP to assign to the server." 28 | } 29 | }, 30 | "publicIpAddressResourceGroup": { 31 | "type": "string", 32 | "metadata": { 33 | "description": "Resource group name of the PublicIP address." 34 | } 35 | }, 36 | "_artifactsLocation": { 37 | "type": "string", 38 | "metadata": { 39 | "description": "The base URI where artifacts required by this template are located. When the template is deployed using the accompanying scripts, a private location in the subscription will be used and this value will be automatically generated." 40 | } 41 | }, 42 | "_artifactsLocationSasToken": { 43 | "type": "securestring", 44 | "metadata": { 45 | "description": "The sasToken required to access _artifactsLocation. When the template is deployed using the accompanying scripts, a sasToken will be automatically generated." 46 | }, 47 | "defaultValue": "" 48 | } 49 | }, 50 | "variables": { 51 | "nicName": "myVMNic", 52 | "subnetName": "Subnet", 53 | "vmName": "[resourceGroup().name]", 54 | "virtualNetworkName": "MyVNET", 55 | "subnetRef": "[resourceId('Microsoft.Network/virtualNetworks/subnets/', variables('virtualNetworkName'), variables('subnetName'))]", 56 | "nsgName": "nsg" 57 | }, 58 | "resources": [ 59 | { 60 | "apiVersion": "2015-06-15", 61 | "type": "Microsoft.Network/networkSecurityGroups", 62 | "name": "[variables('nsgName')]", 63 | "location": "[parameters('location')]", 64 | "properties": { 65 | "securityRules": [ 66 | { 67 | "name": "ssh-rule", 68 | "properties": { 69 | "description": "Allow SSH", 70 | "protocol": "Tcp", 71 | "sourcePortRange": "*", 72 | "destinationPortRange": "22", 73 | "sourceAddressPrefix": "Internet", 74 | "destinationAddressPrefix": "*", 75 | "access": "Allow", 76 | "priority": 1000, 77 | "direction": "Inbound" 78 | } 79 | }, 80 | { 81 | "name": "bot-rule", 82 | "properties": { 83 | "description": "Allow connections from travis ci", 84 | "protocol": "Tcp", 85 | "sourcePortRange": "*", 86 | "destinationPortRange": "3000", 87 | "sourceAddressPrefix": "Internet", 88 | "destinationAddressPrefix": "*", 89 | "access": "Allow", 90 | "priority": 100, 91 | "direction": "Inbound" 92 | } 93 | } 94 | ] 95 | } 96 | }, 97 | { 98 | "apiVersion": "2017-09-01", 99 | "type": "Microsoft.Network/virtualNetworks", 100 | "name": "[variables('virtualNetworkName')]", 101 | "location": "[parameters('location')]", 102 | "dependsOn": [ 103 | "[variables('nsgName')]" 104 | ], 105 | "properties": { 106 | "addressSpace": { 107 | "addressPrefixes": [ 108 | "10.0.0.0/16" 109 | ] 110 | }, 111 | "subnets": [ 112 | { 113 | "name": "[variables('subnetName')]", 114 | "properties": { 115 | "addressPrefix": "10.0.0.0/24", 116 | "networkSecurityGroup": { 117 | "id": "[resourceId('Microsoft.Network/networkSecurityGroups', variables('nsgName'))]" 118 | } 119 | } 120 | } 121 | ] 122 | } 123 | }, 124 | { 125 | "apiVersion": "2017-09-01", 126 | "type": "Microsoft.Network/networkInterfaces", 127 | "name": "[variables('nicName')]", 128 | "location": "[parameters('location')]", 129 | "dependsOn": [ 130 | "[variables('virtualNetworkName')]" 131 | ], 132 | "properties": { 133 | "ipConfigurations": [ 134 | { 135 | "name": "ipconfig1", 136 | "properties": { 137 | "privateIPAllocationMethod": "Dynamic", 138 | "publicIPAddress": { 139 | "id": "[resourceId(parameters('publicIpAddressResourceGroup'), 'Microsoft.Network/publicIPAddresses', parameters('publicIpAddressName'))]" 140 | }, 141 | "subnet": { 142 | "id": "[variables('subnetRef')]" 143 | } 144 | } 145 | } 146 | ] 147 | } 148 | }, 149 | { 150 | "apiVersion": "2017-03-30", 151 | "type": "Microsoft.Compute/virtualMachines", 152 | "name": "[variables('vmName')]", 153 | "location": "[parameters('location')]", 154 | "dependsOn": [ 155 | "[variables('nicName')]" 156 | ], 157 | "properties": { 158 | "hardwareProfile": { 159 | "vmSize": "Standard_DS2_v2" 160 | }, 161 | "osProfile": { 162 | "computerName": "[variables('vmName')]", 163 | "adminUsername": "[parameters('adminUsername')]", 164 | "adminPassword": "[parameters('adminPassword')]" 165 | }, 166 | "storageProfile": { 167 | "imageReference": { 168 | "publisher": "Canonical", 169 | "offer": "UbuntuServer", 170 | "sku": "16.04.0-LTS", 171 | "version": "latest" 172 | }, 173 | "osDisk": { 174 | "createOption": "FromImage", 175 | "diskSizeGB": 512 176 | } 177 | }, 178 | "networkProfile": { 179 | "networkInterfaces": [ 180 | { 181 | "id": "[resourceId('Microsoft.Network/networkInterfaces',variables('nicName'))]" 182 | } 183 | ] 184 | } 185 | }, 186 | "resources": [ 187 | { 188 | "type": "extensions", 189 | "name": "configure-bot", 190 | "apiVersion": "2017-03-30", 191 | "location": "[parameters('location')]", 192 | "dependsOn": [ 193 | "[variables('vmName')]" 194 | ], 195 | "properties": { 196 | "publisher": "Microsoft.Azure.Extensions", 197 | "type": "CustomScript", 198 | "typeHandlerVersion": "2.0", 199 | "autoUpgradeMinorVersion": true, 200 | "settings": { 201 | "fileUris": [ 202 | "[uri(parameters('_artifactsLocation'), concat('configure-bot.sh', parameters('_artifactsLocationSasToken')))]" 203 | ] 204 | }, 205 | "protectedSettings": { 206 | "commandToExecute": "[concat('sh configure-bot.sh ', parameters('adminUsername'), ' ', uri(parameters('_artifactsLocation'), '.'), ' \"', parameters('_artifactsLocationSasToken'), '\"')]" 207 | } 208 | } 209 | } 210 | ] 211 | } 212 | ], 213 | "outputs": { 214 | "hostname": { 215 | "type": "string", 216 | "value": "[reference(resourceId(parameters('publicIpAddressResourceGroup'), 'Microsoft.Network/publicIPAddresses', parameters('publicIpAddressName')), '2017-09-01').dnsSettings.fqdn]" 217 | }, 218 | "sshCommand": { 219 | "type": "string", 220 | "value": "[concat('ssh ', parameters('adminUsername'), '@', reference(resourceId(parameters('publicIpAddressResourceGroup'), 'Microsoft.Network/publicIPAddresses', parameters('publicIpAddressName')), '2017-09-01').dnsSettings.fqdn)]" 221 | } 222 | } 223 | } -------------------------------------------------------------------------------- /routes/validate.js: -------------------------------------------------------------------------------- 1 | var express = require('express'), 2 | router = express.Router(), 3 | path = require('path'), 4 | azureTools = require('../modules/azure'), 5 | paramHelper = require('../modules/param_helper'), 6 | Guid = require('guid'), 7 | fs = require('fs'), 8 | conf = require('../modules/config'), 9 | RSVP = require('rsvp'), 10 | githubHelper = require('../modules/github_helper'), 11 | DelayedResponse = require('http-delayed-response'), 12 | tempDir = 'temp'; 13 | 14 | var debug = require('debug')('arm-validator:server'); 15 | var parallelDeployLimit = parseInt(conf.get('PARALLEL_DEPLOY_LIMIT') || 20); 16 | debug('parallelDeployLimit: ' + parallelDeployLimit); 17 | var parallelDeploys = 0; 18 | 19 | function writeFileHelper(fs, fileName, parametersFileName, template, parameters) { 20 | var writeFile = RSVP.denodeify(fs.writeFile); 21 | return writeFile.call(fs, fileName, JSON.stringify(template, null, '\t')) 22 | .then(function () { 23 | return writeFile.call(fs, parametersFileName, JSON.stringify(parameters, null, '\t')); 24 | }); 25 | } 26 | 27 | // replaces https://raw.githubusercontent.com links to upstream:master to the downstream repo 28 | function replaceRawLinksForPR(template, prNumber) { 29 | var templateString = JSON.stringify(template); 30 | // we make the assumption all links target a source on master 31 | var replaceTarget = 'https://' + path.join('raw.githubusercontent.com/', conf.get('GITHUB_REPO'), '/master'); 32 | debug('replaceTarget: ' + replaceTarget); 33 | return githubHelper.getPullRequestBaseLink(prNumber) 34 | .then(link => { 35 | // replace something like 'https://raw.githubusercontent.com/azure/azure-quickstart-templates/master' 36 | // with 'https://raw.githubusercontent.com/user/azure-quickstart-templates/sourcebranch' 37 | return JSON.parse(templateString.replace(new RegExp(replaceTarget, 'g'), link)); 38 | }); 39 | } 40 | 41 | // replaces 42 | function replaceSpecialParameterPlaceholders(req) { 43 | req.body.parameters = paramHelper.replaceKeyParameters(req.body.parameters); 44 | if (req.body.preReqParameters) { 45 | req.body.preReqParameters = paramHelper.replaceKeyParameters(req.body.preReqParameters); 46 | } 47 | } 48 | router.post('/validate', function (req, res) { 49 | 50 | var fileName = tempDir + '/' + Guid.raw(), 51 | parametersFileName = tempDir + '/' + Guid.raw(), 52 | promise = new RSVP.Promise((resolve) => { 53 | resolve(); 54 | }), 55 | preReqFileName, 56 | preReqParametersFileName; 57 | 58 | replaceSpecialParameterPlaceholders(req); 59 | 60 | debug('pull request number: ' + req.body.pull_request); 61 | if (req.body.pull_request) { 62 | promise = promise 63 | .then(() => { 64 | return replaceRawLinksForPR(req.body.template, req.body.pull_request); 65 | }) 66 | .then((modifiedTemplate) => { 67 | debug('modified template is:'); 68 | debug(modifiedTemplate); 69 | req.body.template = modifiedTemplate; 70 | }); 71 | } 72 | 73 | if (req.body.preReqTemplate) { 74 | promise = promise.then(() => { 75 | preReqFileName = tempDir + '/' + Guid.raw(); 76 | preReqParametersFileName = tempDir + '/' + Guid.raw(); 77 | return writeFileHelper(fs, preReqFileName, preReqParametersFileName, req.body.preReqTemplate, req.body.preReqParameters); 78 | }); 79 | } 80 | 81 | promise.then(() => { 82 | writeFileHelper(fs, fileName, parametersFileName, req.body.template, req.body.parameters) 83 | .then(function() { 84 | if (preReqFileName) { 85 | return azureTools.validateTemplateWithPreReq(fileName, parametersFileName, preReqFileName, preReqParametersFileName); 86 | } else if (req.body.template_link) { 87 | return azureTools.validateTemplate(null, parametersFileName, req.body.template_link); 88 | } else { 89 | return azureTools.validateTemplate(fileName, parametersFileName); 90 | } 91 | }) 92 | .then(function () { 93 | return res.send({ 94 | result: 'Template Valid' 95 | }); 96 | }) 97 | .catch(function (err) { 98 | return res.status(400).send({ 99 | error: err.toString() 100 | }); 101 | }) 102 | .finally(function () { 103 | if (fs.existsSync(fileName)) { 104 | fs.unlink(fileName); 105 | } 106 | 107 | if (fs.existsSync(parametersFileName)) { 108 | fs.unlink(parametersFileName); 109 | } 110 | 111 | if (fs.existsSync(preReqFileName)) { 112 | fs.unlink(preReqFileName); 113 | } 114 | 115 | if (fs.existsSync(preReqParametersFileName)) { 116 | fs.unlink(preReqParametersFileName); 117 | } 118 | }); 119 | }); 120 | }); 121 | 122 | router.post('/deploy', function (req, res) { 123 | 124 | var fileName = tempDir + '/' + Guid.raw(), 125 | rgName = conf.get('RESOURCE_GROUP_NAME_PREFIX') + Guid.raw(), 126 | parametersFileName = tempDir + '/' + Guid.raw(), 127 | preReqFileName, 128 | preReqParametersFileName; 129 | 130 | if (parallelDeploys >= parallelDeployLimit) { 131 | return res.status(403).send({ 132 | error: 'Server has hit its parallel deployment limit. Try again later' 133 | }); 134 | } 135 | 136 | var delayed = new DelayedResponse(req, res); 137 | // shortcut for res.setHeader('Content-Type', 'application/json') 138 | delayed.json(); 139 | replaceSpecialParameterPlaceholders(req); 140 | delayed.start(); 141 | var promise = new RSVP.Promise((resolve) => { 142 | resolve(); 143 | }); 144 | 145 | debug('pull request number: ' + req.body.pull_request); 146 | if (req.body.pull_request) { 147 | promise = promise 148 | .then(() => { 149 | return replaceRawLinksForPR(req.body.template, req.body.pull_request); 150 | }) 151 | .then((modifiedTemplate) => { 152 | debug('modified template is:'); 153 | debug(modifiedTemplate); 154 | req.body.template = modifiedTemplate; 155 | }); 156 | } 157 | 158 | if (req.body.preReqTemplate) { 159 | promise = promise.then(() => { 160 | preReqFileName = tempDir + '/' + Guid.raw(); 161 | preReqParametersFileName = tempDir + '/' + Guid.raw(); 162 | return writeFileHelper(fs, preReqFileName, preReqParametersFileName, req.body.preReqTemplate, req.body.preReqParameters); 163 | }); 164 | } 165 | 166 | 167 | promise.then(() => { 168 | return writeFileHelper(fs, fileName, parametersFileName, req.body.template, req.body.parameters); 169 | }) 170 | .then(function () { 171 | debug('deploying template: '); 172 | debug(JSON.stringify(req.body.template, null, '\t')); 173 | debug('with paremeters: '); 174 | debug(JSON.stringify(req.body.parameters, null, '\t')); 175 | parallelDeploys += 1; 176 | debug('parallel deploy count: ' + parallelDeploys); 177 | if (preReqFileName) { 178 | return azureTools.testTemplateWithPreReq(rgName, fileName, parametersFileName, preReqFileName, preReqParametersFileName); 179 | } else if (req.body.template_link) { 180 | return azureTools.testTemplate(rgName, null, parametersFileName, req.body.template_link); 181 | } else { 182 | return azureTools.testTemplate(rgName, fileName, parametersFileName); 183 | } 184 | }) 185 | .then(function () { 186 | debug('Deployment Successful'); 187 | // stop sending long poll bytes 188 | delayed.stop(); 189 | return res.end(JSON.stringify({ 190 | result: 'Deployment Successful' 191 | })); 192 | }) 193 | .catch(function (err) { 194 | debug(err); 195 | debug('Deployment not Sucessful'); 196 | // stop sending long poll bytes 197 | delayed.stop(); 198 | return res.end(JSON.stringify({ 199 | error: err.toString(), 200 | _rgName: rgName, 201 | command: 'azure group deployment create --resource-group (your_group_name) --template-file azuredeploy.json --parameters-file azuredeploy.parameters.json', 202 | parameters: req.body.parameters, 203 | template: req.body.template 204 | })); 205 | }) 206 | .finally(function () { 207 | parallelDeploys -= 1; 208 | fs.unlink(fileName); 209 | fs.unlink(parametersFileName); 210 | 211 | if (fs.existsSync(preReqFileName)) { 212 | fs.unlink(preReqFileName); 213 | } 214 | 215 | if (fs.existsSync(preReqParametersFileName)) { 216 | fs.unlink(preReqParametersFileName); 217 | } 218 | 219 | azureTools.deleteGroup(rgName) 220 | .then(() => { 221 | debug('Sucessfully cleaned up resource group: ' + rgName); 222 | }) 223 | .catch((err) => { 224 | console.error('failed to delete resource group: ' + rgName); 225 | console.error(err); 226 | }); 227 | }); 228 | }); 229 | 230 | module.exports = router; 231 | -------------------------------------------------------------------------------- /modules/azure.js: -------------------------------------------------------------------------------- 1 | var scriptycli2 = require('azure-scripty-cli2'), 2 | conf = require('./config'), 3 | RSVP = require('rsvp'), 4 | fs = require('fs'), 5 | debug = require('debug')('arm-validator:azure'), 6 | mongoHelper = require('./mongo_helper'); 7 | 8 | var invoke = RSVP.denodeify(scriptycli2.invoke); 9 | 10 | exports.login = function () { 11 | var cmd = { 12 | command: 'login --service-principal', 13 | username: conf.get('AZURE_CLIENT_ID'), 14 | password: conf.get('AZURE_CLIENT_SECRET'), 15 | tenant: conf.get('AZURE_TENANT_ID') 16 | }; 17 | return invoke.call(scriptycli2, cmd) 18 | }; 19 | 20 | exports.validateTemplate = function (templateFile, parametersFile, templateLink) { 21 | var cmd; 22 | /* if (templateFile) { 23 | cmd = { 24 | command: 'group deployment validate', 25 | 'resource-group': conf.get('TEST_RESOURCE_GROUP_NAME'), 26 | 'template-file': templateFile, 27 | 'parameters': parametersFile 28 | }; 29 | } else if (templateLink) { 30 | cmd = { 31 | command: 'group deployment validate', 32 | 'resource-group': conf.get('TEST_RESOURCE_GROUP_NAME'), 33 | 'template-uri': templateLink, 34 | 'parameters': parametersFile 35 | }; 36 | } 37 | debug('DEBUG: using template file:'); 38 | debug(templateFile); 39 | debug('using paramters:'); 40 | debug(parametersFile); 41 | */ 42 | return; //invoke.call(scriptycli2, cmd); 43 | }; 44 | 45 | exports.validateTemplateWithPreReq = function (templateFile, parametersFile, preReqTemplateFile, preReqParametersFile) { 46 | 47 | /* 48 | var cmd = { 49 | command: 'group deployment validate', 50 | 'resource-group': conf.get('TEST_RESOURCE_GROUP_NAME'), 51 | 'template-file': preReqTemplateFile 52 | }; 53 | 54 | var preReqParamContent = JSON.parse(fs.readFileSync(preReqParametersFile)); 55 | for (var key in preReqParamContent.parameters) { 56 | cmd['parameters'] = preReqParametersFile; 57 | break; 58 | } 59 | 60 | return invoke.call(scriptycli2, cmd) 61 | .then(() => { 62 | var cmd = { 63 | command: 'group deployment validate', 64 | 'resource-group': conf.get('TEST_RESOURCE_GROUP_NAME'), 65 | 'template-file': templateFile 66 | }; 67 | 68 | var parametersFileContent = JSON.parse(fs.readFileSync(parametersFile)); 69 | for (var key in parametersFileContent.parameters) { 70 | cmd['parameters'] = parametersFile; 71 | break; 72 | } 73 | 74 | // now deploy! 75 | return; // "Skipping Validation"; //invoke.call(scriptycli2, cmd); 76 | }); 77 | */ 78 | return; 79 | } 80 | 81 | 82 | function createGroup(groupName) { 83 | debug('creating resource group: ' + groupName + ' in region ' + conf.get('AZURE_REGION')); 84 | var cmd = { 85 | command: 'group create', 86 | 'name': groupName, 87 | 'location': conf.get('AZURE_REGION') 88 | 89 | }; 90 | return invoke.call(scriptycli2, cmd); 91 | } 92 | 93 | exports.deleteExistingGroups = function () { 94 | return mongoHelper.connect() 95 | .then(db => { 96 | var resourceGroups = db.collection('resourceGroups'); 97 | var find = RSVP.denodeify(resourceGroups.find); 98 | return find.call(resourceGroups, {}); 99 | }) 100 | .then(results => { 101 | var promises = []; 102 | results.forEach(result => { 103 | var promise = exports.deleteGroup(result.name); 104 | promises.push(promise); 105 | }); 106 | 107 | return RSVP.all(promises); 108 | }); 109 | }; 110 | 111 | exports.deleteGroup = function (groupName) { 112 | var cmd = { 113 | command: 'group delete', 114 | 'name': groupName, 115 | 'yes': '-y', 116 | }; 117 | // first, remove tracking entry in db 118 | return mongoHelper.connect() 119 | .then(db => { 120 | debug('deleting resource group: ' + groupName); 121 | var resourceGroups = db.collection('resourceGroups'); 122 | var deleteOne = RSVP.denodeify(resourceGroups.deleteOne); 123 | return deleteOne.call(resourceGroups, { 124 | name: groupName 125 | }); 126 | }) 127 | .then(() => invoke.call(scriptycli2, cmd)) 128 | .then(() => debug('sucessfully deleted resource group: ' + groupName)); 129 | }; 130 | 131 | exports.testTemplate = function (rgName, templateFile, parametersFile, templateLink) { 132 | debug('DEBUG: using template file:'); 133 | debug(templateFile); 134 | debug('using paramters:'); 135 | debug(parametersFile); 136 | debug('Deploying to RG: ' + rgName); 137 | 138 | return mongoHelper.connect() 139 | .then(db => { 140 | var resourceGroups = db.collection('resourceGroups'); 141 | var insert = RSVP.denodeify(resourceGroups.insert); 142 | return insert.call(resourceGroups, { 143 | name: rgName, 144 | region: rgName 145 | }); 146 | }) 147 | .then(result => { 148 | debug('sucessfully inserted ' + result.ops.length + ' resource group to collection'); 149 | return createGroup(rgName); 150 | }) 151 | .then(() => { 152 | debug('sucessfully created resource group ' + rgName); 153 | var cmd; 154 | if (templateLink) { 155 | cmd = { 156 | command: 'group deployment create', 157 | 'resource-group': rgName, 158 | 'template-uri': templateLink 159 | }; 160 | } else { 161 | cmd = { 162 | command: 'group deployment create', 163 | 'resource-group': rgName, 164 | 'template-file': templateFile 165 | }; 166 | } 167 | 168 | var parametersFileContent = JSON.parse(fs.readFileSync(parametersFile)); 169 | for (var key in parametersFileContent.parameters) { 170 | cmd['parameters'] = parametersFile; 171 | break; 172 | } 173 | 174 | // now deploy! 175 | return invoke.call(scriptycli2, cmd); 176 | }); 177 | }; 178 | 179 | exports.testTemplateWithPreReq = function (rgName, templateFile, parametersFile, preReqTemplateFile, preReqParametersFile) { 180 | debug('DEBUG: using prereq template file:'); 181 | debug(preReqTemplateFile); 182 | debug('using prereq template parameters:'); 183 | debug(preReqParametersFile); 184 | debug('DEBUG: using template file:'); 185 | debug(templateFile); 186 | debug('using paramters:'); 187 | debug(parametersFile); 188 | debug('Deploying to RG: ' + rgName); 189 | 190 | return mongoHelper.connect() 191 | .then(db => { 192 | var resourceGroups = db.collection('resourceGroups'); 193 | var insert = RSVP.denodeify(resourceGroups.insert); 194 | return insert.call(resourceGroups, { 195 | name: rgName, 196 | region: rgName 197 | }); 198 | }) 199 | .then(result => { 200 | debug('sucessfully inserted ' + result.ops.length + ' resource group to collection'); 201 | return createGroup(rgName); 202 | }) 203 | .then(() => { 204 | debug('sucessfully created resource group ' + rgName); 205 | 206 | var cmd = { 207 | command: 'group deployment create', 208 | 'resource-group': rgName, 209 | 'template-file': preReqTemplateFile 210 | }; 211 | 212 | var preReqParamContent = JSON.parse(fs.readFileSync(preReqParametersFile)); 213 | for (var key in preReqParamContent.parameters) { 214 | cmd['parameters'] = preReqParametersFile; 215 | break; 216 | } 217 | 218 | // now deploy! 219 | return invoke.call(scriptycli2, cmd); 220 | }) 221 | .then((response) => { 222 | debug('sucessfully deployed prereq resources'); 223 | 224 | // Handle string replacement based pre-req mapping 225 | var parametersString = fs.readFileSync(parametersFile, 'utf8'); 226 | for (var key in response.properties.outputs) { 227 | debug('key: ' + key); 228 | debug('Value: ' + response.properties.outputs[key].value); 229 | var keyValue = response.properties.outputs[key].value; 230 | parametersString = parametersString.replace(new RegExp('GET-PREREQ-' + key, 'g'), keyValue); 231 | } 232 | 233 | ////// Handle dynamic pre-req mapping based on name mapping 234 | ////var parametersObject = JSON.parse(parametersString); 235 | ////for (var key in response.properties.outputs) { 236 | //// if (parametersObject.parameters[key]) { 237 | //// parametersObject.parameters[key].value = response.properties.outputs[key].value; 238 | //// } 239 | ////} 240 | ////parametersString = JSON.stringify(parametersObject); 241 | 242 | fs.writeFileSync(parametersFile, parametersString, 'utf8'); 243 | 244 | var cmd = { 245 | command: 'group deployment create', 246 | 'resource-group': rgName, 247 | 'template-file': templateFile 248 | }; 249 | 250 | var parametersFileContent = JSON.parse(fs.readFileSync(parametersFile)); 251 | for (var key in parametersFileContent.parameters) { 252 | cmd['parameters'] = parametersFile; 253 | break; 254 | } 255 | 256 | // now deploy! 257 | return invoke.call(scriptycli2, cmd); 258 | }); 259 | }; 260 | 261 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # azure-arm-validator 2 | 3 | [![Build Status](https://travis-ci.org/Azure/azure-arm-validator.svg?branch=master)](https://travis-ci.org/Azure/azure-arm-validator) 4 | [![Code Climate](https://codeclimate.com/github/sedouard/azure-arm-validator/badges/gpa.svg)](https://codeclimate.com/github/sedouard/azure-arm-validator) 5 | 6 | A tiny server which will validate Azure Resource Manager scripts. 7 | 8 | # What does it Do? 9 | 10 | You probably won't need this server. It's a glorified wrapper around the [azure-cli2](https://docs.microsoft.com/en-us/cli/azure/install-azure-cli?view=azure-cli-latest) but is used by the [Azure Quickstart Templates](https://github.com/azure/azure-quickstart-templates) project for automated template deployments. 11 | 12 | # Pre-requisite 13 | - You must have Azure CLI V2 installed before deploying this server. 14 | 15 | ## Endpoints 16 | 17 | The server has two simple endpoints `/validate` and `/deploy`. 18 | 19 | ### POST - /validate 20 | 21 | Body: 22 | 23 | ```json 24 | { 25 | "template": { 26 | }, 27 | "parameters": { 28 | }, 29 | "pull_request": 44 30 | } 31 | ``` 32 | 33 | Response: 34 | 35 | Upon success you will recieve: 36 | 37 | Status: 200 38 | ```json 39 | { "result": "Template Valid" } 40 | ``` 41 | 42 | Upon failure you will recieve: 43 | 44 | ```json 45 | { "result": "[azure cli error message]", "command": "[the cli command used]", "template": "[the exact template file contents used]", "parameters": "[the exact template paramters provided]"} 46 | ``` 47 | 48 | ### POST - /deploy 49 | 50 | The deploy endpoint interface is the same as `/validate` except it will deploy the template. It will keep the HTTP connection as long as required to deploy the template, but will always respond with a 202 'Accepted' status, regardless of the outcome. It is the responsibility of the client to inspect the response body for sucess, or failure. 51 | 52 | Body: 53 | 54 | ```json 55 | { 56 | "template": { 57 | }, 58 | "parameters": { 59 | }, 60 | "pull_request": 44 61 | } 62 | ``` 63 | 64 | Response: 65 | 66 | Upon success you will recieve: 67 | 68 | Status: 202 69 | ```json 70 | { "result": "Deployment Successful" } 71 | ``` 72 | 73 | Upon failure you will recieve: 74 | 75 | ```json 76 | { "result": "[azure cli error message]", "command": "[the cli command used]", "template": "[the exact template file contents used]", "parameters": "[the exact template parameters provided]"} 77 | ``` 78 | 79 | ## Parameters File Special Values 80 | 81 | The server by default configuration will replace several special value type fields in the parameters file: 82 | 83 | - `GEN-UNIQUE` - Replaces this with a generated unique value suitable for domain names, storage accounts and usernames. The length of the generated paramter will be 18 characters long. 84 | - `GEN-UNIQUE-[N]` - Replaces this with a generated unique value suitable for domain names, sotrage accounts and usernames. The length is specified by `[N]` where it can be any number between 3 to 32 inclusive. For example, `GEN_UNIQUE_22`. 85 | - `GEN-PASSWORD` - Replaces this with a generated azure-compatible password, useful for virtual machine names. 86 | - `GEN-SSH-PUB-KEY` - Replaces this with a generated SSH public key 87 | 88 | You can pre-create few azure components which can be used by templates for automated validation. This includes a key vault with sample SSL certificate stored, specialized and generalized Windows Server VHD's, a custom domain and SSL cert data for Azure App Service templates. 89 | 90 | **Key Vault Related placeholders:** 91 | + **GEN-KEYVAULT-NAME** - use this placeholder to leverage an existing test keyvault in your templates 92 | + **GEN-KEYVAULT-FQDN-URI** - use this placeholder to get FQDN URI of existing test keyvault. 93 | + **GEN-KEYVAULT-RESOURCE-ID** - use this placeholder to get Resource ID of existing test keyvault. 94 | + **GEN-KEYVAULT-SSL-SECRET-NAME** - use this placeholder to use the sample SSL cert stored in the test keyvault 95 | + **GEN-KEYVAULT-SSL-SECRET-URI** - use this placeholder to use the sample SSL cert stored in the test keyvault 96 | 97 | ** Existing VHD related placeholders:** 98 | + **GEN-SPECIALIZED-WINVHD-URI** - URI of a specialized Windows VHD stored in an existing storage account. 99 | + **GEN-GENERALIZED-WINVHD-URI** - URI of a generalized Windows VHD stored in an existing storage account. 100 | + **GEN-DATAVHD-URI** - URI of a sample data disk VHD stored in an existing storage account. 101 | + **GEN-VHDSTORAGEACCOUNT-NAME** - Name of storage account in which the VHD's are stored. 102 | + **GEN-VHDRESOURCEGROUP-NAME** - Name of resource group in which the existing storage account having VHD's resides. 103 | 104 | ** Custom Domain & SSL Cert related placeholders:** 105 | + **GEN-CUSTOMFQDN-WEBAPP-NAME** - Placeholder for the name of azure app service where you'd want to attach custom domain. 106 | + **GEN-CUSTOM-FQDN-NAME** - Sample Custom domain which can be added to App Service created above. 107 | + **GEN-CUSTOM-DOMAIN-SSLCERT-THUMBPRINT** - SSL cert thumbpring for the custom domain used in above placeholder 108 | + **GEN-CUSTOM-DOMAIN-SSLCERT-PASSWORD** - Password of the SSL certificate used in above placeholder. 109 | 110 | In a typical `azuredeploy.parameters.json` your template file would look like: 111 | 112 | ```json 113 | { 114 | "$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#", 115 | "contentVersion": "1.0.0.0", 116 | "parameters": { 117 | "newStorageAccountName":{ 118 | "value": "GEN_UNIQUE" 119 | }, 120 | "location": { 121 | "value": "West US" 122 | }, 123 | "adminUsername": { 124 | "value": "sedouard" 125 | }, 126 | "sshKeyData": { 127 | "value": "GEN_SSH_PUB_KEY" 128 | }, 129 | "dnsNameForPublicIP": { 130 | "value": "GEN_UNIQUE" 131 | } 132 | } 133 | } 134 | 135 | ``` 136 | # Running the Server 137 | 138 | There are a couple things to do before you can run the server. 139 | 140 | ## Configuration 141 | 142 | A configuration file example is provided at [`./.example-config.json`](./.example-config.json): 143 | 144 | ```json 145 | { 146 | "comment": "You can either set these values as environment variables or to a file called '.config.json' at the root of the repo", 147 | "AZURE_CLIENT_ID": "00000000-0000-0000-0000-000000000000", 148 | "AZURE_TENANT_ID": "00000000-0000-0000-0000-000000000000", 149 | "AZURE_CLIENT_SECRET": "00000000-0000-0000-0000-000000000000", 150 | "AZURE_REGION": "westus", 151 | "TEST_RESOURCE_GROUP_NAME": "azure_test_rg", 152 | "RESOURCE_GROUP_NAME_PREFIX": "qstci-", 153 | "MONGO_URL": "mongodb://localhost:27017/arm-validator", 154 | "PARAM_REPLACE_INDICATOR": "GEN_UNIQUE", 155 | "SSH_KEY_REPLACE_INDICATOR": "GEN_SSH_PUB_KEY", 156 | "SSH_PUBLIC_KEY": "ssh-rsa create an ssh public key using ssh-keygen", 157 | "PASSWORD_REPLACE_INDICATOR": "GEN_PASSWORD", 158 | "GITHUB_REPO": "Azure/azure-quickstart-templates" 159 | } 160 | ``` 161 | 162 | You can set any of these values as environment variables, or placing it in a file called `.config.json` at the root of this repo making it easy to deploy. 163 | 164 | ## Mongodb 165 | 166 | The server uses MongoDB to record created resource groups to persistenence. On restart, it will attempt to delete any resource group listed in the database. This helps ensure the unlikely possibility of a resource group hanging around after a template deployment. Be sure to set `MONGO_URL` to an accessible MongoDB instance. 167 | 168 | ## Service Prinicpal 169 | 170 | You'll also need to [setup a service principal](https://github.com/cloudfoundry-incubator/bosh-azure-cpi-release/blob/master/docs/create-service-principal.md) for the server to access your azure subscription. 171 | 172 | # Deploying 173 | There is a template that will deploy an instance of the server. The template is designed to be deployed between an "a" and "b" cluster for staging new releases. The template expects the publicIP addresses to be existing and will attach those to the VM's nic when deployed. 174 | 175 | ### .config.json 176 | You can add a "production" version of .config.json into the deployment-template folder. The file will be ignored by git and if present when deployed, the script will install the config file and start the service. If the config file is not found in staging, it must be installed manually and the service configured and started after deployment. See service-start.sh if needed. 177 | 178 | To deploy between alternate resource groups you can use the command: 179 | 180 | ```PowerShell 181 | .\Deploy-AzureResourceGroup.ps1 -ArtifactStagingDirectory .\deployment-template -ResourceGroupLocation westus2 -UploadArtifacts -TemplateParametersFile .\deployment-template\azuredeploy.parameters.b.json -ResourceGroupName template-bot-b 182 | ``` 183 | 184 | ```bash 185 | az-group-deploy -a deployment-template -l westus2 -u -e ./deployment-template/azuredeploy.parameters.b.json -g template-bot-b 186 | ``` 187 | 188 | Alternate between "a" and "b" with the parameters and resource group name. 189 | 190 | 191 | # Contributing 192 | 193 | See the [contributing guide](./CONTRIBUTING.md) file for details on how to contribute. 194 | 195 | 196 | -------------------------------------------------------------------------------- /test/assets/dokku-vm/azuredeploy.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "newStorageAccountName": { 6 | "type": "string", 7 | "metadata": { 8 | "description": "A Unique Name for the Storage Account where the Virtual Machine's disks will be placed." 9 | } 10 | }, 11 | "adminUsername": { 12 | "type": "string", 13 | "metadata": { 14 | "description": "User name for the Virtual Machine." 15 | } 16 | }, 17 | "sshKeyData": { 18 | "type": "string", 19 | "metadata": { 20 | "description": "The SSH public key data for the administrator account as a string." 21 | } 22 | }, 23 | "dnsNameForPublicIP": { 24 | "type": "string", 25 | "metadata": { 26 | "description": "A Unique sub-domain for the Public IP used to access the Virtual Machine. Your full domain name will be: dnsNameForPublicIP.location.cloudapp.azure.com" 27 | } 28 | }, 29 | "ubuntuOSVersion": { 30 | "type": "string", 31 | "defaultValue": "14.04.2-LTS", 32 | "allowedValues": [ 33 | "12.04.5-LTS", 34 | "14.04.2-LTS", 35 | "15.04" 36 | ], 37 | "metadata": { 38 | "description": "The Ubuntu version for the VM. This will pick a fully patched image of this given Ubuntu version. Allowed values: 12.04.5-LTS, 14.04.2-LTS, 15.04." 39 | } 40 | }, 41 | "vmSize": { 42 | "type": "string", 43 | "defaultValue": "Standard_D1", 44 | "allowedValues": [ 45 | "Standard_A1", 46 | "Standard_A2", 47 | "Standard_A3", 48 | "Standard_A4", 49 | "Standard_D1", 50 | "Standard_D2", 51 | "Standard_D3", 52 | "Standard_D4" 53 | ], 54 | "metadata": { 55 | "description": "Size of vm" 56 | } 57 | }, 58 | "location": { 59 | "type": "string", 60 | "metadata": { 61 | "description": "Region where the resources will be deployed" 62 | }, 63 | "allowedValues": [ 64 | "East US", 65 | "East US 2", 66 | "East Asia", 67 | "West US", 68 | "West Europe", 69 | "Southeast Asia", 70 | "South Central US", 71 | "East US 2", 72 | "Japan East", 73 | "Japan West", 74 | "Central US" 75 | ] 76 | } 77 | }, 78 | "variables": { 79 | "imagePublisher": "Canonical", 80 | "imageOffer": "UbuntuServer", 81 | "OSDiskName": "osdiskforlinuxsimple", 82 | "nicName": "myVMNic", 83 | "addressPrefix": "10.0.0.0/16", 84 | "subnetName": "Subnet", 85 | "subnetPrefix": "10.0.0.0/24", 86 | "storageAccountType": "Standard_LRS", 87 | "publicIPAddressName": "myPublicIP", 88 | "publicIPAddressType": "Dynamic", 89 | "vmStorageAccountContainerName": "vhds", 90 | "vmName": "DokkuVM", 91 | "virtualNetworkName": "DokkuVNet", 92 | "vnetID": "[resourceId('Microsoft.Network/virtualNetworks',variables('virtualNetworkName'))]", 93 | "subnetRef": "[concat(variables('vnetID'),'/subnets/',variables('subnetName'))]", 94 | "api-version": "2015-06-15", 95 | "sshKeyPath": "[concat('/home/',parameters('adminUsername'),'/.ssh/authorized_keys')]", 96 | "githubPath": "https://raw.githubusercontent.com/azure/azure-quickstart-templates/master/dokku-vm/" 97 | }, 98 | "resources": [ 99 | { 100 | "type": "Microsoft.Storage/storageAccounts", 101 | "name": "[parameters('newStorageAccountName')]", 102 | "apiVersion": "[variables('api-version')]", 103 | "location": "[parameters('location')]", 104 | "properties": { 105 | "accountType": "[variables('storageAccountType')]" 106 | } 107 | }, 108 | { 109 | "apiVersion": "[variables('api-version')]", 110 | "type": "Microsoft.Network/publicIPAddresses", 111 | "name": "[variables('publicIPAddressName')]", 112 | "location": "[parameters('location')]", 113 | "properties": { 114 | "publicIPAllocationMethod": "[variables('publicIPAddressType')]", 115 | "dnsSettings": { 116 | "domainNameLabel": "[parameters('dnsNameForPublicIP')]" 117 | } 118 | } 119 | }, 120 | { 121 | "apiVersion": "[variables('api-version')]", 122 | "type": "Microsoft.Network/virtualNetworks", 123 | "name": "[variables('virtualNetworkName')]", 124 | "location": "[parameters('location')]", 125 | "properties": { 126 | "addressSpace": { 127 | "addressPrefixes": [ 128 | "[variables('addressPrefix')]" 129 | ] 130 | }, 131 | "subnets": [ 132 | { 133 | "name": "[variables('subnetName')]", 134 | "properties": { 135 | "addressPrefix": "[variables('subnetPrefix')]" 136 | } 137 | } 138 | ] 139 | } 140 | }, 141 | { 142 | "apiVersion": "[variables('api-version')]", 143 | "type": "Microsoft.Network/networkInterfaces", 144 | "name": "[variables('nicName')]", 145 | "location": "[parameters('location')]", 146 | "dependsOn": [ 147 | "[concat('Microsoft.Network/publicIPAddresses/', variables('publicIPAddressName'))]", 148 | "[concat('Microsoft.Network/virtualNetworks/', variables('virtualNetworkName'))]" 149 | ], 150 | "properties": { 151 | "ipConfigurations": [ 152 | { 153 | "name": "ipconfig1", 154 | "properties": { 155 | "privateIPAllocationMethod": "Dynamic", 156 | "publicIPAddress": { 157 | "id": "[resourceId('Microsoft.Network/publicIPAddresses',variables('publicIPAddressName'))]" 158 | }, 159 | "subnet": { 160 | "id": "[variables('subnetRef')]" 161 | } 162 | } 163 | } 164 | ] 165 | } 166 | }, 167 | { 168 | "apiVersion": "[variables('api-version')]", 169 | "type": "Microsoft.Compute/virtualMachines", 170 | "name": "[variables('vmName')]", 171 | "location": "[parameters('location')]", 172 | "dependsOn": [ 173 | "[concat('Microsoft.Storage/storageAccounts/', parameters('newStorageAccountName'))]", 174 | "[concat('Microsoft.Network/networkInterfaces/', variables('nicName'))]" 175 | ], 176 | "properties": { 177 | "hardwareProfile": { 178 | "vmSize": "[parameters('vmSize')]" 179 | }, 180 | "osProfile": { 181 | "computerName": "[variables('vmName')]", 182 | "adminUsername": "[parameters('adminUsername')]", 183 | "linuxConfiguration": { 184 | "disablePasswordAuthentication": "true", 185 | "ssh": { 186 | "publicKeys": [ 187 | { 188 | "path": "[variables('sshKeyPath')]", 189 | "keyData": "[parameters('sshKeyData')]" 190 | } 191 | ] 192 | } 193 | } 194 | }, 195 | "storageProfile": { 196 | "imageReference": { 197 | "publisher": "[variables('imagePublisher')]", 198 | "offer": "[variables('imageOffer')]", 199 | "sku": "[parameters('ubuntuOSVersion')]", 200 | "version": "latest" 201 | }, 202 | "osDisk": { 203 | "name": "osdisk", 204 | "vhd": { 205 | "uri": "[concat('http://',parameters('newStorageAccountName'),'.blob.core.windows.net/',variables('vmStorageAccountContainerName'),'/',variables('OSDiskName'),'.vhd')]" 206 | }, 207 | "caching": "ReadWrite", 208 | "createOption": "FromImage" 209 | } 210 | }, 211 | "networkProfile": { 212 | "networkInterfaces": [ 213 | { 214 | "id": "[resourceId('Microsoft.Network/networkInterfaces',variables('nicName'))]" 215 | } 216 | ] 217 | }, 218 | "diagnosticsProfile": { 219 | "bootDiagnostics": { 220 | "enabled": "true", 221 | "storageUri": "[concat('http://',parameters('newStorageAccountName'),'.blob.core.windows.net')]" 222 | } 223 | } 224 | } 225 | }, 226 | { 227 | "type": "Microsoft.Compute/virtualMachines/extensions", 228 | "name": "[concat(variables('vmName'),'/initdokku')]", 229 | "apiVersion": "[variables('api-version')]", 230 | "location": "[parameters('location')]", 231 | "dependsOn": [ 232 | "[concat('Microsoft.Compute/virtualMachines/', variables('vmName'))]" 233 | ], 234 | "properties": { 235 | "publisher": "Microsoft.OSTCExtensions", 236 | "type": "CustomScriptForLinux", 237 | "typeHandlerVersion": "1.2", 238 | "settings": { 239 | "fileUris": [ 240 | "[concat(variables('githubPath'),'deploy_dokku.sh')]" 241 | ], 242 | "commandToExecute": "[concat('sh deploy_dokku.sh')]" 243 | } 244 | } 245 | } 246 | ] 247 | } 248 | -------------------------------------------------------------------------------- /Deploy-AzureResourceGroup.ps1: -------------------------------------------------------------------------------- 1 | #Requires -Version 3.0 2 | #Requires -Module AzureRM.Resources 3 | #Requires -Module Azure.Storage 4 | #Requires -Module @{ModuleName="AzureRm.Profile";ModuleVersion="3.0"} 5 | 6 | 7 | Param( 8 | [string] [Parameter(Mandatory=$true)] $ArtifactStagingDirectory, 9 | [string] [Parameter(Mandatory=$true)] $ResourceGroupLocation, 10 | [string] $ResourceGroupName = (Split-Path $ArtifactStagingDirectory -Leaf), 11 | [switch] $UploadArtifacts, 12 | [string] $StorageAccountName, 13 | [string] $StorageContainerName = $ResourceGroupName.ToLowerInvariant() + '-stageartifacts', 14 | [string] $TemplateFile = $ArtifactStagingDirectory + '\mainTemplate.json', 15 | [string] $TemplateParametersFile = $ArtifactStagingDirectory + '.\azuredeploy.parameters.json', 16 | [string] $DSCSourceFolder = $ArtifactStagingDirectory + '.\DSC', 17 | [switch] $ValidateOnly, 18 | [string] $DebugOptions = "None", 19 | [switch] $Dev 20 | ) 21 | 22 | try { 23 | [Microsoft.Azure.Common.Authentication.AzureSession]::ClientFactory.AddUserAgent("AzureQuickStarts-$UI$($host.name)".replace(" ","_"), "1.0") 24 | } catch { } 25 | 26 | $ErrorActionPreference = 'Stop' 27 | Set-StrictMode -Version 3 28 | 29 | function Format-ValidationOutput { 30 | param ($ValidationOutput, [int] $Depth = 0) 31 | Set-StrictMode -Off 32 | return @($ValidationOutput | Where-Object { $_ -ne $null } | ForEach-Object { @(' ' * $Depth + ': ' + $_.Message) + @(Format-ValidationOutput @($_.Details) ($Depth + 1)) }) 33 | } 34 | 35 | $OptionalParameters = New-Object -TypeName Hashtable 36 | $TemplateArgs = New-Object -TypeName Hashtable 37 | 38 | # if the template file isn't found, try the another default 39 | if(!(Test-Path $TemplateFile)) { 40 | $TemplateFile = $ArtifactStagingDirectory + '\azuredeploy.json' 41 | } 42 | 43 | #try a few different default options for param files when the -dev switch is use 44 | if ($Dev) { 45 | $TemplateParametersFile = $TemplateParametersFile.Replace('azuredeploy.parameters.json', 'azuredeploy.parameters.dev.json') 46 | if (!(Test-Path $TemplateParametersFile)) { 47 | $TemplateParametersFile = $TemplateParametersFile.Replace('azuredeploy.parameters.dev.json', 'azuredeploy.parameters.1.json') 48 | } 49 | } 50 | 51 | Write-Host "Using parameter file: $TemplateParametersFile" 52 | 53 | if (!$ValidateOnly) { 54 | $OptionalParameters.Add('DeploymentDebugLogLevel', $DebugOptions) 55 | } 56 | 57 | $TemplateFile = [System.IO.Path]::GetFullPath([System.IO.Path]::Combine($PSScriptRoot, $TemplateFile)) 58 | $TemplateParametersFile = [System.IO.Path]::GetFullPath([System.IO.Path]::Combine($PSScriptRoot, $TemplateParametersFile)) 59 | 60 | $TemplateJSON = Get-Content $TemplateFile -Raw | ConvertFrom-Json 61 | 62 | $ArtifactsLocationParameter = $TemplateJson | Select-Object -expand 'parameters' -ErrorAction Ignore | Select-Object -Expand '_artifactsLocation' -ErrorAction Ignore 63 | 64 | #if the switch is set or the standard parameter is present in the template, upload all artifacts 65 | if ($UploadArtifacts -Or $ArtifactsLocationParameter -ne $null) { 66 | # Convert relative paths to absolute paths if needed 67 | $ArtifactStagingDirectory = [System.IO.Path]::GetFullPath([System.IO.Path]::Combine($PSScriptRoot, $ArtifactStagingDirectory)) 68 | $DSCSourceFolder = [System.IO.Path]::GetFullPath([System.IO.Path]::Combine($PSScriptRoot, $DSCSourceFolder)) 69 | 70 | # Parse the parameter file and update the values of artifacts location and artifacts location SAS token if they are present 71 | $JsonParameters = Get-Content $TemplateParametersFile -Raw | ConvertFrom-Json 72 | if (($JsonParameters | Get-Member -Type NoteProperty 'parameters') -ne $null) { 73 | $JsonParameters = $JsonParameters.parameters 74 | } 75 | $ArtifactsLocationName = '_artifactsLocation' 76 | $ArtifactsLocationSasTokenName = '_artifactsLocationSasToken' 77 | $OptionalParameters[$ArtifactsLocationName] = $JsonParameters | Select-Object -Expand $ArtifactsLocationName -ErrorAction Ignore | Select-Object -Expand 'value' -ErrorAction Ignore 78 | $OptionalParameters[$ArtifactsLocationSasTokenName] = $JsonParameters | Select-Object -Expand $ArtifactsLocationSasTokenName -ErrorAction Ignore | Select-Object -Expand 'value' -ErrorAction Ignore 79 | 80 | # Create DSC configuration archive 81 | if (Test-Path $DSCSourceFolder) { 82 | $DSCSourceFilePaths = @(Get-ChildItem $DSCSourceFolder -File -Filter '*.ps1' | ForEach-Object -Process {$_.FullName}) 83 | foreach ($DSCSourceFilePath in $DSCSourceFilePaths) { 84 | $DSCArchiveFilePath = $DSCSourceFilePath.Substring(0, $DSCSourceFilePath.Length - 4) + '.zip' 85 | Publish-AzureRmVMDscConfiguration $DSCSourceFilePath -OutputArchivePath $DSCArchiveFilePath -Force -Verbose 86 | } 87 | } 88 | 89 | # Create a storage account name if none was provided 90 | if ($StorageAccountName -eq '') { 91 | $StorageAccountName = 'stage' + ((Get-AzureRmContext).Subscription.Id).Replace('-', '').substring(0, 19) 92 | } 93 | 94 | $StorageAccount = (Get-AzureRmStorageAccount | Where-Object{$_.StorageAccountName -eq $StorageAccountName}) 95 | 96 | # Create the storage account if it doesn't already exist 97 | if ($StorageAccount -eq $null) { 98 | $StorageResourceGroupName = 'ARM_Deploy_Staging' 99 | New-AzureRmResourceGroup -Location "$ResourceGroupLocation" -Name $StorageResourceGroupName -Force 100 | $StorageAccount = New-AzureRmStorageAccount -StorageAccountName $StorageAccountName -Type 'Standard_LRS' -ResourceGroupName $StorageResourceGroupName -Location "$ResourceGroupLocation" 101 | } 102 | 103 | $ArtifactStagingLocation = $StorageAccount.Context.BlobEndPoint + $StorageContainerName + "/" 104 | 105 | # Generate the value for artifacts location if it is not provided in the parameter file 106 | if ($OptionalParameters[$ArtifactsLocationName] -eq $null) { 107 | #if the defaultValue for _artifactsLocation is using the template location, use the defaultValue, otherwise set it to the staging location 108 | $defaultValue = $ArtifactsLocationParameter | Select-Object -Expand 'defaultValue' -ErrorAction Ignore 109 | if($defaultValue -like '*deployment().properties.templateLink.uri*') { 110 | $OptionalParameters.Remove($ArtifactsLocationName) 111 | } 112 | else { 113 | $OptionalParameters[$ArtifactsLocationName] = $ArtifactStagingLocation 114 | } 115 | } 116 | 117 | # Copy files from the local storage staging location to the storage account container 118 | New-AzureStorageContainer -Name $StorageContainerName -Context $StorageAccount.Context -ErrorAction SilentlyContinue *>&1 119 | 120 | $ArtifactFilePaths = Get-ChildItem $ArtifactStagingDirectory -Recurse -File | ForEach-Object -Process {$_.FullName} 121 | foreach ($SourcePath in $ArtifactFilePaths) { 122 | 123 | if ($SourcePath -like "$DSCSourceFolder*" -and $SourcePath -like "*.zip" -or !($SourcePath -like "$DSCSourceFolder*")) { #When using DSC, just copy the DSC archive, not all the modules and source files 124 | Set-AzureStorageBlobContent -File $SourcePath -Blob $SourcePath.Substring($ArtifactStagingDirectory.length + 1) -Container $StorageContainerName -Context $StorageAccount.Context -Force 125 | #Write-host $SourcePath 126 | } 127 | } 128 | # Generate a 4 hour SAS token for the artifacts location if one was not provided in the parameters file 129 | if ($OptionalParameters[$ArtifactsLocationSasTokenName] -eq $null) { 130 | $OptionalParameters[$ArtifactsLocationSasTokenName] = (New-AzureStorageContainerSASToken -Container $StorageContainerName -Context $StorageAccount.Context -Permission r -ExpiryTime (Get-Date).AddHours(4)) 131 | } 132 | 133 | $TemplateArgs.Add('TemplateFile', $ArtifactStagingLocation + (Get-ChildItem $TemplateFile).Name + $OptionalParameters[$ArtifactsLocationSasTokenName]) 134 | 135 | $OptionalParameters[$ArtifactsLocationSasTokenName] = ConvertTo-SecureString $OptionalParameters[$ArtifactsLocationSasTokenName] -AsPlainText -Force 136 | 137 | } 138 | else { 139 | 140 | $TemplateArgs.Add('TemplateFile', $TemplateFile) 141 | 142 | } 143 | 144 | $TemplateArgs.Add('TemplateParameterFile', $TemplateParametersFile) 145 | 146 | # Create the resource group only when it doesn't already exist 147 | if ((Get-AzureRmresourcegroup -Name $ResourceGroupName -Location $ResourceGroupLocation -Verbose -ErrorAction SilentlyContinue) -eq $null) { 148 | New-AzureRmResourceGroup -Name $ResourceGroupName -Location $ResourceGroupLocation -Verbose -Force -ErrorAction Stop 149 | } 150 | 151 | if ($ValidateOnly) { 152 | $ErrorMessages = Format-ValidationOutput (Test-AzureRmResourceGroupDeployment -ResourceGroupName $ResourceGroupName ` 153 | @TemplateArgs ` 154 | @OptionalParameters) 155 | if ($ErrorMessages) { 156 | Write-Output '', 'Validation returned the following errors:', @($ErrorMessages), '', 'Template is invalid.' 157 | } 158 | else { 159 | Write-Output '', 'Template is valid.' 160 | } 161 | } 162 | else { 163 | New-AzureRmResourceGroupDeployment -Name ((Get-ChildItem $TemplateFile).BaseName + '-' + ((Get-Date).ToUniversalTime()).ToString('MMdd-HHmm')) ` 164 | -ResourceGroupName $ResourceGroupName ` 165 | @TemplateArgs ` 166 | @OptionalParameters ` 167 | -Force -Verbose ` 168 | -ErrorVariable ErrorMessages 169 | if ($ErrorMessages) { 170 | Write-Output '', 'Template deployment returned the following errors:', @(@($ErrorMessages) | ForEach-Object { $_.Exception.Message.TrimEnd("`r`n") }) 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /test/assets/githubresponse.json: -------------------------------------------------------------------------------- 1 | { 2 | "url": "https://api.github.com/repos/singhkay/testtemplate/pulls/44", 3 | "id": 51266781, 4 | "html_url": "https://github.com/singhkay/testtemplate/pull/44", 5 | "diff_url": "https://github.com/singhkay/testtemplate/pull/44.diff", 6 | "patch_url": "https://github.com/singhkay/testtemplate/pull/44.patch", 7 | "issue_url": "https://api.github.com/repos/singhkay/testtemplate/issues/44", 8 | "number": 44, 9 | "state": "closed", 10 | "locked": false, 11 | "title": "Test what TRAVIS_BUILD value is", 12 | "user": { 13 | "login": "sedouard", 14 | "id": 3053263, 15 | "avatar_url": "https://avatars.githubusercontent.com/u/3053263?v=3", 16 | "gravatar_id": "", 17 | "url": "https://api.github.com/users/sedouard", 18 | "html_url": "https://github.com/sedouard", 19 | "followers_url": "https://api.github.com/users/sedouard/followers", 20 | "following_url": "https://api.github.com/users/sedouard/following{/other_user}", 21 | "gists_url": "https://api.github.com/users/sedouard/gists{/gist_id}", 22 | "starred_url": "https://api.github.com/users/sedouard/starred{/owner}{/repo}", 23 | "subscriptions_url": "https://api.github.com/users/sedouard/subscriptions", 24 | "organizations_url": "https://api.github.com/users/sedouard/orgs", 25 | "repos_url": "https://api.github.com/users/sedouard/repos", 26 | "events_url": "https://api.github.com/users/sedouard/events{/privacy}", 27 | "received_events_url": "https://api.github.com/users/sedouard/received_events", 28 | "type": "User", 29 | "site_admin": false 30 | }, 31 | "body": null, 32 | "created_at": "2015-11-19T19:13:00Z", 33 | "updated_at": "2015-11-19T21:07:21Z", 34 | "closed_at": "2015-11-19T21:07:21Z", 35 | "merged_at": null, 36 | "merge_commit_sha": "3a1afebd0446cfbcd505dd39c6ae161d5b96b964", 37 | "assignee": null, 38 | "milestone": null, 39 | "commits_url": "https://api.github.com/repos/singhkay/testtemplate/pulls/44/commits", 40 | "review_comments_url": "https://api.github.com/repos/singhkay/testtemplate/pulls/44/comments", 41 | "review_comment_url": "https://api.github.com/repos/singhkay/testtemplate/pulls/comments{/number}", 42 | "comments_url": "https://api.github.com/repos/singhkay/testtemplate/issues/44/comments", 43 | "statuses_url": "https://api.github.com/repos/singhkay/testtemplate/statuses/15c87cc7e9187f8c3e8a02a2fe607890e8d8331d", 44 | "head": { 45 | "label": "sedouard:test_branch_name", 46 | "ref": "test_branch_name", 47 | "sha": "15c87cc7e9187f8c3e8a02a2fe607890e8d8331d", 48 | "user": { 49 | "login": "sedouard", 50 | "id": 3053263, 51 | "avatar_url": "https://avatars.githubusercontent.com/u/3053263?v=3", 52 | "gravatar_id": "", 53 | "url": "https://api.github.com/users/sedouard", 54 | "html_url": "https://github.com/sedouard", 55 | "followers_url": "https://api.github.com/users/sedouard/followers", 56 | "following_url": "https://api.github.com/users/sedouard/following{/other_user}", 57 | "gists_url": "https://api.github.com/users/sedouard/gists{/gist_id}", 58 | "starred_url": "https://api.github.com/users/sedouard/starred{/owner}{/repo}", 59 | "subscriptions_url": "https://api.github.com/users/sedouard/subscriptions", 60 | "organizations_url": "https://api.github.com/users/sedouard/orgs", 61 | "repos_url": "https://api.github.com/users/sedouard/repos", 62 | "events_url": "https://api.github.com/users/sedouard/events{/privacy}", 63 | "received_events_url": "https://api.github.com/users/sedouard/received_events", 64 | "type": "User", 65 | "site_admin": false 66 | }, 67 | "repo": { 68 | "id": 45638305, 69 | "name": "testtemplate", 70 | "full_name": "sedouard/testtemplate", 71 | "owner": { 72 | "login": "sedouard", 73 | "id": 3053263, 74 | "avatar_url": "https://avatars.githubusercontent.com/u/3053263?v=3", 75 | "gravatar_id": "", 76 | "url": "https://api.github.com/users/sedouard", 77 | "html_url": "https://github.com/sedouard", 78 | "followers_url": "https://api.github.com/users/sedouard/followers", 79 | "following_url": "https://api.github.com/users/sedouard/following{/other_user}", 80 | "gists_url": "https://api.github.com/users/sedouard/gists{/gist_id}", 81 | "starred_url": "https://api.github.com/users/sedouard/starred{/owner}{/repo}", 82 | "subscriptions_url": "https://api.github.com/users/sedouard/subscriptions", 83 | "organizations_url": "https://api.github.com/users/sedouard/orgs", 84 | "repos_url": "https://api.github.com/users/sedouard/repos", 85 | "events_url": "https://api.github.com/users/sedouard/events{/privacy}", 86 | "received_events_url": "https://api.github.com/users/sedouard/received_events", 87 | "type": "User", 88 | "site_admin": false 89 | }, 90 | "private": false, 91 | "html_url": "https://github.com/sedouard/testtemplate", 92 | "description": "", 93 | "fork": true, 94 | "url": "https://api.github.com/repos/sedouard/testtemplate", 95 | "forks_url": "https://api.github.com/repos/sedouard/testtemplate/forks", 96 | "keys_url": "https://api.github.com/repos/sedouard/testtemplate/keys{/key_id}", 97 | "collaborators_url": "https://api.github.com/repos/sedouard/testtemplate/collaborators{/collaborator}", 98 | "teams_url": "https://api.github.com/repos/sedouard/testtemplate/teams", 99 | "hooks_url": "https://api.github.com/repos/sedouard/testtemplate/hooks", 100 | "issue_events_url": "https://api.github.com/repos/sedouard/testtemplate/issues/events{/number}", 101 | "events_url": "https://api.github.com/repos/sedouard/testtemplate/events", 102 | "assignees_url": "https://api.github.com/repos/sedouard/testtemplate/assignees{/user}", 103 | "branches_url": "https://api.github.com/repos/sedouard/testtemplate/branches{/branch}", 104 | "tags_url": "https://api.github.com/repos/sedouard/testtemplate/tags", 105 | "blobs_url": "https://api.github.com/repos/sedouard/testtemplate/git/blobs{/sha}", 106 | "git_tags_url": "https://api.github.com/repos/sedouard/testtemplate/git/tags{/sha}", 107 | "git_refs_url": "https://api.github.com/repos/sedouard/testtemplate/git/refs{/sha}", 108 | "trees_url": "https://api.github.com/repos/sedouard/testtemplate/git/trees{/sha}", 109 | "statuses_url": "https://api.github.com/repos/sedouard/testtemplate/statuses/{sha}", 110 | "languages_url": "https://api.github.com/repos/sedouard/testtemplate/languages", 111 | "stargazers_url": "https://api.github.com/repos/sedouard/testtemplate/stargazers", 112 | "contributors_url": "https://api.github.com/repos/sedouard/testtemplate/contributors", 113 | "subscribers_url": "https://api.github.com/repos/sedouard/testtemplate/subscribers", 114 | "subscription_url": "https://api.github.com/repos/sedouard/testtemplate/subscription", 115 | "commits_url": "https://api.github.com/repos/sedouard/testtemplate/commits{/sha}", 116 | "git_commits_url": "https://api.github.com/repos/sedouard/testtemplate/git/commits{/sha}", 117 | "comments_url": "https://api.github.com/repos/sedouard/testtemplate/comments{/number}", 118 | "issue_comment_url": "https://api.github.com/repos/sedouard/testtemplate/issues/comments{/number}", 119 | "contents_url": "https://api.github.com/repos/sedouard/testtemplate/contents/{+path}", 120 | "compare_url": "https://api.github.com/repos/sedouard/testtemplate/compare/{base}...{head}", 121 | "merges_url": "https://api.github.com/repos/sedouard/testtemplate/merges", 122 | "archive_url": "https://api.github.com/repos/sedouard/testtemplate/{archive_format}{/ref}", 123 | "downloads_url": "https://api.github.com/repos/sedouard/testtemplate/downloads", 124 | "issues_url": "https://api.github.com/repos/sedouard/testtemplate/issues{/number}", 125 | "pulls_url": "https://api.github.com/repos/sedouard/testtemplate/pulls{/number}", 126 | "milestones_url": "https://api.github.com/repos/sedouard/testtemplate/milestones{/number}", 127 | "notifications_url": "https://api.github.com/repos/sedouard/testtemplate/notifications{?since,all,participating}", 128 | "labels_url": "https://api.github.com/repos/sedouard/testtemplate/labels{/name}", 129 | "releases_url": "https://api.github.com/repos/sedouard/testtemplate/releases{/id}", 130 | "created_at": "2015-11-05T20:46:26Z", 131 | "updated_at": "2015-11-05T20:46:27Z", 132 | "pushed_at": "2015-12-10T19:05:41Z", 133 | "git_url": "git://github.com/sedouard/testtemplate.git", 134 | "ssh_url": "git@github.com:sedouard/testtemplate.git", 135 | "clone_url": "https://github.com/sedouard/testtemplate.git", 136 | "svn_url": "https://github.com/sedouard/testtemplate", 137 | "homepage": null, 138 | "size": 1413, 139 | "stargazers_count": 0, 140 | "watchers_count": 0, 141 | "language": "Shell", 142 | "has_issues": false, 143 | "has_downloads": true, 144 | "has_wiki": true, 145 | "has_pages": false, 146 | "forks_count": 0, 147 | "mirror_url": null, 148 | "open_issues_count": 0, 149 | "forks": 0, 150 | "open_issues": 0, 151 | "watchers": 0, 152 | "default_branch": "master" 153 | } 154 | }, 155 | "base": { 156 | "label": "singhkay:master", 157 | "ref": "master", 158 | "sha": "626f5119711a51315ba89f0fa3507423ca2c5f27", 159 | "user": { 160 | "login": "singhkay", 161 | "id": 6164178, 162 | "avatar_url": "https://avatars.githubusercontent.com/u/6164178?v=3", 163 | "gravatar_id": "", 164 | "url": "https://api.github.com/users/singhkay", 165 | "html_url": "https://github.com/singhkay", 166 | "followers_url": "https://api.github.com/users/singhkay/followers", 167 | "following_url": "https://api.github.com/users/singhkay/following{/other_user}", 168 | "gists_url": "https://api.github.com/users/singhkay/gists{/gist_id}", 169 | "starred_url": "https://api.github.com/users/singhkay/starred{/owner}{/repo}", 170 | "subscriptions_url": "https://api.github.com/users/singhkay/subscriptions", 171 | "organizations_url": "https://api.github.com/users/singhkay/orgs", 172 | "repos_url": "https://api.github.com/users/singhkay/repos", 173 | "events_url": "https://api.github.com/users/singhkay/events{/privacy}", 174 | "received_events_url": "https://api.github.com/users/singhkay/received_events", 175 | "type": "User", 176 | "site_admin": false 177 | }, 178 | "repo": { 179 | "id": 42326530, 180 | "name": "testtemplate", 181 | "full_name": "singhkay/testtemplate", 182 | "owner": { 183 | "login": "singhkay", 184 | "id": 6164178, 185 | "avatar_url": "https://avatars.githubusercontent.com/u/6164178?v=3", 186 | "gravatar_id": "", 187 | "url": "https://api.github.com/users/singhkay", 188 | "html_url": "https://github.com/singhkay", 189 | "followers_url": "https://api.github.com/users/singhkay/followers", 190 | "following_url": "https://api.github.com/users/singhkay/following{/other_user}", 191 | "gists_url": "https://api.github.com/users/singhkay/gists{/gist_id}", 192 | "starred_url": "https://api.github.com/users/singhkay/starred{/owner}{/repo}", 193 | "subscriptions_url": "https://api.github.com/users/singhkay/subscriptions", 194 | "organizations_url": "https://api.github.com/users/singhkay/orgs", 195 | "repos_url": "https://api.github.com/users/singhkay/repos", 196 | "events_url": "https://api.github.com/users/singhkay/events{/privacy}", 197 | "received_events_url": "https://api.github.com/users/singhkay/received_events", 198 | "type": "User", 199 | "site_admin": false 200 | }, 201 | "private": false, 202 | "html_url": "https://github.com/singhkay/testtemplate", 203 | "description": "", 204 | "fork": false, 205 | "url": "https://api.github.com/repos/singhkay/testtemplate", 206 | "forks_url": "https://api.github.com/repos/singhkay/testtemplate/forks", 207 | "keys_url": "https://api.github.com/repos/singhkay/testtemplate/keys{/key_id}", 208 | "collaborators_url": "https://api.github.com/repos/singhkay/testtemplate/collaborators{/collaborator}", 209 | "teams_url": "https://api.github.com/repos/singhkay/testtemplate/teams", 210 | "hooks_url": "https://api.github.com/repos/singhkay/testtemplate/hooks", 211 | "issue_events_url": "https://api.github.com/repos/singhkay/testtemplate/issues/events{/number}", 212 | "events_url": "https://api.github.com/repos/singhkay/testtemplate/events", 213 | "assignees_url": "https://api.github.com/repos/singhkay/testtemplate/assignees{/user}", 214 | "branches_url": "https://api.github.com/repos/singhkay/testtemplate/branches{/branch}", 215 | "tags_url": "https://api.github.com/repos/singhkay/testtemplate/tags", 216 | "blobs_url": "https://api.github.com/repos/singhkay/testtemplate/git/blobs{/sha}", 217 | "git_tags_url": "https://api.github.com/repos/singhkay/testtemplate/git/tags{/sha}", 218 | "git_refs_url": "https://api.github.com/repos/singhkay/testtemplate/git/refs{/sha}", 219 | "trees_url": "https://api.github.com/repos/singhkay/testtemplate/git/trees{/sha}", 220 | "statuses_url": "https://api.github.com/repos/singhkay/testtemplate/statuses/{sha}", 221 | "languages_url": "https://api.github.com/repos/singhkay/testtemplate/languages", 222 | "stargazers_url": "https://api.github.com/repos/singhkay/testtemplate/stargazers", 223 | "contributors_url": "https://api.github.com/repos/singhkay/testtemplate/contributors", 224 | "subscribers_url": "https://api.github.com/repos/singhkay/testtemplate/subscribers", 225 | "subscription_url": "https://api.github.com/repos/singhkay/testtemplate/subscription", 226 | "commits_url": "https://api.github.com/repos/singhkay/testtemplate/commits{/sha}", 227 | "git_commits_url": "https://api.github.com/repos/singhkay/testtemplate/git/commits{/sha}", 228 | "comments_url": "https://api.github.com/repos/singhkay/testtemplate/comments{/number}", 229 | "issue_comment_url": "https://api.github.com/repos/singhkay/testtemplate/issues/comments{/number}", 230 | "contents_url": "https://api.github.com/repos/singhkay/testtemplate/contents/{+path}", 231 | "compare_url": "https://api.github.com/repos/singhkay/testtemplate/compare/{base}...{head}", 232 | "merges_url": "https://api.github.com/repos/singhkay/testtemplate/merges", 233 | "archive_url": "https://api.github.com/repos/singhkay/testtemplate/{archive_format}{/ref}", 234 | "downloads_url": "https://api.github.com/repos/singhkay/testtemplate/downloads", 235 | "issues_url": "https://api.github.com/repos/singhkay/testtemplate/issues{/number}", 236 | "pulls_url": "https://api.github.com/repos/singhkay/testtemplate/pulls{/number}", 237 | "milestones_url": "https://api.github.com/repos/singhkay/testtemplate/milestones{/number}", 238 | "notifications_url": "https://api.github.com/repos/singhkay/testtemplate/notifications{?since,all,participating}", 239 | "labels_url": "https://api.github.com/repos/singhkay/testtemplate/labels{/name}", 240 | "releases_url": "https://api.github.com/repos/singhkay/testtemplate/releases{/id}", 241 | "created_at": "2015-09-11T18:51:38Z", 242 | "updated_at": "2015-11-06T19:46:11Z", 243 | "pushed_at": "2015-12-10T19:05:41Z", 244 | "git_url": "git://github.com/singhkay/testtemplate.git", 245 | "ssh_url": "git@github.com:singhkay/testtemplate.git", 246 | "clone_url": "https://github.com/singhkay/testtemplate.git", 247 | "svn_url": "https://github.com/singhkay/testtemplate", 248 | "homepage": null, 249 | "size": 538, 250 | "stargazers_count": 0, 251 | "watchers_count": 0, 252 | "language": "JavaScript", 253 | "has_issues": true, 254 | "has_downloads": true, 255 | "has_wiki": true, 256 | "has_pages": false, 257 | "forks_count": 3, 258 | "mirror_url": null, 259 | "open_issues_count": 9, 260 | "forks": 3, 261 | "open_issues": 9, 262 | "watchers": 0, 263 | "default_branch": "master" 264 | } 265 | }, 266 | "_links": { 267 | "self": { 268 | "href": "https://api.github.com/repos/singhkay/testtemplate/pulls/44" 269 | }, 270 | "html": { 271 | "href": "https://github.com/singhkay/testtemplate/pull/44" 272 | }, 273 | "issue": { 274 | "href": "https://api.github.com/repos/singhkay/testtemplate/issues/44" 275 | }, 276 | "comments": { 277 | "href": "https://api.github.com/repos/singhkay/testtemplate/issues/44/comments" 278 | }, 279 | "review_comments": { 280 | "href": "https://api.github.com/repos/singhkay/testtemplate/pulls/44/comments" 281 | }, 282 | "review_comment": { 283 | "href": "https://api.github.com/repos/singhkay/testtemplate/pulls/comments{/number}" 284 | }, 285 | "commits": { 286 | "href": "https://api.github.com/repos/singhkay/testtemplate/pulls/44/commits" 287 | }, 288 | "statuses": { 289 | "href": "https://api.github.com/repos/singhkay/testtemplate/statuses/15c87cc7e9187f8c3e8a02a2fe607890e8d8331d" 290 | } 291 | }, 292 | "merged": false, 293 | "mergeable": true, 294 | "mergeable_state": "clean", 295 | "merged_by": null, 296 | "comments": 1, 297 | "review_comments": 0, 298 | "commits": 1, 299 | "additions": 1, 300 | "deletions": 0, 301 | "changed_files": 1 302 | } 303 | --------------------------------------------------------------------------------