├── SampleWebApp ├── wwwroot │ ├── js │ │ ├── site.min.js │ │ └── site.js │ ├── favicon.ico │ ├── lib │ │ ├── bootstrap │ │ │ ├── dist │ │ │ │ ├── fonts │ │ │ │ │ ├── glyphicons-halflings-regular.eot │ │ │ │ │ ├── glyphicons-halflings-regular.ttf │ │ │ │ │ ├── glyphicons-halflings-regular.woff │ │ │ │ │ └── glyphicons-halflings-regular.woff2 │ │ │ │ └── js │ │ │ │ │ └── npm.js │ │ │ ├── .bower.json │ │ │ └── LICENSE │ │ ├── jquery-validation-unobtrusive │ │ │ ├── .bower.json │ │ │ ├── LICENSE.txt │ │ │ ├── jquery.validate.unobtrusive.min.js │ │ │ └── jquery.validate.unobtrusive.js │ │ ├── jquery │ │ │ ├── .bower.json │ │ │ └── LICENSE.txt │ │ └── jquery-validation │ │ │ ├── .bower.json │ │ │ ├── LICENSE.md │ │ │ └── dist │ │ │ ├── additional-methods.min.js │ │ │ └── jquery.validate.min.js │ ├── css │ │ ├── site.min.css │ │ └── site.css │ └── images │ │ ├── banner2.svg │ │ ├── banner1.svg │ │ └── banner3.svg ├── Views │ ├── _ViewStart.cshtml │ ├── _ViewImports.cshtml │ ├── Home │ │ ├── Privacy.cshtml │ │ ├── About.cshtml │ │ ├── Index.cshtml │ │ └── Contact.cshtml │ └── Shared │ │ ├── Error.cshtml │ │ ├── _ValidationScriptsPartial.cshtml │ │ ├── _CookieConsentPartial.cshtml │ │ └── _Layout.cshtml ├── appsettings.json ├── obj │ ├── Debug │ │ └── netcoreapp2.1 │ │ │ └── SampleWebApp.assets.cache │ ├── SampleWebApp.csproj.nuget.cache │ ├── SampleWebApp.csproj.nuget.g.targets │ └── SampleWebApp.csproj.nuget.g.props ├── appsettings.Development.json ├── Models │ └── ErrorViewModel.cs ├── SampleWebApp.csproj ├── Program.cs ├── Properties │ └── launchSettings.json ├── Controllers │ └── HomeController.cs └── Startup.cs ├── AzureFunction-Code ├── host.json └── AzureDevOpsFunctionGate │ ├── function.json │ └── run.csx ├── .vscode ├── tasks.json └── launch.json ├── Sample-Docker └── Dockerfile ├── Sample-Docker-ARM-Templates ├── arm-template-acr.json └── arm-template-web.json ├── .gitignore ├── DevOps-Best-Practices.md ├── AzureFunction-ARM-Templates └── arm-function-template.json ├── SampleWebApp-ARM-Templates └── arm-template.json └── README.md /SampleWebApp/wwwroot/js/site.min.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /AzureFunction-Code/host.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0" 3 | } -------------------------------------------------------------------------------- /SampleWebApp/Views/_ViewStart.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | Layout = "_Layout"; 3 | } 4 | -------------------------------------------------------------------------------- /SampleWebApp/wwwroot/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AdamPaternostro/Training-Azure-DevOps/HEAD/SampleWebApp/wwwroot/favicon.ico -------------------------------------------------------------------------------- /SampleWebApp/Views/_ViewImports.cshtml: -------------------------------------------------------------------------------- 1 | @using SampleWebApp 2 | @using SampleWebApp.Models 3 | @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers 4 | -------------------------------------------------------------------------------- /SampleWebApp/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Warning" 5 | } 6 | }, 7 | "AllowedHosts": "*" 8 | } 9 | -------------------------------------------------------------------------------- /SampleWebApp/obj/Debug/netcoreapp2.1/SampleWebApp.assets.cache: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AdamPaternostro/Training-Azure-DevOps/HEAD/SampleWebApp/obj/Debug/netcoreapp2.1/SampleWebApp.assets.cache -------------------------------------------------------------------------------- /SampleWebApp/Views/Home/Privacy.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | ViewData["Title"] = "Privacy Policy"; 3 | } 4 |

@ViewData["Title"]

5 | 6 |

Use this page to detail your site's privacy policy.

7 | -------------------------------------------------------------------------------- /SampleWebApp/obj/SampleWebApp.csproj.nuget.cache: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "dgSpecHash": "4zsQ+4pXO7ONwcoxSKXeuLGt9oGYr+OkmFMz49PtzgFMe1zapQbWFPHrw97FNiYJz02t7YKBsNHVBHnRBOFFxA==", 4 | "success": true 5 | } -------------------------------------------------------------------------------- /SampleWebApp/Views/Home/About.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | ViewData["Title"] = "About"; 3 | } 4 |

@ViewData["Title"]

5 |

@ViewData["Message"]

6 | 7 |

Use this area to provide additional information.

8 | -------------------------------------------------------------------------------- /SampleWebApp/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Debug", 5 | "System": "Information", 6 | "Microsoft": "Information" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /SampleWebApp/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AdamPaternostro/Training-Azure-DevOps/HEAD/SampleWebApp/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /SampleWebApp/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AdamPaternostro/Training-Azure-DevOps/HEAD/SampleWebApp/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /SampleWebApp/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AdamPaternostro/Training-Azure-DevOps/HEAD/SampleWebApp/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /SampleWebApp/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AdamPaternostro/Training-Azure-DevOps/HEAD/SampleWebApp/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /SampleWebApp/wwwroot/js/site.js: -------------------------------------------------------------------------------- 1 | // Please see documentation at https://docs.microsoft.com/aspnet/core/client-side/bundling-and-minification 2 | // for details on configuring this project to bundle and minify static web assets. 3 | 4 | // Write your JavaScript code. 5 | -------------------------------------------------------------------------------- /SampleWebApp/Models/ErrorViewModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace SampleWebApp.Models 4 | { 5 | public class ErrorViewModel 6 | { 7 | public string RequestId { get; set; } 8 | 9 | public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); 10 | } 11 | } -------------------------------------------------------------------------------- /SampleWebApp/wwwroot/css/site.min.css: -------------------------------------------------------------------------------- 1 | body{padding-top:50px;padding-bottom:20px}.body-content{padding-left:15px;padding-right:15px}.carousel-caption p{font-size:20px;line-height:1.4}.carousel-inner .item img[src$=".svg"]{width:100%}#qrCode{margin:15px}@media screen and (max-width:767px){.carousel-caption{display:none}} -------------------------------------------------------------------------------- /SampleWebApp/Views/Home/Index.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | ViewData["Title"] = "Home Page"; 3 | } 4 | 5 |
6 |
7 |
8 | Environment: @ViewData["Environment"]
9 | ServerName: @ViewData["ServerName"] 10 |
 
11 |
12 |
13 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "build", 6 | "command": "dotnet", 7 | "type": "process", 8 | "args": [ 9 | "build", 10 | "${workspaceFolder}/SampleWebApp/SampleWebApp.csproj" 11 | ], 12 | "problemMatcher": "$msCompile" 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /Sample-Docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM busybox 2 | 3 | WORKDIR /website 4 | 5 | RUN echo '' >> /website/index.html && \ 6 | echo 'My Test Docker App' >> /website/index.html && \ 7 | echo 'Hi from Azure Dev Ops 001' >> /website/index.html && \ 8 | echo '' >> /website/index.html 9 | 10 | EXPOSE 8000 11 | 12 | CMD trap "exit 0;" TERM INT; httpd -p 8000 -h /website -f & wait -------------------------------------------------------------------------------- /SampleWebApp/SampleWebApp.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp2.1 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /AzureFunction-Code/AzureDevOpsFunctionGate/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "bindings": [ 3 | { 4 | "authLevel": "function", 5 | "name": "req", 6 | "type": "httpTrigger", 7 | "direction": "in", 8 | "methods": [ 9 | "get", 10 | "post" 11 | ] 12 | }, 13 | { 14 | "name": "$return", 15 | "type": "http", 16 | "direction": "out" 17 | } 18 | ] 19 | } -------------------------------------------------------------------------------- /SampleWebApp/Views/Home/Contact.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | ViewData["Title"] = "Contact"; 3 | } 4 |

@ViewData["Title"]

5 |

@ViewData["Message"]

6 | 7 |
8 | One Microsoft Way
9 | Redmond, WA 98052-6399
10 | P: 11 | 425.555.0100 12 |
13 | 14 |
15 | Support: Support@example.com
16 | Marketing: Marketing@example.com 17 |
18 | -------------------------------------------------------------------------------- /AzureFunction-Code/AzureDevOpsFunctionGate/run.csx: -------------------------------------------------------------------------------- 1 | #r "Newtonsoft.Json" 2 | #r "Microsoft.AspNetCore.Mvc.Formatters.Json" 3 | 4 | using System.Net; 5 | using Microsoft.AspNetCore.Mvc; 6 | using Microsoft.Extensions.Primitives; 7 | using Newtonsoft.Json; 8 | 9 | public static async Task Run(HttpRequest req, ILogger log) 10 | { 11 | log.LogInformation("C# HTTP trigger function processed a request."); 12 | 13 | var returnValue = new { status = "true"}; 14 | 15 | return new Microsoft.AspNetCore.Mvc.JsonResult(returnValue); 16 | } 17 | -------------------------------------------------------------------------------- /SampleWebApp/wwwroot/lib/bootstrap/dist/js/npm.js: -------------------------------------------------------------------------------- 1 | // This file is autogenerated via the `commonjs` Grunt task. You can require() this file in a CommonJS environment. 2 | require('../../js/transition.js') 3 | require('../../js/alert.js') 4 | require('../../js/button.js') 5 | require('../../js/carousel.js') 6 | require('../../js/collapse.js') 7 | require('../../js/dropdown.js') 8 | require('../../js/modal.js') 9 | require('../../js/tooltip.js') 10 | require('../../js/popover.js') 11 | require('../../js/scrollspy.js') 12 | require('../../js/tab.js') 13 | require('../../js/affix.js') -------------------------------------------------------------------------------- /SampleWebApp/wwwroot/lib/jquery-validation-unobtrusive/.bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jquery-validation-unobtrusive", 3 | "homepage": "https://github.com/aspnet/jquery-validation-unobtrusive", 4 | "version": "3.2.9", 5 | "_release": "3.2.9", 6 | "_resolution": { 7 | "type": "version", 8 | "tag": "v3.2.9", 9 | "commit": "a91f5401898e125f10771c5f5f0909d8c4c82396" 10 | }, 11 | "_source": "https://github.com/aspnet/jquery-validation-unobtrusive.git", 12 | "_target": "^3.2.9", 13 | "_originalSource": "jquery-validation-unobtrusive", 14 | "_direct": true 15 | } -------------------------------------------------------------------------------- /SampleWebApp/wwwroot/lib/jquery-validation-unobtrusive/LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) .NET Foundation. All rights reserved. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use 4 | these files except in compliance with the License. You may obtain a copy of the 5 | License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software distributed 10 | under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 11 | CONDITIONS OF ANY KIND, either express or implied. See the License for the 12 | specific language governing permissions and limitations under the License. 13 | -------------------------------------------------------------------------------- /SampleWebApp/wwwroot/lib/jquery/.bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jquery", 3 | "main": "dist/jquery.js", 4 | "license": "MIT", 5 | "ignore": [ 6 | "package.json" 7 | ], 8 | "keywords": [ 9 | "jquery", 10 | "javascript", 11 | "browser", 12 | "library" 13 | ], 14 | "homepage": "https://github.com/jquery/jquery-dist", 15 | "version": "3.3.1", 16 | "_release": "3.3.1", 17 | "_resolution": { 18 | "type": "version", 19 | "tag": "3.3.1", 20 | "commit": "9e8ec3d10fad04748176144f108d7355662ae75e" 21 | }, 22 | "_source": "https://github.com/jquery/jquery-dist.git", 23 | "_target": "^3.3.1", 24 | "_originalSource": "jquery", 25 | "_direct": true 26 | } -------------------------------------------------------------------------------- /SampleWebApp/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | using Microsoft.AspNetCore; 7 | using Microsoft.AspNetCore.Hosting; 8 | using Microsoft.Extensions.Configuration; 9 | using Microsoft.Extensions.Logging; 10 | 11 | namespace SampleWebApp 12 | { 13 | public class Program 14 | { 15 | public static void Main(string[] args) 16 | { 17 | CreateWebHostBuilder(args).Build().Run(); 18 | } 19 | 20 | public static IWebHostBuilder CreateWebHostBuilder(string[] args) => 21 | WebHost.CreateDefaultBuilder(args) 22 | .UseStartup(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /SampleWebApp/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:29289", 7 | "sslPort": 44348 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": true, 14 | "environmentVariables": { 15 | "ASPNETCORE_ENVIRONMENT": "Development" 16 | } 17 | }, 18 | "SampleWebApp": { 19 | "commandName": "Project", 20 | "launchBrowser": true, 21 | "applicationUrl": "https://localhost:5001;http://localhost:5000", 22 | "environmentVariables": { 23 | "ASPNETCORE_ENVIRONMENT": "Development" 24 | } 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /Sample-Docker-ARM-Templates/arm-template-acr.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "ACR_Name": { 6 | "defaultValue": "TrainingAzureDevOpsACR", 7 | "type": "String" 8 | } 9 | }, 10 | "variables": {}, 11 | "resources": [ 12 | { 13 | "type": "Microsoft.ContainerRegistry/registries", 14 | "sku": { 15 | "name": "Standard", 16 | "tier": "Standard" 17 | }, 18 | "name": "[toLower(parameters('ACR_Name'))]", 19 | "apiVersion": "2017-10-01", 20 | "location": "eastus", 21 | "tags": {}, 22 | "scale": null, 23 | "properties": { 24 | "adminUserEnabled": true 25 | }, 26 | "dependsOn": [] 27 | } 28 | ] 29 | } -------------------------------------------------------------------------------- /SampleWebApp/Views/Shared/Error.cshtml: -------------------------------------------------------------------------------- 1 | @model ErrorViewModel 2 | @{ 3 | ViewData["Title"] = "Error"; 4 | } 5 | 6 |

Error.

7 |

An error occurred while processing your request.

8 | 9 | @if (Model.ShowRequestId) 10 | { 11 |

12 | Request ID: @Model.RequestId 13 |

14 | } 15 | 16 |

Development Mode

17 |

18 | Swapping to Development environment will display more detailed information about the error that occurred. 19 |

20 |

21 | Development environment should not be enabled in deployed applications, as it can result in sensitive information from exceptions being displayed to end users. For local debugging, development environment can be enabled by setting the ASPNETCORE_ENVIRONMENT environment variable to Development, and restarting the application. 22 |

23 | -------------------------------------------------------------------------------- /SampleWebApp/wwwroot/css/site.css: -------------------------------------------------------------------------------- 1 | /* Please see documentation at https://docs.microsoft.com/aspnet/core/client-side/bundling-and-minification\ 2 | for details on configuring this project to bundle and minify static web assets. */ 3 | body { 4 | padding-top: 50px; 5 | padding-bottom: 20px; 6 | } 7 | 8 | /* Wrapping element */ 9 | /* Set some basic padding to keep content from hitting the edges */ 10 | .body-content { 11 | padding-left: 15px; 12 | padding-right: 15px; 13 | } 14 | 15 | /* Carousel */ 16 | .carousel-caption p { 17 | font-size: 20px; 18 | line-height: 1.4; 19 | } 20 | 21 | /* Make .svg files in the carousel display properly in older browsers */ 22 | .carousel-inner .item img[src$=".svg"] { 23 | width: 100%; 24 | } 25 | 26 | /* QR code generator */ 27 | #qrCode { 28 | margin: 15px; 29 | } 30 | 31 | /* Hide/rearrange for smaller screens */ 32 | @media screen and (max-width: 767px) { 33 | /* Hide captions */ 34 | .carousel-caption { 35 | display: none; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /SampleWebApp/wwwroot/lib/jquery-validation/.bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jquery-validation", 3 | "homepage": "https://jqueryvalidation.org/", 4 | "repository": { 5 | "type": "git", 6 | "url": "git://github.com/jquery-validation/jquery-validation.git" 7 | }, 8 | "authors": [ 9 | "Jörn Zaefferer " 10 | ], 11 | "description": "Form validation made easy", 12 | "main": "dist/jquery.validate.js", 13 | "keywords": [ 14 | "forms", 15 | "validation", 16 | "validate" 17 | ], 18 | "license": "MIT", 19 | "ignore": [ 20 | "**/.*", 21 | "node_modules", 22 | "bower_components", 23 | "test", 24 | "demo", 25 | "lib" 26 | ], 27 | "dependencies": { 28 | "jquery": ">= 1.7.2" 29 | }, 30 | "version": "1.17.0", 31 | "_release": "1.17.0", 32 | "_resolution": { 33 | "type": "version", 34 | "tag": "1.17.0", 35 | "commit": "fc9b12d3bfaa2d0c04605855b896edb2934c0772" 36 | }, 37 | "_source": "https://github.com/jzaefferer/jquery-validation.git", 38 | "_target": "^1.17.0", 39 | "_originalSource": "jquery-validation", 40 | "_direct": true 41 | } -------------------------------------------------------------------------------- /SampleWebApp/wwwroot/lib/bootstrap/.bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bootstrap", 3 | "description": "The most popular front-end framework for developing responsive, mobile first projects on the web.", 4 | "keywords": [ 5 | "css", 6 | "js", 7 | "less", 8 | "mobile-first", 9 | "responsive", 10 | "front-end", 11 | "framework", 12 | "web" 13 | ], 14 | "homepage": "http://getbootstrap.com", 15 | "license": "MIT", 16 | "moduleType": "globals", 17 | "main": [ 18 | "less/bootstrap.less", 19 | "dist/js/bootstrap.js" 20 | ], 21 | "ignore": [ 22 | "/.*", 23 | "_config.yml", 24 | "CNAME", 25 | "composer.json", 26 | "CONTRIBUTING.md", 27 | "docs", 28 | "js/tests", 29 | "test-infra" 30 | ], 31 | "dependencies": { 32 | "jquery": "1.9.1 - 3" 33 | }, 34 | "version": "3.3.7", 35 | "_release": "3.3.7", 36 | "_resolution": { 37 | "type": "version", 38 | "tag": "v3.3.7", 39 | "commit": "0b9c4a4007c44201dce9a6cc1a38407005c26c86" 40 | }, 41 | "_source": "https://github.com/twbs/bootstrap.git", 42 | "_target": "v3.3.7", 43 | "_originalSource": "bootstrap", 44 | "_direct": true 45 | } -------------------------------------------------------------------------------- /SampleWebApp/wwwroot/lib/bootstrap/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2011-2016 Twitter, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /SampleWebApp/wwwroot/lib/jquery-validation/LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | ===================== 3 | 4 | Copyright Jörn Zaefferer 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /SampleWebApp/Views/Shared/_ValidationScriptsPartial.cshtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 12 | 18 | 19 | -------------------------------------------------------------------------------- /SampleWebApp/Controllers/HomeController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | using Microsoft.AspNetCore.Mvc; 7 | using SampleWebApp.Models; 8 | 9 | namespace SampleWebApp.Controllers 10 | { 11 | public class HomeController : Controller 12 | { 13 | public IActionResult Index() 14 | { 15 | if (System.Environment.GetEnvironmentVariable("Environment") == null) 16 | { 17 | ViewData["Environment"] = "Environment (Environment) Not Set"; 18 | } 19 | else 20 | { 21 | ViewData["Environment"] = System.Environment.GetEnvironmentVariable("Environment"); 22 | } 23 | ViewData["ServerName"] = System.Environment.MachineName; 24 | 25 | return View(); 26 | } 27 | 28 | public IActionResult About() 29 | { 30 | ViewData["Message"] = "Your application description page."; 31 | 32 | return View(); 33 | } 34 | 35 | public IActionResult Contact() 36 | { 37 | ViewData["Message"] = "Your contact page."; 38 | 39 | return View(); 40 | } 41 | 42 | public IActionResult Privacy() 43 | { 44 | return View(); 45 | } 46 | 47 | [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] 48 | public IActionResult Error() 49 | { 50 | return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier }); 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /SampleWebApp/wwwroot/lib/jquery/LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright JS Foundation and other contributors, https://js.foundation/ 2 | 3 | This software consists of voluntary contributions made by many 4 | individuals. For exact contribution history, see the revision history 5 | available at https://github.com/jquery/jquery 6 | 7 | The following license applies to all parts of this software except as 8 | documented below: 9 | 10 | ==== 11 | 12 | Permission is hereby granted, free of charge, to any person obtaining 13 | a copy of this software and associated documentation files (the 14 | "Software"), to deal in the Software without restriction, including 15 | without limitation the rights to use, copy, modify, merge, publish, 16 | distribute, sublicense, and/or sell copies of the Software, and to 17 | permit persons to whom the Software is furnished to do so, subject to 18 | the following conditions: 19 | 20 | The above copyright notice and this permission notice shall be 21 | included in all copies or substantial portions of the Software. 22 | 23 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 24 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 25 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 26 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 27 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 28 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 29 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 30 | 31 | ==== 32 | 33 | All files located in the node_modules and external directories are 34 | externally maintained libraries used by this software which have their 35 | own licenses; we recommend you read them, as their terms may differ from 36 | the terms above. 37 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to find out which attributes exist for C# debugging 3 | // Use hover for the description of the existing attributes 4 | // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": ".NET Core Launch (web)", 9 | "type": "coreclr", 10 | "request": "launch", 11 | "preLaunchTask": "build", 12 | // If you have changed target frameworks, make sure to update the program path. 13 | "program": "${workspaceFolder}/SampleWebApp/bin/Debug/netcoreapp2.1/SampleWebApp.dll", 14 | "args": [], 15 | "cwd": "${workspaceFolder}/SampleWebApp", 16 | "stopAtEntry": false, 17 | "internalConsoleOptions": "openOnSessionStart", 18 | "launchBrowser": { 19 | "enabled": true, 20 | "args": "${auto-detect-url}", 21 | "windows": { 22 | "command": "cmd.exe", 23 | "args": "/C start ${auto-detect-url}" 24 | }, 25 | "osx": { 26 | "command": "open" 27 | }, 28 | "linux": { 29 | "command": "xdg-open" 30 | } 31 | }, 32 | "env": { 33 | "ASPNETCORE_ENVIRONMENT": "Development" 34 | }, 35 | "sourceFileMap": { 36 | "/Views": "${workspaceFolder}/Views" 37 | } 38 | }, 39 | { 40 | "name": ".NET Core Attach", 41 | "type": "coreclr", 42 | "request": "attach", 43 | "processId": "${command:pickProcess}" 44 | } 45 | ,] 46 | } -------------------------------------------------------------------------------- /SampleWebApp/Views/Shared/_CookieConsentPartial.cshtml: -------------------------------------------------------------------------------- 1 | @using Microsoft.AspNetCore.Http.Features 2 | 3 | @{ 4 | var consentFeature = Context.Features.Get(); 5 | var showBanner = !consentFeature?.CanTrack ?? false; 6 | var cookieString = consentFeature?.CreateConsentCookie(); 7 | } 8 | 9 | @if (showBanner) 10 | { 11 | 33 | 41 | } -------------------------------------------------------------------------------- /SampleWebApp/Startup.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Builder; 6 | using Microsoft.AspNetCore.Hosting; 7 | using Microsoft.AspNetCore.Http; 8 | using Microsoft.AspNetCore.HttpsPolicy; 9 | using Microsoft.AspNetCore.Mvc; 10 | using Microsoft.Extensions.Configuration; 11 | using Microsoft.Extensions.DependencyInjection; 12 | 13 | namespace SampleWebApp 14 | { 15 | public class Startup 16 | { 17 | public Startup(IConfiguration configuration) 18 | { 19 | Configuration = configuration; 20 | } 21 | 22 | public IConfiguration Configuration { get; } 23 | 24 | // This method gets called by the runtime. Use this method to add services to the container. 25 | public void ConfigureServices(IServiceCollection services) 26 | { 27 | services.Configure(options => 28 | { 29 | // This lambda determines whether user consent for non-essential cookies is needed for a given request. 30 | options.CheckConsentNeeded = context => true; 31 | options.MinimumSameSitePolicy = SameSiteMode.None; 32 | }); 33 | 34 | 35 | services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1); 36 | } 37 | 38 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 39 | public void Configure(IApplicationBuilder app, IHostingEnvironment env) 40 | { 41 | if (env.IsDevelopment()) 42 | { 43 | app.UseDeveloperExceptionPage(); 44 | } 45 | else 46 | { 47 | app.UseExceptionHandler("/Home/Error"); 48 | app.UseHsts(); 49 | } 50 | 51 | app.UseHttpsRedirection(); 52 | app.UseStaticFiles(); 53 | app.UseCookiePolicy(); 54 | 55 | app.UseMvc(routes => 56 | { 57 | routes.MapRoute( 58 | name: "default", 59 | template: "{controller=Home}/{action=Index}/{id?}"); 60 | }); 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | SampleWebApp/obj/Debug/netcoreapp2.1/Razor/Views/Shared/_ValidationScriptsPartial.g.cshtml.cs 2 | SampleWebApp/bin/Debug/netcoreapp2.1/SampleWebApp.dll 3 | SampleWebApp/bin/Debug/netcoreapp2.1/SampleWebApp.pdb 4 | SampleWebApp/bin/Debug/netcoreapp2.1/SampleWebApp.Views.dll 5 | SampleWebApp/bin/Debug/netcoreapp2.1/SampleWebApp.Views.pdb 6 | SampleWebApp/bin/Debug/netcoreapp2.1/SampleWebApp.deps.json 7 | SampleWebApp/bin/Debug/netcoreapp2.1/SampleWebApp.runtimeconfig.json 8 | SampleWebApp/bin/Debug/netcoreapp2.1/SampleWebApp.runtimeconfig.dev.json 9 | SampleWebApp/obj/Debug/netcoreapp2.1/SampleWebApp.dll 10 | SampleWebApp/obj/Debug/netcoreapp2.1/SampleWebApp.pdb 11 | SampleWebApp/obj/Debug/netcoreapp2.1/SampleWebApp.AssemblyInfo.cs 12 | SampleWebApp/obj/Debug/netcoreapp2.1/SampleWebApp.AssemblyInfoInputs.cache 13 | SampleWebApp/obj/Debug/netcoreapp2.1/SampleWebApp.RazorAssemblyInfo.cache 14 | SampleWebApp/obj/Debug/netcoreapp2.1/SampleWebApp.RazorAssemblyInfo.cs 15 | SampleWebApp/obj/Debug/netcoreapp2.1/SampleWebApp.RazorCoreGenerate.cache 16 | SampleWebApp/obj/Debug/netcoreapp2.1/SampleWebApp.RazorTargetAssemblyInfo.cache 17 | SampleWebApp/obj/Debug/netcoreapp2.1/SampleWebApp.RazorTargetAssemblyInfo.cs 18 | SampleWebApp/obj/Debug/netcoreapp2.1/SampleWebApp.TagHelpers.input.cache 19 | SampleWebApp/obj/Debug/netcoreapp2.1/SampleWebApp.TagHelpers.output.cache 20 | SampleWebApp/obj/Debug/netcoreapp2.1/SampleWebApp.Views.dll 21 | SampleWebApp/obj/Debug/netcoreapp2.1/SampleWebApp.Views.pdb 22 | SampleWebApp/obj/Debug/netcoreapp2.1/SampleWebApp.assets.cache 23 | SampleWebApp/obj/Debug/netcoreapp2.1/SampleWebApp.csproj.CopyComplete 24 | SampleWebApp/obj/Debug/netcoreapp2.1/SampleWebApp.csproj.CoreCompileInputs.cache 25 | SampleWebApp/obj/Debug/netcoreapp2.1/SampleWebApp.csproj.FileListAbsolute.txt 26 | SampleWebApp/obj/Debug/netcoreapp2.1/SampleWebApp.csprojAssemblyReference.cache 27 | SampleWebApp/obj/Debug/netcoreapp2.1/Razor/Views/_ViewImports.g.cshtml.cs 28 | SampleWebApp/obj/Debug/netcoreapp2.1/Razor/Views/_ViewStart.g.cshtml.cs 29 | SampleWebApp/obj/Debug/netcoreapp2.1/Razor/Views/Home/About.g.cshtml.cs 30 | SampleWebApp/obj/Debug/netcoreapp2.1/Razor/Views/Home/Contact.g.cshtml.cs 31 | SampleWebApp/obj/Debug/netcoreapp2.1/Razor/Views/Home/Index.g.cshtml.cs 32 | SampleWebApp/obj/Debug/netcoreapp2.1/Razor/Views/Home/Privacy.g.cshtml.cs 33 | SampleWebApp/obj/Debug/netcoreapp2.1/Razor/Views/Shared/Error.g.cshtml.cs 34 | SampleWebApp/obj/Debug/netcoreapp2.1/Razor/Views/Shared/_CookieConsentPartial.g.cshtml.cs 35 | SampleWebApp/obj/Debug/netcoreapp2.1/Razor/Views/Shared/_Layout.g.cshtml.cs 36 | SampleWebApp/obj/Debug/netcoreapp2.1/SampleWebApp.assets.cache 37 | SampleWebApp/obj/Debug/netcoreapp2.1/project.razor.json 38 | SampleWebApp/obj/Debug/netcoreapp2.1/SampleWebApp.assets.cache 39 | SampleWebApp/obj/Debug/netcoreapp2.1/SampleWebApp.assets.cache 40 | .DS_Store 41 | -------------------------------------------------------------------------------- /SampleWebApp/obj/SampleWebApp.csproj.nuget.g.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | $(MSBuildAllProjects);$(MSBuildThisFileFullPath) 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /SampleWebApp/Views/Shared/_Layout.cshtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | @ViewData["Title"] - SampleWebApp 7 | 8 | 9 | 10 | 11 | 12 | 13 | 16 | 17 | 18 | 19 | 20 | 40 | 41 | 42 | 43 |
44 | @RenderBody() 45 |
46 |
47 |

© 2019 - SampleWebApp

48 |
49 |
50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 63 | 69 | 70 | 71 | 72 | @RenderSection("Scripts", required: false) 73 | 74 | 75 | -------------------------------------------------------------------------------- /SampleWebApp/obj/SampleWebApp.csproj.nuget.g.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | True 5 | NuGet 6 | /Users/adampaternostro/Training-Azure-DevOps/SampleWebApp/obj/project.assets.json 7 | /Users/adampaternostro/.nuget/packages/ 8 | /Users/adampaternostro/.nuget/packages/;/usr/local/share/dotnet/sdk/NuGetFallbackFolder 9 | PackageReference 10 | 4.9.0 11 | 12 | 13 | $(MSBuildAllProjects);$(MSBuildThisFileFullPath) 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | /usr/local/share/dotnet/sdk/NuGetFallbackFolder/microsoft.entityframeworkcore.tools/2.1.1 26 | /usr/local/share/dotnet/sdk/NuGetFallbackFolder/microsoft.codeanalysis.analyzers/1.1.0 27 | /usr/local/share/dotnet/sdk/NuGetFallbackFolder/microsoft.aspnetcore.razor.design/2.1.2 28 | 29 | -------------------------------------------------------------------------------- /SampleWebApp/wwwroot/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js: -------------------------------------------------------------------------------- 1 | // Unobtrusive validation support library for jQuery and jQuery Validate 2 | // Copyright (C) Microsoft Corporation. All rights reserved. 3 | // @version v3.2.9 4 | !function(a){"function"==typeof define&&define.amd?define("jquery.validate.unobtrusive",["jquery.validation"],a):"object"==typeof module&&module.exports?module.exports=a(require("jquery-validation")):jQuery.validator.unobtrusive=a(jQuery)}(function(a){function e(a,e,n){a.rules[e]=n,a.message&&(a.messages[e]=a.message)}function n(a){return a.replace(/^\s+|\s+$/g,"").split(/\s*,\s*/g)}function t(a){return a.replace(/([!"#$%&'()*+,.\/:;<=>?@\[\\\]^`{|}~])/g,"\\$1")}function r(a){return a.substr(0,a.lastIndexOf(".")+1)}function i(a,e){return 0===a.indexOf("*.")&&(a=a.replace("*.",e)),a}function o(e,n){var r=a(this).find("[data-valmsg-for='"+t(n[0].name)+"']"),i=r.attr("data-valmsg-replace"),o=i?a.parseJSON(i)!==!1:null;r.removeClass("field-validation-valid").addClass("field-validation-error"),e.data("unobtrusiveContainer",r),o?(r.empty(),e.removeClass("input-validation-error").appendTo(r)):e.hide()}function d(e,n){var t=a(this).find("[data-valmsg-summary=true]"),r=t.find("ul");r&&r.length&&n.errorList.length&&(r.empty(),t.addClass("validation-summary-errors").removeClass("validation-summary-valid"),a.each(n.errorList,function(){a("
  • ").html(this.message).appendTo(r)}))}function s(e){var n=e.data("unobtrusiveContainer");if(n){var t=n.attr("data-valmsg-replace"),r=t?a.parseJSON(t):null;n.addClass("field-validation-valid").removeClass("field-validation-error"),e.removeData("unobtrusiveContainer"),r&&n.empty()}}function l(e){var n=a(this),t="__jquery_unobtrusive_validation_form_reset";if(!n.data(t)){n.data(t,!0);try{n.data("validator").resetForm()}finally{n.removeData(t)}n.find(".validation-summary-errors").addClass("validation-summary-valid").removeClass("validation-summary-errors"),n.find(".field-validation-error").addClass("field-validation-valid").removeClass("field-validation-error").removeData("unobtrusiveContainer").find(">*").removeData("unobtrusiveContainer")}}function u(e){var n=a(e),t=n.data(v),r=a.proxy(l,e),i=f.unobtrusive.options||{},u=function(n,t){var r=i[n];r&&a.isFunction(r)&&r.apply(e,t)};return t||(t={options:{errorClass:i.errorClass||"input-validation-error",errorElement:i.errorElement||"span",errorPlacement:function(){o.apply(e,arguments),u("errorPlacement",arguments)},invalidHandler:function(){d.apply(e,arguments),u("invalidHandler",arguments)},messages:{},rules:{},success:function(){s.apply(e,arguments),u("success",arguments)}},attachValidation:function(){n.off("reset."+v,r).on("reset."+v,r).validate(this.options)},validate:function(){return n.validate(),n.valid()}},n.data(v,t)),t}var m,f=a.validator,v="unobtrusiveValidation";return f.unobtrusive={adapters:[],parseElement:function(e,n){var t,r,i,o=a(e),d=o.parents("form")[0];d&&(t=u(d),t.options.rules[e.name]=r={},t.options.messages[e.name]=i={},a.each(this.adapters,function(){var n="data-val-"+this.name,t=o.attr(n),s={};void 0!==t&&(n+="-",a.each(this.params,function(){s[this]=o.attr(n+this)}),this.adapt({element:e,form:d,message:t,params:s,rules:r,messages:i}))}),a.extend(r,{__dummy__:!0}),n||t.attachValidation())},parse:function(e){var n=a(e),t=n.parents().addBack().filter("form").add(n.find("form")).has("[data-val=true]");n.find("[data-val=true]").each(function(){f.unobtrusive.parseElement(this,!0)}),t.each(function(){var a=u(this);a&&a.attachValidation()})}},m=f.unobtrusive.adapters,m.add=function(a,e,n){return n||(n=e,e=[]),this.push({name:a,params:e,adapt:n}),this},m.addBool=function(a,n){return this.add(a,function(t){e(t,n||a,!0)})},m.addMinMax=function(a,n,t,r,i,o){return this.add(a,[i||"min",o||"max"],function(a){var i=a.params.min,o=a.params.max;i&&o?e(a,r,[i,o]):i?e(a,n,i):o&&e(a,t,o)})},m.addSingleVal=function(a,n,t){return this.add(a,[n||"val"],function(r){e(r,t||a,r.params[n])})},f.addMethod("__dummy__",function(a,e,n){return!0}),f.addMethod("regex",function(a,e,n){var t;return!!this.optional(e)||(t=new RegExp(n).exec(a),t&&0===t.index&&t[0].length===a.length)}),f.addMethod("nonalphamin",function(a,e,n){var t;return n&&(t=a.match(/\W/g),t=t&&t.length>=n),t}),f.methods.extension?(m.addSingleVal("accept","mimtype"),m.addSingleVal("extension","extension")):m.addSingleVal("extension","extension","accept"),m.addSingleVal("regex","pattern"),m.addBool("creditcard").addBool("date").addBool("digits").addBool("email").addBool("number").addBool("url"),m.addMinMax("length","minlength","maxlength","rangelength").addMinMax("range","min","max","range"),m.addMinMax("minlength","minlength").addMinMax("maxlength","minlength","maxlength"),m.add("equalto",["other"],function(n){var o=r(n.element.name),d=n.params.other,s=i(d,o),l=a(n.form).find(":input").filter("[name='"+t(s)+"']")[0];e(n,"equalTo",l)}),m.add("required",function(a){"INPUT"===a.element.tagName.toUpperCase()&&"CHECKBOX"===a.element.type.toUpperCase()||e(a,"required",!0)}),m.add("remote",["url","type","additionalfields"],function(o){var d={url:o.params.url,type:o.params.type||"GET",data:{}},s=r(o.element.name);a.each(n(o.params.additionalfields||o.element.name),function(e,n){var r=i(n,s);d.data[r]=function(){var e=a(o.form).find(":input").filter("[name='"+t(r)+"']");return e.is(":checkbox")?e.filter(":checked").val()||e.filter(":hidden").val()||"":e.is(":radio")?e.filter(":checked").val()||"":e.val()}}),e(o,"remote",d)}),m.add("password",["min","nonalphamin","regex"],function(a){a.params.min&&e(a,"minlength",a.params.min),a.params.nonalphamin&&e(a,"nonalphamin",a.params.nonalphamin),a.params.regex&&e(a,"regex",a.params.regex)}),m.add("fileextensions",["extensions"],function(a){e(a,"extension",a.params.extensions)}),a(function(){f.unobtrusive.parse(document)}),f.unobtrusive}); -------------------------------------------------------------------------------- /DevOps-Best-Practices.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | Here are the items I have learned and implement on my DevOps in Azure. 3 | 4 | # Best Practices 5 | - I usually name my resource groups with a "-DEV", "-QA" and "-PROD". Naming your items accordingly will make creating new environments easy. Also, if you want to deploy to many Azure Regions see: https://github.com/AdamPaternostro/Azure-Dual-Region-Deployment-Approach 6 | 7 | - The DevOps team and developers should work together to build the original pipeline. Developers needs to understand what they need to expose as "variables" to the CI/CD engine. 8 | 9 | - I have all my applications setup for 24/7 deployments. This means that I can deploy at anytime without the need for downtime. The cloud makes this "easy" by providing the abilty to stand up the next version of your application, smoke test it and then swap to production. My goal is to have the business users approve the releases to QA and Production. To support this, along with CI/CD, please review: https://github.com/AdamPaternostro/Azure-Dev-Ops-Single-CI-CD-Dev-To-Production-Pipeline. This shows how you can have a single pipeline from Dev to Prod while using CI/CD releases to Dev. 10 | 11 | 12 | # Database DevOps (Schema Changes) 13 | If you have a database as part of your application 14 | 1. You should deploy your database schema changes before deploying your application. A seperate deployment. 15 | 2. Database schema changes should NOT break your current application 16 | - No dropping of columns, tables, stored procedures, etc. 17 | - If you add a column, it must have a default value 18 | - If you add a parameter to a stored procedure, it must have a default value 19 | 3. When you need to drop an item (e.g. Table) 20 | - If your current code release is version 1.0 and then next release is version 1.1 21 | - Do not drop the table in v1.1 deployment 22 | - When you deploy your v1.1 code to the preprod slot the production slot will have v1.0, so BOTH v1.0 and v1.1 will be running and using the same database schema. 23 | - Drop the table in the v1.2 release since v1.1 will be in the production slot and v1.2 will go to preprod and netiher release relies on the dropped table 24 | 4. If you have seed data (e.g. states table or zipcode table, etc.) 25 | - Create a stored procedure called "InitializeDatabase" 26 | - This procedure should be very robust/idempotent 27 | - IF NOT EXISTS(SELECT 1 FROM myTABLE WHERE id = 10) THEN INSERT INTO myTable (myField) VALUES (10); 28 | - When your code starts up (or part of Dev Ops), you should call this procedure 29 | - By placing all your look up values in a stored procedure, the stored procedure should be under source control (yes, you should be using database source control) 30 | 31 | 32 | # Database DevOps (Data Updates) 33 | - For minor data updates you can run these as part of a "InitializeDataUpdate" stored procedure much like the "InitializeDatabase". This procedure can do tests like, if all values in a column is NULL then seed the value. 34 | - For large data changes, like changing all the values of a lookup table used in calcuations, you can backup the table and then update the entire table. If this is millions of rows and takes a long time, you can backup the database (the cloud does this for you), then issue the update. I currently do not have a good DevOps way of hanlding this. It is mainly manual since some updates might take hours. Rolling back is hard since you might be looking at a restore and copy data. 35 | 36 | 37 | # Azure Resource DevOps 38 | - With the cloud, your code should create all the items within a resource. 39 | - Your infrastructure as code should create the Azure Storage account 40 | - Your code should create all the blob containers, queues, tables, etc. 41 | - The goal of your code should be that you can compile and run without any prerequisites! This way you can spin up new environments quickly and without a person creating a bunch of required dependencies. 42 | 43 | 44 | # Beware of ourside shared resources 45 | - If you code uses a file in blob storage, lets say it if a PDF file with fields that need to be populate by your code 46 | - If you need to update this file and are using the staging slot technique then when you deploy to staging, this file will mostlikely be updated. The issue is the current production code is now using an updated file. 47 | - Try to keep these dependecies as part of your project 48 | 49 | 50 | # Configuration values 51 | - I have mainly given up on updating an application configuration value when an application is running in production. You should have some configuration values as part of the application settings (e.g. environment variables). If you need to update a value, I perfer to push out a whole new release. 52 | - My code typically loads the configuration values at runtime. The code does not get these values from application settings. The code loads the values from something like KeyVault. I usually have my configuration values named "DatabaseConnectionString-{Environment}" and I get the ENVIRONMENT variable from my settings / DevOps and then load based upon the string. This means my code is really dependent on just a single application setting variable named ENVIRONMENT. This avoids having all the configuration/secret values as part of my Dev Ops process. If someone wants to change a database password, then they change it in KeyVault. You application should cache the values from KeyVault for an acceptable amount of time (e.g. 5 minutes). This way if a value changes, you should be able to do your key rotations in 5 to 10 minute window. 53 | 54 | 55 | # RDP / SSH access 56 | - If you do your DevOps process correctly you should never allow RDP or SSH access to a machine in QA or Production. Dev is okay for troubleshoot some items (remote installs), but I have not remoted to a production machine in many, many, many years. 57 | 58 | 59 | # How I do DevOps on my projects 60 | 1. Create a resource group named MyProject-PoC (this is my playground) 61 | 2. Create my Azure resources by hand and do some testing (change / delete resources) 62 | 3. Create a Hello World app that has all my tiers (make sure the App works) 63 | 4. Add security to my Hello World app to all my tiers (pass security between tiers and get the security teams sign-off) 64 | 5. Export my ARM template from the Azure Portal Edit my ARM template. Create parameters for everything. 65 | 6. Run my ARM template and create a new resource group called MyProject-DEV (all my resources will have a –DEV suffix) 66 | 7. Run my application and make sure it works just like step 4. 67 | 8. Repeat Steps 6 and 7 over and over! 68 | 9. The code and ARM template should now be in source control 69 | 10. Create a build definition 70 | 11. Create a release definition 71 | 12. Run it. Make sure it works. 72 | 13. Delete my MyProject-POC since everything should now be automated. 73 | 14. Create a QA and Prod pipeline by cloning the Dev pipeline (they should use a suffix of –QA and –PROD) 74 | 15. Now code! 75 | 16. Implement Logging, Error handling and Monitoring 76 | 17. Make minor adjustments to my CI/CD pipeline. -------------------------------------------------------------------------------- /SampleWebApp/wwwroot/images/banner2.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /SampleWebApp/wwwroot/images/banner1.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Sample-Docker-ARM-Templates/arm-template-web.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "sites_azuretrainingdockerimageapp_name": { 6 | "defaultValue": "azuretrainingdockerimageapp", 7 | "type": "String" 8 | }, 9 | "serverfarms_azuretrainingdockerimageappservice_name": { 10 | "defaultValue": "azuretrainingdockerimageappservice", 11 | "type": "String" 12 | }, 13 | "ACR_Name": { 14 | "defaultValue": "TrainingAzureDevOpsACR", 15 | "type": "String" 16 | }, 17 | "ACR_Password": { 18 | "defaultValue": "SetMe", 19 | "type": "String" 20 | }, 21 | "DockerImageName": { 22 | "defaultValue": "Unknown", 23 | "type": "String" 24 | }, 25 | "DockerTag": { 26 | "defaultValue": "latest", 27 | "type": "String" 28 | } 29 | }, 30 | "variables": { 31 | "hostNameBindings_azuretrainingdockerimageapp.azurewebsites.net_name": "[concat(toLower(parameters('sites_azuretrainingdockerimageapp_name')), '.azurewebsites.net')]" 32 | }, 33 | "resources": [ 34 | { 35 | "type": "Microsoft.Web/serverfarms", 36 | "sku": { 37 | "name": "P1v2", 38 | "tier": "PremiumV2", 39 | "size": "P1v2", 40 | "family": "Pv2", 41 | "capacity": 1 42 | }, 43 | "kind": "linux", 44 | "name": "[parameters('serverfarms_azuretrainingdockerimageappservice_name')]", 45 | "apiVersion": "2016-09-01", 46 | "location": "East US", 47 | "scale": null, 48 | "properties": { 49 | "name": "[parameters('serverfarms_azuretrainingdockerimageappservice_name')]", 50 | "workerTierName": null, 51 | "adminSiteName": null, 52 | "hostingEnvironmentProfile": null, 53 | "perSiteScaling": false, 54 | "reserved": true, 55 | "targetWorkerCount": 0, 56 | "targetWorkerSizeId": 0 57 | }, 58 | "dependsOn": [] 59 | }, 60 | { 61 | "type": "Microsoft.Web/sites", 62 | "kind": "app,linux,container", 63 | "name": "[parameters('sites_azuretrainingdockerimageapp_name')]", 64 | "apiVersion": "2016-08-01", 65 | "location": "East US", 66 | "scale": null, 67 | "properties": { 68 | "enabled": true, 69 | "hostNameSslStates": [ 70 | { 71 | "name": "[concat(parameters('sites_azuretrainingdockerimageapp_name'),'.azurewebsites.net')]", 72 | "sslState": "Disabled", 73 | "virtualIP": null, 74 | "thumbprint": null, 75 | "toUpdate": null, 76 | "hostType": "Standard" 77 | }, 78 | { 79 | "name": "[concat(parameters('sites_azuretrainingdockerimageapp_name'),'.scm.azurewebsites.net')]", 80 | "sslState": "Disabled", 81 | "virtualIP": null, 82 | "thumbprint": null, 83 | "toUpdate": null, 84 | "hostType": "Repository" 85 | } 86 | ], 87 | "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', parameters('serverfarms_azuretrainingdockerimageappservice_name'))]", 88 | "reserved": true, 89 | "siteConfig": { 90 | "appSettings": [ 91 | { 92 | "name": "DOCKER_REGISTRY_SERVER_PASSWORD", 93 | "value": "[parameters('ACR_Password')]" 94 | }, 95 | { 96 | "name": "DOCKER_REGISTRY_SERVER_URL", 97 | "value": "[concat('https://',toLower(parameters('ACR_Name')),'.azurecr.io')]" 98 | }, 99 | { 100 | "name": "DOCKER_REGISTRY_SERVER_USERNAME", 101 | "value": "[toLower(parameters('ACR_Name'))]" 102 | }, 103 | { 104 | "name": "WEBSITES_PORT", 105 | "value": "8000" 106 | } 107 | ] 108 | }, 109 | "scmSiteAlsoStopped": false, 110 | "hostingEnvironmentProfile": null, 111 | "clientAffinityEnabled": true, 112 | "clientCertEnabled": false, 113 | "hostNamesDisabled": false, 114 | "containerSize": 0, 115 | "dailyMemoryTimeQuota": 0, 116 | "cloningInfo": null, 117 | "httpsOnly": false 118 | }, 119 | "dependsOn": [ 120 | "[resourceId('Microsoft.Web/serverfarms', parameters('serverfarms_azuretrainingdockerimageappservice_name'))]" 121 | ] 122 | }, 123 | { 124 | "type": "Microsoft.Web/sites/config", 125 | "name": "[concat(parameters('sites_azuretrainingdockerimageapp_name'), '/', 'web')]", 126 | "apiVersion": "2016-08-01", 127 | "location": "East US", 128 | "scale": null, 129 | "properties": { 130 | "numberOfWorkers": 1, 131 | "defaultDocuments": [ 132 | "Default.htm", 133 | "Default.html", 134 | "Default.asp", 135 | "index.htm", 136 | "index.html", 137 | "iisstart.htm", 138 | "default.aspx", 139 | "index.php", 140 | "hostingstart.html" 141 | ], 142 | "netFrameworkVersion": "v4.0", 143 | "phpVersion": "", 144 | "pythonVersion": "", 145 | "nodeVersion": "", 146 | "linuxFxVersion": "[concat('DOCKER|',toLower(parameters('ACR_Name')),'.azurecr.io/',toLower(parameters('DockerImageName')),':',parameters('DockerTag'))]", 147 | "windowsFxVersion": null, 148 | "requestTracingEnabled": false, 149 | "remoteDebuggingEnabled": false, 150 | "remoteDebuggingVersion": null, 151 | "httpLoggingEnabled": false, 152 | "logsDirectorySizeLimit": 35, 153 | "detailedErrorLoggingEnabled": false, 154 | "publishingUsername": "$azuretrainingdockerimageapp", 155 | "publishingPassword": null, 156 | "appSettings": null, 157 | "azureStorageAccounts": {}, 158 | "metadata": null, 159 | "connectionStrings": null, 160 | "machineKey": null, 161 | "handlerMappings": null, 162 | "documentRoot": null, 163 | "scmType": "None", 164 | "use32BitWorkerProcess": true, 165 | "webSocketsEnabled": false, 166 | "alwaysOn": false, 167 | "javaVersion": null, 168 | "javaContainer": null, 169 | "javaContainerVersion": null, 170 | "appCommandLine": "", 171 | "managedPipelineMode": "Integrated", 172 | "virtualApplications": [ 173 | { 174 | "virtualPath": "/", 175 | "physicalPath": "site\\wwwroot", 176 | "preloadEnabled": false, 177 | "virtualDirectories": null 178 | } 179 | ], 180 | "winAuthAdminState": 0, 181 | "winAuthTenantState": 0, 182 | "customAppPoolIdentityAdminState": false, 183 | "customAppPoolIdentityTenantState": false, 184 | "runtimeADUser": null, 185 | "runtimeADUserPassword": null, 186 | "loadBalancing": "LeastRequests", 187 | "routingRules": [], 188 | "experiments": { 189 | "rampUpRules": [] 190 | }, 191 | "limits": null, 192 | "autoHealEnabled": false, 193 | "autoHealRules": null, 194 | "tracingOptions": null, 195 | "vnetName": "", 196 | "siteAuthEnabled": false, 197 | "siteAuthSettings": { 198 | "enabled": null, 199 | "unauthenticatedClientAction": null, 200 | "tokenStoreEnabled": null, 201 | "allowedExternalRedirectUrls": null, 202 | "defaultProvider": null, 203 | "clientId": null, 204 | "clientSecret": null, 205 | "clientSecretCertificateThumbprint": null, 206 | "issuer": null, 207 | "allowedAudiences": null, 208 | "additionalLoginParams": null, 209 | "isAadAutoProvisioned": false, 210 | "googleClientId": null, 211 | "googleClientSecret": null, 212 | "googleOAuthScopes": null, 213 | "facebookAppId": null, 214 | "facebookAppSecret": null, 215 | "facebookOAuthScopes": null, 216 | "twitterConsumerKey": null, 217 | "twitterConsumerSecret": null, 218 | "microsoftAccountClientId": null, 219 | "microsoftAccountClientSecret": null, 220 | "microsoftAccountOAuthScopes": null 221 | }, 222 | "cors": null, 223 | "push": null, 224 | "apiDefinition": null, 225 | "autoSwapSlotName": null, 226 | "localMySqlEnabled": false, 227 | "managedServiceIdentityId": null, 228 | "xManagedServiceIdentityId": null, 229 | "ipSecurityRestrictions": null, 230 | "scmIpSecurityRestrictions": null, 231 | "scmIpSecurityRestrictionsUseMain": null, 232 | "http20Enabled": false, 233 | "minTlsVersion": "1.2", 234 | "ftpsState": "AllAllowed", 235 | "reservedInstanceCount": 0 236 | }, 237 | "dependsOn": [ 238 | "[resourceId('Microsoft.Web/sites', parameters('sites_azuretrainingdockerimageapp_name'))]" 239 | ] 240 | }, 241 | { 242 | "type": "Microsoft.Web/sites/hostNameBindings", 243 | "name": "[concat(parameters('sites_azuretrainingdockerimageapp_name'), '/', variables('hostNameBindings_azuretrainingdockerimageapp.azurewebsites.net_name'))]", 244 | "apiVersion": "2016-08-01", 245 | "location": "East US", 246 | "scale": null, 247 | "properties": { 248 | "siteName": "[parameters('sites_azuretrainingdockerimageapp_name')]", 249 | "domainId": null, 250 | "hostNameType": "Verified" 251 | }, 252 | "dependsOn": [ 253 | "[resourceId('Microsoft.Web/sites', parameters('sites_azuretrainingdockerimageapp_name'))]" 254 | ] 255 | } 256 | ] 257 | } -------------------------------------------------------------------------------- /SampleWebApp/wwwroot/images/banner3.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /AzureFunction-ARM-Templates/arm-function-template.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "AzureFunctionPlanName": { 6 | "defaultValue": "EastUSPlan", 7 | "type": "String" 8 | }, 9 | "AzureFunctionAppName": { 10 | "defaultValue": "Training-Azure-DevOps-FunctionApp", 11 | "type": "String" 12 | }, 13 | "AzureFunctionStorageAccountName": { 14 | "defaultValue": "functionappstorage001", 15 | "type": "String" 16 | } 17 | }, 18 | "variables": { 19 | "hostNameBindings_training_azure_devops_functionapp.azurewebsites.net_name": "[concat(toLower(parameters('AzureFunctionAppName')), '.azurewebsites.net')]" 20 | }, 21 | "resources": [ 22 | { 23 | "type": "Microsoft.Storage/storageAccounts", 24 | "sku": { 25 | "name": "Standard_LRS", 26 | "tier": "Standard" 27 | }, 28 | "kind": "Storage", 29 | "name": "[parameters('AzureFunctionStorageAccountName')]", 30 | "apiVersion": "2018-07-01", 31 | "location": "East US", 32 | "tags": {}, 33 | "scale": null, 34 | "properties": { 35 | "networkAcls": { 36 | "bypass": "AzureServices", 37 | "virtualNetworkRules": [], 38 | "ipRules": [], 39 | "defaultAction": "Allow" 40 | }, 41 | "supportsHttpsTrafficOnly": false, 42 | "encryption": { 43 | "services": { 44 | "file": { 45 | "enabled": true 46 | }, 47 | "blob": { 48 | "enabled": true 49 | } 50 | }, 51 | "keySource": "Microsoft.Storage" 52 | } 53 | }, 54 | "dependsOn": [] 55 | }, 56 | { 57 | "type": "Microsoft.Web/serverfarms", 58 | "sku": { 59 | "name": "Y1", 60 | "tier": "Dynamic", 61 | "size": "Y1", 62 | "family": "Y", 63 | "capacity": 0 64 | }, 65 | "kind": "functionapp", 66 | "name": "[parameters('AzureFunctionPlanName')]", 67 | "apiVersion": "2016-09-01", 68 | "location": "East US", 69 | "scale": null, 70 | "properties": { 71 | "name": "[parameters('AzureFunctionPlanName')]", 72 | "workerTierName": null, 73 | "adminSiteName": null, 74 | "hostingEnvironmentProfile": null, 75 | "perSiteScaling": false, 76 | "reserved": false, 77 | "targetWorkerCount": 0, 78 | "targetWorkerSizeId": 0 79 | }, 80 | "dependsOn": [] 81 | }, 82 | { 83 | "type": "Microsoft.Web/sites", 84 | "kind": "functionapp", 85 | "name": "[parameters('AzureFunctionAppName')]", 86 | "apiVersion": "2016-08-01", 87 | "location": "East US", 88 | "scale": null, 89 | "properties": { 90 | "enabled": true, 91 | "hostNameSslStates": [ 92 | { 93 | "name": "[concat(parameters('AzureFunctionAppName'),'.azurewebsites.net')]", 94 | "sslState": "Disabled", 95 | "virtualIP": null, 96 | "thumbprint": null, 97 | "toUpdate": null, 98 | "hostType": "Standard" 99 | }, 100 | { 101 | "name": "[concat(parameters('AzureFunctionAppName'),'.scm.azurewebsites.net')]", 102 | "sslState": "Disabled", 103 | "virtualIP": null, 104 | "thumbprint": null, 105 | "toUpdate": null, 106 | "hostType": "Repository" 107 | } 108 | ], 109 | "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', parameters('AzureFunctionPlanName'))]", 110 | "reserved": false, 111 | "siteConfig": { 112 | "appSettings": [ 113 | { 114 | "name": "AzureWebJobsStorage", 115 | "value": "[concat('DefaultEndpointsProtocol=https;AccountName=',parameters('AzureFunctionStorageAccountName'),';AccountKey=',listKeys(resourceId('Microsoft.Storage/storageAccounts', parameters('AzureFunctionStorageAccountName')), providers('Microsoft.Storage', 'storageAccounts').apiVersions[0]).keys[0].value)]" 116 | }, 117 | { 118 | "name": "WEBSITE_CONTENTAZUREFILECONNECTIONSTRING", 119 | "value": "[concat('DefaultEndpointsProtocol=https;AccountName=',parameters('AzureFunctionStorageAccountName'),';AccountKey=',listKeys(resourceId('Microsoft.Storage/storageAccounts', parameters('AzureFunctionStorageAccountName')), providers('Microsoft.Storage', 'storageAccounts').apiVersions[0]).keys[0].value)]" 120 | }, 121 | { 122 | "name": "WEBSITE_CONTENTSHARE", 123 | "value": "[toLower(parameters('AzureFunctionAppName'))]" 124 | }, 125 | { 126 | "name": "FUNCTIONS_EXTENSION_VERSION", 127 | "value": "~2" 128 | }, 129 | { 130 | "name": "FUNCTIONS_WORKER_RUNTIME", 131 | "value": "dotnet" 132 | } 133 | ] 134 | }, 135 | "scmSiteAlsoStopped": false, 136 | "hostingEnvironmentProfile": null, 137 | "clientAffinityEnabled": false, 138 | "clientCertEnabled": false, 139 | "hostNamesDisabled": false, 140 | "containerSize": 1536, 141 | "dailyMemoryTimeQuota": 0, 142 | "cloningInfo": null, 143 | "httpsOnly": false 144 | }, 145 | "dependsOn": [ 146 | "[resourceId('Microsoft.Web/serverfarms', parameters('AzureFunctionPlanName'))]", 147 | "[resourceId('Microsoft.Storage/storageAccounts', parameters('AzureFunctionStorageAccountName'))]" 148 | ] 149 | }, 150 | { 151 | "type": "Microsoft.Web/sites/config", 152 | "name": "[concat(parameters('AzureFunctionAppName'), '/', 'web')]", 153 | "apiVersion": "2016-08-01", 154 | "location": "East US", 155 | "scale": null, 156 | "properties": { 157 | "numberOfWorkers": 1, 158 | "defaultDocuments": [ 159 | "Default.htm", 160 | "Default.html", 161 | "Default.asp", 162 | "index.htm", 163 | "index.html", 164 | "iisstart.htm", 165 | "default.aspx", 166 | "index.php" 167 | ], 168 | "netFrameworkVersion": "v4.0", 169 | "phpVersion": "5.6", 170 | "pythonVersion": "", 171 | "nodeVersion": "", 172 | "linuxFxVersion": "", 173 | "windowsFxVersion": null, 174 | "requestTracingEnabled": false, 175 | "remoteDebuggingEnabled": false, 176 | "remoteDebuggingVersion": null, 177 | "httpLoggingEnabled": false, 178 | "logsDirectorySizeLimit": 35, 179 | "detailedErrorLoggingEnabled": false, 180 | "publishingUsername": "$Training-Azure-DevOps-FunctionApp", 181 | "publishingPassword": null, 182 | "appSettings": null, 183 | "azureStorageAccounts": {}, 184 | "metadata": null, 185 | "connectionStrings": null, 186 | "machineKey": null, 187 | "handlerMappings": null, 188 | "documentRoot": null, 189 | "scmType": "None", 190 | "use32BitWorkerProcess": true, 191 | "webSocketsEnabled": false, 192 | "alwaysOn": false, 193 | "javaVersion": null, 194 | "javaContainer": null, 195 | "javaContainerVersion": null, 196 | "appCommandLine": "", 197 | "managedPipelineMode": "Integrated", 198 | "virtualApplications": [ 199 | { 200 | "virtualPath": "/", 201 | "physicalPath": "site\\wwwroot", 202 | "preloadEnabled": false, 203 | "virtualDirectories": null 204 | } 205 | ], 206 | "winAuthAdminState": 0, 207 | "winAuthTenantState": 0, 208 | "customAppPoolIdentityAdminState": false, 209 | "customAppPoolIdentityTenantState": false, 210 | "runtimeADUser": null, 211 | "runtimeADUserPassword": null, 212 | "loadBalancing": "LeastRequests", 213 | "routingRules": [], 214 | "experiments": { 215 | "rampUpRules": [] 216 | }, 217 | "limits": null, 218 | "autoHealEnabled": false, 219 | "autoHealRules": null, 220 | "tracingOptions": null, 221 | "vnetName": "", 222 | "siteAuthEnabled": false, 223 | "siteAuthSettings": { 224 | "enabled": null, 225 | "unauthenticatedClientAction": null, 226 | "tokenStoreEnabled": null, 227 | "allowedExternalRedirectUrls": null, 228 | "defaultProvider": null, 229 | "clientId": null, 230 | "clientSecret": null, 231 | "clientSecretCertificateThumbprint": null, 232 | "issuer": null, 233 | "allowedAudiences": null, 234 | "additionalLoginParams": null, 235 | "isAadAutoProvisioned": false, 236 | "googleClientId": null, 237 | "googleClientSecret": null, 238 | "googleOAuthScopes": null, 239 | "facebookAppId": null, 240 | "facebookAppSecret": null, 241 | "facebookOAuthScopes": null, 242 | "twitterConsumerKey": null, 243 | "twitterConsumerSecret": null, 244 | "microsoftAccountClientId": null, 245 | "microsoftAccountClientSecret": null, 246 | "microsoftAccountOAuthScopes": null 247 | }, 248 | "cors": { 249 | "allowedOrigins": [ 250 | "https://functions.azure.com", 251 | "https://functions-staging.azure.com", 252 | "https://functions-next.azure.com" 253 | ], 254 | "supportCredentials": false 255 | }, 256 | "push": null, 257 | "apiDefinition": null, 258 | "autoSwapSlotName": null, 259 | "localMySqlEnabled": false, 260 | "managedServiceIdentityId": null, 261 | "xManagedServiceIdentityId": null, 262 | "ipSecurityRestrictions": null, 263 | "http20Enabled": false, 264 | "minTlsVersion": "1.2", 265 | "ftpsState": "AllAllowed", 266 | "reservedInstanceCount": 0 267 | }, 268 | "dependsOn": [ 269 | "[resourceId('Microsoft.Web/sites', parameters('AzureFunctionAppName'))]" 270 | ] 271 | }, 272 | { 273 | "type": "Microsoft.Web/sites/hostNameBindings", 274 | "name": "[concat(parameters('AzureFunctionAppName'), '/', variables('hostNameBindings_training_azure_devops_functionapp.azurewebsites.net_name'))]", 275 | "apiVersion": "2016-08-01", 276 | "location": "East US", 277 | "scale": null, 278 | "properties": { 279 | "siteName": "Training-Azure-DevOps-FunctionApp", 280 | "domainId": null, 281 | "hostNameType": "Verified" 282 | }, 283 | "dependsOn": [ 284 | "[resourceId('Microsoft.Web/sites', parameters('AzureFunctionAppName'))]" 285 | ] 286 | } 287 | ] 288 | } -------------------------------------------------------------------------------- /SampleWebApp/wwwroot/lib/jquery-validation/dist/additional-methods.min.js: -------------------------------------------------------------------------------- 1 | /*! jQuery Validation Plugin - v1.17.0 - 7/29/2017 2 | * https://jqueryvalidation.org/ 3 | * Copyright (c) 2017 Jörn Zaefferer; Licensed MIT */ 4 | !function(a){"function"==typeof define&&define.amd?define(["jquery","./jquery.validate.min"],a):"object"==typeof module&&module.exports?module.exports=a(require("jquery")):a(jQuery)}(function(a){return function(){function b(a){return a.replace(/<.[^<>]*?>/g," ").replace(/ | /gi," ").replace(/[.(),;:!?%#$'\"_+=\/\-“”’]*/g,"")}a.validator.addMethod("maxWords",function(a,c,d){return this.optional(c)||b(a).match(/\b\w+\b/g).length<=d},a.validator.format("Please enter {0} words or less.")),a.validator.addMethod("minWords",function(a,c,d){return this.optional(c)||b(a).match(/\b\w+\b/g).length>=d},a.validator.format("Please enter at least {0} words.")),a.validator.addMethod("rangeWords",function(a,c,d){var e=b(a),f=/\b\w+\b/g;return this.optional(c)||e.match(f).length>=d[0]&&e.match(f).length<=d[1]},a.validator.format("Please enter between {0} and {1} words."))}(),a.validator.addMethod("accept",function(b,c,d){var e,f,g,h="string"==typeof d?d.replace(/\s/g,""):"image/*",i=this.optional(c);if(i)return i;if("file"===a(c).attr("type")&&(h=h.replace(/[\-\[\]\/\{\}\(\)\+\?\.\\\^\$\|]/g,"\\$&").replace(/,/g,"|").replace(/\/\*/g,"/.*"),c.files&&c.files.length))for(g=new RegExp(".?("+h+")$","i"),e=0;e9?"0":f,g="JABCDEFGHI".substr(f,1).toString(),i.match(/[ABEH]/)?k===f:i.match(/[KPQS]/)?k===g:k===f||k===g},"Please specify a valid CIF number."),a.validator.addMethod("cpfBR",function(a){if(a=a.replace(/([~!@#$%^&*()_+=`{}\[\]\-|\\:;'<>,.\/? ])+/g,""),11!==a.length)return!1;var b,c,d,e,f=0;if(b=parseInt(a.substring(9,10),10),c=parseInt(a.substring(10,11),10),d=function(a,b){var c=10*a%11;return 10!==c&&11!==c||(c=0),c===b},""===a||"00000000000"===a||"11111111111"===a||"22222222222"===a||"33333333333"===a||"44444444444"===a||"55555555555"===a||"66666666666"===a||"77777777777"===a||"88888888888"===a||"99999999999"===a)return!1;for(e=1;e<=9;e++)f+=parseInt(a.substring(e-1,e),10)*(11-e);if(d(f,b)){for(f=0,e=1;e<=10;e++)f+=parseInt(a.substring(e-1,e),10)*(12-e);return d(f,c)}return!1},"Please specify a valid CPF number"),a.validator.addMethod("creditcard",function(a,b){if(this.optional(b))return"dependency-mismatch";if(/[^0-9 \-]+/.test(a))return!1;var c,d,e=0,f=0,g=!1;if(a=a.replace(/\D/g,""),a.length<13||a.length>19)return!1;for(c=a.length-1;c>=0;c--)d=a.charAt(c),f=parseInt(d,10),g&&(f*=2)>9&&(f-=9),e+=f,g=!g;return e%10===0},"Please enter a valid credit card number."),a.validator.addMethod("creditcardtypes",function(a,b,c){if(/[^0-9\-]+/.test(a))return!1;a=a.replace(/\D/g,"");var d=0;return c.mastercard&&(d|=1),c.visa&&(d|=2),c.amex&&(d|=4),c.dinersclub&&(d|=8),c.enroute&&(d|=16),c.discover&&(d|=32),c.jcb&&(d|=64),c.unknown&&(d|=128),c.all&&(d=255),1&d&&/^(5[12345])/.test(a)?16===a.length:2&d&&/^(4)/.test(a)?16===a.length:4&d&&/^(3[47])/.test(a)?15===a.length:8&d&&/^(3(0[012345]|[68]))/.test(a)?14===a.length:16&d&&/^(2(014|149))/.test(a)?15===a.length:32&d&&/^(6011)/.test(a)?16===a.length:64&d&&/^(3)/.test(a)?16===a.length:64&d&&/^(2131|1800)/.test(a)?15===a.length:!!(128&d)},"Please enter a valid credit card number."),a.validator.addMethod("currency",function(a,b,c){var d,e="string"==typeof c,f=e?c:c[0],g=!!e||c[1];return f=f.replace(/,/g,""),f=g?f+"]":f+"]?",d="^["+f+"([1-9]{1}[0-9]{0,2}(\\,[0-9]{3})*(\\.[0-9]{0,2})?|[1-9]{1}[0-9]{0,}(\\.[0-9]{0,2})?|0(\\.[0-9]{0,2})?|(\\.[0-9]{1,2})?)$",d=new RegExp(d),this.optional(b)||d.test(a)},"Please specify a valid currency"),a.validator.addMethod("dateFA",function(a,b){return this.optional(b)||/^[1-4]\d{3}\/((0?[1-6]\/((3[0-1])|([1-2][0-9])|(0?[1-9])))|((1[0-2]|(0?[7-9]))\/(30|([1-2][0-9])|(0?[1-9]))))$/.test(a)},a.validator.messages.date),a.validator.addMethod("dateITA",function(a,b){var c,d,e,f,g,h=!1,i=/^\d{1,2}\/\d{1,2}\/\d{4}$/;return i.test(a)?(c=a.split("/"),d=parseInt(c[0],10),e=parseInt(c[1],10),f=parseInt(c[2],10),g=new Date(Date.UTC(f,e-1,d,12,0,0,0)),h=g.getUTCFullYear()===f&&g.getUTCMonth()===e-1&&g.getUTCDate()===d):h=!1,this.optional(b)||h},a.validator.messages.date),a.validator.addMethod("dateNL",function(a,b){return this.optional(b)||/^(0?[1-9]|[12]\d|3[01])[\.\/\-](0?[1-9]|1[012])[\.\/\-]([12]\d)?(\d\d)$/.test(a)},a.validator.messages.date),a.validator.addMethod("extension",function(a,b,c){return c="string"==typeof c?c.replace(/,/g,"|"):"png|jpe?g|gif",this.optional(b)||a.match(new RegExp("\\.("+c+")$","i"))},a.validator.format("Please enter a value with a valid extension.")),a.validator.addMethod("giroaccountNL",function(a,b){return this.optional(b)||/^[0-9]{1,7}$/.test(a)},"Please specify a valid giro account number"),a.validator.addMethod("iban",function(a,b){if(this.optional(b))return!0;var c,d,e,f,g,h,i,j,k,l=a.replace(/ /g,"").toUpperCase(),m="",n=!0,o="",p="",q=5;if(l.length9&&a.match(/^(?:(?:(?:00\s?|\+)44\s?|0)7(?:[1345789]\d{2}|624)\s?\d{3}\s?\d{3})$/)},"Please specify a valid mobile number"),a.validator.addMethod("netmask",function(a,b){return this.optional(b)||/^(254|252|248|240|224|192|128)\.0\.0\.0|255\.(254|252|248|240|224|192|128|0)\.0\.0|255\.255\.(254|252|248|240|224|192|128|0)\.0|255\.255\.255\.(254|252|248|240|224|192|128|0)/i.test(a)},"Please enter a valid netmask."),a.validator.addMethod("nieES",function(a,b){"use strict";if(this.optional(b))return!0;var c,d=new RegExp(/^[MXYZ]{1}[0-9]{7,8}[TRWAGMYFPDXBNJZSQVHLCKET]{1}$/gi),e="TRWAGMYFPDXBNJZSQVHLCKET",f=a.substr(a.length-1).toUpperCase();return a=a.toString().toUpperCase(),!(a.length>10||a.length<9||!d.test(a))&&(a=a.replace(/^[X]/,"0").replace(/^[Y]/,"1").replace(/^[Z]/,"2"),c=9===a.length?a.substr(0,8):a.substr(0,9),e.charAt(parseInt(c,10)%23)===f)},"Please specify a valid NIE number."),a.validator.addMethod("nifES",function(a,b){"use strict";return!!this.optional(b)||(a=a.toUpperCase(),!!a.match("((^[A-Z]{1}[0-9]{7}[A-Z0-9]{1}$|^[T]{1}[A-Z0-9]{8}$)|^[0-9]{8}[A-Z]{1}$)")&&(/^[0-9]{8}[A-Z]{1}$/.test(a)?"TRWAGMYFPDXBNJZSQVHLCKE".charAt(a.substring(8,0)%23)===a.charAt(8):!!/^[KLM]{1}/.test(a)&&a[8]==="TRWAGMYFPDXBNJZSQVHLCKE".charAt(a.substring(8,1)%23)))},"Please specify a valid NIF number."),a.validator.addMethod("nipPL",function(a){"use strict";if(a=a.replace(/[^0-9]/g,""),10!==a.length)return!1;for(var b=[6,5,7,2,3,4,5,6,7],c=0,d=0;d<9;d++)c+=b[d]*a[d];var e=c%11,f=10===e?0:e;return f===parseInt(a[9],10)},"Please specify a valid NIP number."),a.validator.addMethod("notEqualTo",function(b,c,d){return this.optional(c)||!a.validator.methods.equalTo.call(this,b,c,d)},"Please enter a different value, values must not be the same."),a.validator.addMethod("nowhitespace",function(a,b){return this.optional(b)||/^\S+$/i.test(a)},"No white space please"),a.validator.addMethod("pattern",function(a,b,c){return!!this.optional(b)||("string"==typeof c&&(c=new RegExp("^(?:"+c+")$")),c.test(a))},"Invalid format."),a.validator.addMethod("phoneNL",function(a,b){return this.optional(b)||/^((\+|00(\s|\s?\-\s?)?)31(\s|\s?\-\s?)?(\(0\)[\-\s]?)?|0)[1-9]((\s|\s?\-\s?)?[0-9]){8}$/.test(a)},"Please specify a valid phone number."),a.validator.addMethod("phonesUK",function(a,b){return a=a.replace(/\(|\)|\s+|-/g,""),this.optional(b)||a.length>9&&a.match(/^(?:(?:(?:00\s?|\+)44\s?|0)(?:1\d{8,9}|[23]\d{9}|7(?:[1345789]\d{8}|624\d{6})))$/)},"Please specify a valid uk phone number"),a.validator.addMethod("phoneUK",function(a,b){return a=a.replace(/\(|\)|\s+|-/g,""),this.optional(b)||a.length>9&&a.match(/^(?:(?:(?:00\s?|\+)44\s?)|(?:\(?0))(?:\d{2}\)?\s?\d{4}\s?\d{4}|\d{3}\)?\s?\d{3}\s?\d{3,4}|\d{4}\)?\s?(?:\d{5}|\d{3}\s?\d{3})|\d{5}\)?\s?\d{4,5})$/)},"Please specify a valid phone number"),a.validator.addMethod("phoneUS",function(a,b){return a=a.replace(/\s+/g,""),this.optional(b)||a.length>9&&a.match(/^(\+?1-?)?(\([2-9]([02-9]\d|1[02-9])\)|[2-9]([02-9]\d|1[02-9]))-?[2-9]([02-9]\d|1[02-9])-?\d{4}$/)},"Please specify a valid phone number"),a.validator.addMethod("postalcodeBR",function(a,b){return this.optional(b)||/^\d{2}.\d{3}-\d{3}?$|^\d{5}-?\d{3}?$/.test(a)},"Informe um CEP válido."),a.validator.addMethod("postalCodeCA",function(a,b){return this.optional(b)||/^[ABCEGHJKLMNPRSTVXY]\d[ABCEGHJKLMNPRSTVWXYZ] *\d[ABCEGHJKLMNPRSTVWXYZ]\d$/i.test(a)},"Please specify a valid postal code"),a.validator.addMethod("postalcodeIT",function(a,b){return this.optional(b)||/^\d{5}$/.test(a)},"Please specify a valid postal code"),a.validator.addMethod("postalcodeNL",function(a,b){return this.optional(b)||/^[1-9][0-9]{3}\s?[a-zA-Z]{2}$/.test(a)},"Please specify a valid postal code"),a.validator.addMethod("postcodeUK",function(a,b){return this.optional(b)||/^((([A-PR-UWYZ][0-9])|([A-PR-UWYZ][0-9][0-9])|([A-PR-UWYZ][A-HK-Y][0-9])|([A-PR-UWYZ][A-HK-Y][0-9][0-9])|([A-PR-UWYZ][0-9][A-HJKSTUW])|([A-PR-UWYZ][A-HK-Y][0-9][ABEHMNPRVWXY]))\s?([0-9][ABD-HJLNP-UW-Z]{2})|(GIR)\s?(0AA))$/i.test(a)},"Please specify a valid UK postcode"),a.validator.addMethod("require_from_group",function(b,c,d){var e=a(d[1],c.form),f=e.eq(0),g=f.data("valid_req_grp")?f.data("valid_req_grp"):a.extend({},this),h=e.filter(function(){return g.elementValue(this)}).length>=d[0];return f.data("valid_req_grp",g),a(c).data("being_validated")||(e.data("being_validated",!0),e.each(function(){g.element(this)}),e.data("being_validated",!1)),h},a.validator.format("Please fill at least {0} of these fields.")),a.validator.addMethod("skip_or_fill_minimum",function(b,c,d){var e=a(d[1],c.form),f=e.eq(0),g=f.data("valid_skip")?f.data("valid_skip"):a.extend({},this),h=e.filter(function(){return g.elementValue(this)}).length,i=0===h||h>=d[0];return f.data("valid_skip",g),a(c).data("being_validated")||(e.data("being_validated",!0),e.each(function(){g.element(this)}),e.data("being_validated",!1)),i},a.validator.format("Please either skip these fields or fill at least {0} of them.")),a.validator.addMethod("stateUS",function(a,b,c){var d,e="undefined"==typeof c,f=!e&&"undefined"!=typeof c.caseSensitive&&c.caseSensitive,g=!e&&"undefined"!=typeof c.includeTerritories&&c.includeTerritories,h=!e&&"undefined"!=typeof c.includeMilitary&&c.includeMilitary;return d=g||h?g&&h?"^(A[AEKLPRSZ]|C[AOT]|D[CE]|FL|G[AU]|HI|I[ADLN]|K[SY]|LA|M[ADEINOPST]|N[CDEHJMVY]|O[HKR]|P[AR]|RI|S[CD]|T[NX]|UT|V[AIT]|W[AIVY])$":g?"^(A[KLRSZ]|C[AOT]|D[CE]|FL|G[AU]|HI|I[ADLN]|K[SY]|LA|M[ADEINOPST]|N[CDEHJMVY]|O[HKR]|P[AR]|RI|S[CD]|T[NX]|UT|V[AIT]|W[AIVY])$":"^(A[AEKLPRZ]|C[AOT]|D[CE]|FL|GA|HI|I[ADLN]|K[SY]|LA|M[ADEINOST]|N[CDEHJMVY]|O[HKR]|PA|RI|S[CD]|T[NX]|UT|V[AT]|W[AIVY])$":"^(A[KLRZ]|C[AOT]|D[CE]|FL|GA|HI|I[ADLN]|K[SY]|LA|M[ADEINOST]|N[CDEHJMVY]|O[HKR]|PA|RI|S[CD]|T[NX]|UT|V[AT]|W[AIVY])$",d=f?new RegExp(d):new RegExp(d,"i"),this.optional(b)||d.test(a)},"Please specify a valid state"),a.validator.addMethod("strippedminlength",function(b,c,d){return a(b).text().length>=d},a.validator.format("Please enter at least {0} characters")),a.validator.addMethod("time",function(a,b){return this.optional(b)||/^([01]\d|2[0-3]|[0-9])(:[0-5]\d){1,2}$/.test(a)},"Please enter a valid time, between 00:00 and 23:59"),a.validator.addMethod("time12h",function(a,b){return this.optional(b)||/^((0?[1-9]|1[012])(:[0-5]\d){1,2}(\ ?[AP]M))$/i.test(a)},"Please enter a valid time in 12-hour am/pm format"),a.validator.addMethod("url2",function(a,b){return this.optional(b)||/^(https?|ftp):\/\/(((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:)*@)?(((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)*(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?)(:\d*)?)(\/((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)+(\/(([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)*)*)?)?(\?((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|[\uE000-\uF8FF]|\/|\?)*)?(#((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|\/|\?)*)?$/i.test(a)},a.validator.messages.url),a.validator.addMethod("vinUS",function(a){if(17!==a.length)return!1;var b,c,d,e,f,g,h=["A","B","C","D","E","F","G","H","J","K","L","M","N","P","R","S","T","U","V","W","X","Y","Z"],i=[1,2,3,4,5,6,7,8,1,2,3,4,5,7,9,2,3,4,5,6,7,8,9],j=[8,7,6,5,4,3,2,10,0,9,8,7,6,5,4,3,2],k=0;for(b=0;b<17;b++){if(e=j[b],d=a.slice(b,b+1),8===b&&(g=d),isNaN(d)){for(c=0;c?@\[\\\]^`{|}~])/g, "\\$1"); 38 | } 39 | 40 | function getModelPrefix(fieldName) { 41 | return fieldName.substr(0, fieldName.lastIndexOf(".") + 1); 42 | } 43 | 44 | function appendModelPrefix(value, prefix) { 45 | if (value.indexOf("*.") === 0) { 46 | value = value.replace("*.", prefix); 47 | } 48 | return value; 49 | } 50 | 51 | function onError(error, inputElement) { // 'this' is the form element 52 | var container = $(this).find("[data-valmsg-for='" + escapeAttributeValue(inputElement[0].name) + "']"), 53 | replaceAttrValue = container.attr("data-valmsg-replace"), 54 | replace = replaceAttrValue ? $.parseJSON(replaceAttrValue) !== false : null; 55 | 56 | container.removeClass("field-validation-valid").addClass("field-validation-error"); 57 | error.data("unobtrusiveContainer", container); 58 | 59 | if (replace) { 60 | container.empty(); 61 | error.removeClass("input-validation-error").appendTo(container); 62 | } 63 | else { 64 | error.hide(); 65 | } 66 | } 67 | 68 | function onErrors(event, validator) { // 'this' is the form element 69 | var container = $(this).find("[data-valmsg-summary=true]"), 70 | list = container.find("ul"); 71 | 72 | if (list && list.length && validator.errorList.length) { 73 | list.empty(); 74 | container.addClass("validation-summary-errors").removeClass("validation-summary-valid"); 75 | 76 | $.each(validator.errorList, function () { 77 | $("
  • ").html(this.message).appendTo(list); 78 | }); 79 | } 80 | } 81 | 82 | function onSuccess(error) { // 'this' is the form element 83 | var container = error.data("unobtrusiveContainer"); 84 | 85 | if (container) { 86 | var replaceAttrValue = container.attr("data-valmsg-replace"), 87 | replace = replaceAttrValue ? $.parseJSON(replaceAttrValue) : null; 88 | 89 | container.addClass("field-validation-valid").removeClass("field-validation-error"); 90 | error.removeData("unobtrusiveContainer"); 91 | 92 | if (replace) { 93 | container.empty(); 94 | } 95 | } 96 | } 97 | 98 | function onReset(event) { // 'this' is the form element 99 | var $form = $(this), 100 | key = '__jquery_unobtrusive_validation_form_reset'; 101 | if ($form.data(key)) { 102 | return; 103 | } 104 | // Set a flag that indicates we're currently resetting the form. 105 | $form.data(key, true); 106 | try { 107 | $form.data("validator").resetForm(); 108 | } finally { 109 | $form.removeData(key); 110 | } 111 | 112 | $form.find(".validation-summary-errors") 113 | .addClass("validation-summary-valid") 114 | .removeClass("validation-summary-errors"); 115 | $form.find(".field-validation-error") 116 | .addClass("field-validation-valid") 117 | .removeClass("field-validation-error") 118 | .removeData("unobtrusiveContainer") 119 | .find(">*") // If we were using valmsg-replace, get the underlying error 120 | .removeData("unobtrusiveContainer"); 121 | } 122 | 123 | function validationInfo(form) { 124 | var $form = $(form), 125 | result = $form.data(data_validation), 126 | onResetProxy = $.proxy(onReset, form), 127 | defaultOptions = $jQval.unobtrusive.options || {}, 128 | execInContext = function (name, args) { 129 | var func = defaultOptions[name]; 130 | func && $.isFunction(func) && func.apply(form, args); 131 | }; 132 | 133 | if (!result) { 134 | result = { 135 | options: { // options structure passed to jQuery Validate's validate() method 136 | errorClass: defaultOptions.errorClass || "input-validation-error", 137 | errorElement: defaultOptions.errorElement || "span", 138 | errorPlacement: function () { 139 | onError.apply(form, arguments); 140 | execInContext("errorPlacement", arguments); 141 | }, 142 | invalidHandler: function () { 143 | onErrors.apply(form, arguments); 144 | execInContext("invalidHandler", arguments); 145 | }, 146 | messages: {}, 147 | rules: {}, 148 | success: function () { 149 | onSuccess.apply(form, arguments); 150 | execInContext("success", arguments); 151 | } 152 | }, 153 | attachValidation: function () { 154 | $form 155 | .off("reset." + data_validation, onResetProxy) 156 | .on("reset." + data_validation, onResetProxy) 157 | .validate(this.options); 158 | }, 159 | validate: function () { // a validation function that is called by unobtrusive Ajax 160 | $form.validate(); 161 | return $form.valid(); 162 | } 163 | }; 164 | $form.data(data_validation, result); 165 | } 166 | 167 | return result; 168 | } 169 | 170 | $jQval.unobtrusive = { 171 | adapters: [], 172 | 173 | parseElement: function (element, skipAttach) { 174 | /// 175 | /// Parses a single HTML element for unobtrusive validation attributes. 176 | /// 177 | /// The HTML element to be parsed. 178 | /// [Optional] true to skip attaching the 179 | /// validation to the form. If parsing just this single element, you should specify true. 180 | /// If parsing several elements, you should specify false, and manually attach the validation 181 | /// to the form when you are finished. The default is false. 182 | var $element = $(element), 183 | form = $element.parents("form")[0], 184 | valInfo, rules, messages; 185 | 186 | if (!form) { // Cannot do client-side validation without a form 187 | return; 188 | } 189 | 190 | valInfo = validationInfo(form); 191 | valInfo.options.rules[element.name] = rules = {}; 192 | valInfo.options.messages[element.name] = messages = {}; 193 | 194 | $.each(this.adapters, function () { 195 | var prefix = "data-val-" + this.name, 196 | message = $element.attr(prefix), 197 | paramValues = {}; 198 | 199 | if (message !== undefined) { // Compare against undefined, because an empty message is legal (and falsy) 200 | prefix += "-"; 201 | 202 | $.each(this.params, function () { 203 | paramValues[this] = $element.attr(prefix + this); 204 | }); 205 | 206 | this.adapt({ 207 | element: element, 208 | form: form, 209 | message: message, 210 | params: paramValues, 211 | rules: rules, 212 | messages: messages 213 | }); 214 | } 215 | }); 216 | 217 | $.extend(rules, { "__dummy__": true }); 218 | 219 | if (!skipAttach) { 220 | valInfo.attachValidation(); 221 | } 222 | }, 223 | 224 | parse: function (selector) { 225 | /// 226 | /// Parses all the HTML elements in the specified selector. It looks for input elements decorated 227 | /// with the [data-val=true] attribute value and enables validation according to the data-val-* 228 | /// attribute values. 229 | /// 230 | /// Any valid jQuery selector. 231 | 232 | // $forms includes all forms in selector's DOM hierarchy (parent, children and self) that have at least one 233 | // element with data-val=true 234 | var $selector = $(selector), 235 | $forms = $selector.parents() 236 | .addBack() 237 | .filter("form") 238 | .add($selector.find("form")) 239 | .has("[data-val=true]"); 240 | 241 | $selector.find("[data-val=true]").each(function () { 242 | $jQval.unobtrusive.parseElement(this, true); 243 | }); 244 | 245 | $forms.each(function () { 246 | var info = validationInfo(this); 247 | if (info) { 248 | info.attachValidation(); 249 | } 250 | }); 251 | } 252 | }; 253 | 254 | adapters = $jQval.unobtrusive.adapters; 255 | 256 | adapters.add = function (adapterName, params, fn) { 257 | /// Adds a new adapter to convert unobtrusive HTML into a jQuery Validate validation. 258 | /// The name of the adapter to be added. This matches the name used 259 | /// in the data-val-nnnn HTML attribute (where nnnn is the adapter name). 260 | /// [Optional] An array of parameter names (strings) that will 261 | /// be extracted from the data-val-nnnn-mmmm HTML attributes (where nnnn is the adapter name, and 262 | /// mmmm is the parameter name). 263 | /// The function to call, which adapts the values from the HTML 264 | /// attributes into jQuery Validate rules and/or messages. 265 | /// 266 | if (!fn) { // Called with no params, just a function 267 | fn = params; 268 | params = []; 269 | } 270 | this.push({ name: adapterName, params: params, adapt: fn }); 271 | return this; 272 | }; 273 | 274 | adapters.addBool = function (adapterName, ruleName) { 275 | /// Adds a new adapter to convert unobtrusive HTML into a jQuery Validate validation, where 276 | /// the jQuery Validate validation rule has no parameter values. 277 | /// The name of the adapter to be added. This matches the name used 278 | /// in the data-val-nnnn HTML attribute (where nnnn is the adapter name). 279 | /// [Optional] The name of the jQuery Validate rule. If not provided, the value 280 | /// of adapterName will be used instead. 281 | /// 282 | return this.add(adapterName, function (options) { 283 | setValidationValues(options, ruleName || adapterName, true); 284 | }); 285 | }; 286 | 287 | adapters.addMinMax = function (adapterName, minRuleName, maxRuleName, minMaxRuleName, minAttribute, maxAttribute) { 288 | /// Adds a new adapter to convert unobtrusive HTML into a jQuery Validate validation, where 289 | /// the jQuery Validate validation has three potential rules (one for min-only, one for max-only, and 290 | /// one for min-and-max). The HTML parameters are expected to be named -min and -max. 291 | /// The name of the adapter to be added. This matches the name used 292 | /// in the data-val-nnnn HTML attribute (where nnnn is the adapter name). 293 | /// The name of the jQuery Validate rule to be used when you only 294 | /// have a minimum value. 295 | /// The name of the jQuery Validate rule to be used when you only 296 | /// have a maximum value. 297 | /// The name of the jQuery Validate rule to be used when you 298 | /// have both a minimum and maximum value. 299 | /// [Optional] The name of the HTML attribute that 300 | /// contains the minimum value. The default is "min". 301 | /// [Optional] The name of the HTML attribute that 302 | /// contains the maximum value. The default is "max". 303 | /// 304 | return this.add(adapterName, [minAttribute || "min", maxAttribute || "max"], function (options) { 305 | var min = options.params.min, 306 | max = options.params.max; 307 | 308 | if (min && max) { 309 | setValidationValues(options, minMaxRuleName, [min, max]); 310 | } 311 | else if (min) { 312 | setValidationValues(options, minRuleName, min); 313 | } 314 | else if (max) { 315 | setValidationValues(options, maxRuleName, max); 316 | } 317 | }); 318 | }; 319 | 320 | adapters.addSingleVal = function (adapterName, attribute, ruleName) { 321 | /// Adds a new adapter to convert unobtrusive HTML into a jQuery Validate validation, where 322 | /// the jQuery Validate validation rule has a single value. 323 | /// The name of the adapter to be added. This matches the name used 324 | /// in the data-val-nnnn HTML attribute(where nnnn is the adapter name). 325 | /// [Optional] The name of the HTML attribute that contains the value. 326 | /// The default is "val". 327 | /// [Optional] The name of the jQuery Validate rule. If not provided, the value 328 | /// of adapterName will be used instead. 329 | /// 330 | return this.add(adapterName, [attribute || "val"], function (options) { 331 | setValidationValues(options, ruleName || adapterName, options.params[attribute]); 332 | }); 333 | }; 334 | 335 | $jQval.addMethod("__dummy__", function (value, element, params) { 336 | return true; 337 | }); 338 | 339 | $jQval.addMethod("regex", function (value, element, params) { 340 | var match; 341 | if (this.optional(element)) { 342 | return true; 343 | } 344 | 345 | match = new RegExp(params).exec(value); 346 | return (match && (match.index === 0) && (match[0].length === value.length)); 347 | }); 348 | 349 | $jQval.addMethod("nonalphamin", function (value, element, nonalphamin) { 350 | var match; 351 | if (nonalphamin) { 352 | match = value.match(/\W/g); 353 | match = match && match.length >= nonalphamin; 354 | } 355 | return match; 356 | }); 357 | 358 | if ($jQval.methods.extension) { 359 | adapters.addSingleVal("accept", "mimtype"); 360 | adapters.addSingleVal("extension", "extension"); 361 | } else { 362 | // for backward compatibility, when the 'extension' validation method does not exist, such as with versions 363 | // of JQuery Validation plugin prior to 1.10, we should use the 'accept' method for 364 | // validating the extension, and ignore mime-type validations as they are not supported. 365 | adapters.addSingleVal("extension", "extension", "accept"); 366 | } 367 | 368 | adapters.addSingleVal("regex", "pattern"); 369 | adapters.addBool("creditcard").addBool("date").addBool("digits").addBool("email").addBool("number").addBool("url"); 370 | adapters.addMinMax("length", "minlength", "maxlength", "rangelength").addMinMax("range", "min", "max", "range"); 371 | adapters.addMinMax("minlength", "minlength").addMinMax("maxlength", "minlength", "maxlength"); 372 | adapters.add("equalto", ["other"], function (options) { 373 | var prefix = getModelPrefix(options.element.name), 374 | other = options.params.other, 375 | fullOtherName = appendModelPrefix(other, prefix), 376 | element = $(options.form).find(":input").filter("[name='" + escapeAttributeValue(fullOtherName) + "']")[0]; 377 | 378 | setValidationValues(options, "equalTo", element); 379 | }); 380 | adapters.add("required", function (options) { 381 | // jQuery Validate equates "required" with "mandatory" for checkbox elements 382 | if (options.element.tagName.toUpperCase() !== "INPUT" || options.element.type.toUpperCase() !== "CHECKBOX") { 383 | setValidationValues(options, "required", true); 384 | } 385 | }); 386 | adapters.add("remote", ["url", "type", "additionalfields"], function (options) { 387 | var value = { 388 | url: options.params.url, 389 | type: options.params.type || "GET", 390 | data: {} 391 | }, 392 | prefix = getModelPrefix(options.element.name); 393 | 394 | $.each(splitAndTrim(options.params.additionalfields || options.element.name), function (i, fieldName) { 395 | var paramName = appendModelPrefix(fieldName, prefix); 396 | value.data[paramName] = function () { 397 | var field = $(options.form).find(":input").filter("[name='" + escapeAttributeValue(paramName) + "']"); 398 | // For checkboxes and radio buttons, only pick up values from checked fields. 399 | if (field.is(":checkbox")) { 400 | return field.filter(":checked").val() || field.filter(":hidden").val() || ''; 401 | } 402 | else if (field.is(":radio")) { 403 | return field.filter(":checked").val() || ''; 404 | } 405 | return field.val(); 406 | }; 407 | }); 408 | 409 | setValidationValues(options, "remote", value); 410 | }); 411 | adapters.add("password", ["min", "nonalphamin", "regex"], function (options) { 412 | if (options.params.min) { 413 | setValidationValues(options, "minlength", options.params.min); 414 | } 415 | if (options.params.nonalphamin) { 416 | setValidationValues(options, "nonalphamin", options.params.nonalphamin); 417 | } 418 | if (options.params.regex) { 419 | setValidationValues(options, "regex", options.params.regex); 420 | } 421 | }); 422 | adapters.add("fileextensions", ["extensions"], function (options) { 423 | setValidationValues(options, "extension", options.params.extensions); 424 | }); 425 | 426 | $(function () { 427 | $jQval.unobtrusive.parse(document); 428 | }); 429 | 430 | return $jQval.unobtrusive; 431 | })); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Training-Azure-DevOps 2 | This lab/walkthrough shows how to create a build and release pipeline that demostrates: 3 | - Infrastructure as Code 4 | - Deploys an App Service 5 | - Deploys a Web App 6 | - Deploys a Function App (consumption plan using v2) 7 | - Creates a build defination with CI 8 | - Builds code 9 | - Zips an Azure Function 10 | - Publishes code 11 | - Create a release defination with CD 12 | - Deploys an ARM template 13 | - Uses Variables 14 | - Deploys a web app 15 | - Swap web app slots 16 | - Uses approvals 17 | - Uses conditions 18 | - Uses cloning to quickly duplicate environments 19 | - Uses gates 20 | - Create a build defination for a Docker Image 21 | - Creates an Azure Container Registry 22 | - Builds the Docker image 23 | - Pushs the Docker image to an Azure Container Registry 24 | - Create a release defination for a Docker Image 25 | - Creates a Linux App Service 26 | - Creates a Linux Web App 27 | - Releases the Docker image from the ACR 28 | 29 | 30 | # Setup 31 | ### Azure Setup 32 | 1. Create a resource group in Azure named: Training-Azure-DevOps 33 | 2. Create a resource group in Azure named: Training-Azure-DevOps-Docker 34 | 2. Create a service principle in Azure named (web/api): Training-Azure-DevOps-SP 35 | 3. Grant the service principle Contributor access to the resource groups 36 | 37 | 38 | ### Azure DevOps Setup 39 | 1. Create an Azure DevOps project named: Training-Azure-DevOps 40 | 41 | 42 | # Web App Build and Release 43 | ### Open Azure DevOps 44 | 1. Click on Repos | Files 45 | 2. Click on Import under "or import a Repository" 46 | 3. Enter: https://github.com/AdamPaternostro/Training-Azure-DevOps.git 47 | 48 | 49 | ### Create a build defination 50 | We will be using the visual interface to create a build defination for a .NET core app 51 | 1. Click on Pipelines | Builds 52 | 2. Click on New Pipeline button 53 | 3. Click on the small link "Use the visual designer" 54 | 4. Click on Azure Repos Git (should all be filled out) | Click Continue 55 | 5. Select template "ASP.NET Core" 56 | 6. Click Save & Queue (make sure it works) 57 | 7. Click on the Artifacts button. You should see a zip. 58 | 59 | 60 | ### Alter build defination 61 | We will added additional files to our artifact store in Azure DevOps. These files can be scripts, resources or anything you need when creating your release pipeline. 62 | 1. Add a Copy Files task (click +) on the Agent 63 | 2. Move it above the Publish Artifact task 64 | 2. Set the Source folder: SampleWebApp-ARM-Templates 65 | 3. Set the Target folder: $(Build.ArtifactStagingDirectory) 66 | 4. Build again 67 | 5. View artifacts and you should see arm-template.json in the artifacts 68 | 69 | 70 | ### Create a Release pipeline (Deploy ARM Template) 71 | We not will create Azure resources (Infrastucture as Code). This will create an App Service Plan, an App Service (your main Web App) and a slot (Web App). 72 | 1. Click on Pipelines | Releases | New Pipeline button 73 | 2. Click on Empty Job link (where it says Select a template) 74 | 3. Rename Stage 1 to Dev and click the X to close 75 | 4. Click on Add an Artifact 76 | 5. Select your build 77 | 6. Select Default version: Latest 78 | 7. Click on Variables at the top 79 | 8. Add variables 80 | - Environment -> DEV (Note if you are working with several people all sharing a resource group them make this DEV-{yourname}) 81 | - AppServicePlan -> TrainingAzureDevOpsAppServicePlan (you can specify what you like - must be globally unique in Azure) 82 | - WebApp -> TrainingAzureDevOpsWebApp (you can specify what you like - must be globally unique in Azure) 83 | - ResourceGroup -> Training-Azure-DevOps 84 | 9. Click on "New release pipeline" and change the name to something you like 85 | 10. Click Save 86 | 11. Click on Dev Stage 87 | 12. Add a task (click +) 88 | 13. Select "Azure Resource Group Deployment" 89 | 14. Click on the task 90 | - Click the manage link next to "Azure Subscription" 91 | - Click on New Service Connection 92 | - Click on Azure Resource Manager 93 | - Click on "Use the full version of the service connection dialog" (link at bottom) 94 | - Give it a name 95 | - Enter the Service Principle Id (Application Id) 96 | - Enter the Service Principle Key 97 | - Test the connection 98 | - If you get an error make sure the service principle exists and has Contributor access to your resource groups 99 | - Close the tab and return to your pipeline 100 | - Click the refresh button next to Azure Subscription 101 | - Select the connection you just setup 102 | - Enter the resource group name: $(ResourceGroup) 103 | - Pick your location (I keep mine the same as my resource group) 104 | - You should make Location a variable and a parameter to your ARM template. This is just "simplified" for the training. 105 | - Pick your template "arm-template.json" 106 | - Leave parameters blank, we do not have a parameters file 107 | - In "Override template parameters" enter: 108 | ``` 109 | -serverfarms_TrainingAzureDevOpsAppServicePlan_name $(AppServicePlan)-$(Environment) -sites_TrainingAzureDevOpsWebApp_name $(WebApp)-$(Environment) 110 | ``` 111 | - Leave Deployment Mode as Incremental (this is very important as Complete will remove unused resources which can do things like delete a storage account) 112 | 15. Save 113 | 16. Click on Release | Create a Release 114 | - You can click the Release-1 link to view the release (click on logs to see the progress) 115 | - You can view the resources being created in the Azure Portal 116 | 117 | 118 | ### Release pipeline (Deploy Code) 119 | Now we want to deploy code (to the production slot). We will deploy our sample web application and set an application setting that can be accessed as an environment variable. 120 | 1. Edit the release pipeline 121 | 2. Add a task to the Dev stage "Azure App Service Deploy" 122 | 3. Edit the settings 123 | - Set the subscription 124 | - Set App Service name to $(WebApp)-$(Environment) 125 | - Set the Package / Folder to the SampleWebApp.zip file (use the selector) 126 | - Set the App Settings to "-Environment $(Environment)" under Application and Configuration Settings 127 | 4. Disable the ARM deployment task (right click and disable). This is a time saver during development. 128 | 5. Save and Run a release 129 | 6. After the release open the website 130 | - e.g. https://trainingazuredevopswebapp-dev.azurewebsites.net 131 | - Note: The staging slot will be empty at this point. 132 | 133 | 134 | ### Release pipeline (Deploy to slot) 135 | You never want to deploy to your production slot. You should always deploy to a staging slot, warm up your app and then swap the application into the production slot. 136 | 1. Edit the release pipeline 137 | 2. Edit the Web App Deployment 138 | 3. Click Deploy to Slot 139 | - Set resource group: $(ResourceGroup) 140 | - Set Slot to: Preprod 141 | 4. Save and Run a release 142 | 5. After the release open the websites 143 | - e.g. https://trainingazuredevopswebapp-dev.azurewebsites.net/ 144 | - e.g. https://trainingazuredevopswebapp-dev-PREPROD.azurewebsites.net/ 145 | 6. You will notice the ServerName is the same (both apps are on the same server) 146 | 147 | #### Notes (VERY IMPORTANT!) 148 | You will have ERRORS if you do not do this! Your errors will happen when you swap your slots AND when you autoscale! It all really depends on how long your application takes to warm-up. If your application take 1 to 15 seconds to warm-up then you are probably fine. If your application takes 15 seconds to 15 minutes (yes, some apps take 15 minutes), then you need to properly warm-up your application. 149 | - To warm up a Windows App Service: https://docs.microsoft.com/en-us/azure/app-service/deploy-staging-slots#custom-warm-up 150 | - To warm up a Linux App Service: https://github.com/AdamPaternostro/Azure-Web-App-Nginx-Reverse-Proxy-Dotnet-Core-Linux 151 | 152 | 153 | ### Release pipeline (Swap slot) 154 | We will now take the code in our preprod slot and swap to the production slot. 155 | 1. Edit the release pipeline 156 | 2. Add a task to the Dev stage "Azure App Service Manage" 157 | 3. Edit the settings 158 | - Set the subscription 159 | - Set the App Service name: $(WebApp)-$(Environment) 160 | - Set the Resource Group: $(ResourceGroup) 161 | - Set the Source Slot: Preprod 162 | 4. Save and Run a release 163 | 164 | ### Create QA Release 165 | Once you have your Dev deployment up and running you can quickly create new environments by cloning your stage and using scoped variables. 166 | 1. Edit the release pipeline 167 | 2. Edit variables 168 | 3. Change Environment variable Scope to DEV (just the one variable) 169 | 4. Click on Pipeline 170 | 5. Clone the Dev Stage 171 | 6. Click on new Stage and rename to QA 172 | 7. Click on variables, you will see Environment twice (once for each stage) 173 | 8. Change the QA scope to "QA" for the value 174 | 9. Click on the QA stage and re-enable the ARM task 175 | 10. Save and Run a release 176 | 177 | 178 | ### Create Production Release 179 | Now create a production environment, but we do not want to swap slots automatically. You typically will push your code to the production staging slot and then smoke test with some business users. They will be hitting production data, so perform some read only tasks to make sure things are working. 180 | 1. Edit the release pipeline 181 | 2. Clone QA 182 | 3. Rename to Prod 183 | 4. Edit variables 184 | 5. In QA disable the ARM task 185 | 6. Clone Prod 186 | 7. Rename cloned Stage to Prod-Swap and Edit 187 | 8. Delete/Remove the ARM task from Prod-Swap Stage 188 | 9. Delete/Remove the Web Deploy task from Prod-Swap Stage 189 | 10. Edit the Prod stage 190 | 11. Delete/Remove the Swap Slots task (we want this task just in the Prod-Swap stage) 191 | 12. Save and Run a release 192 | 193 | ### Add conditions 194 | 1. Added a variable called DeployARMTemplate and set the value to "false". Leave the scope set to "Release" which means the value applies to all stages. 195 | 2. For each stage 196 | - Enable the ARM template 197 | - Edit the step 198 | - Under Control Options 199 | - Set Run this task to: Custom Conditions 200 | - Set Custom condition: eq(variables['DeployARMTemplate'], 'true') 201 | 3. Save and Run a release 202 | - You should see the ARM template skipped 203 | 204 | ### Add approvals 205 | 1. Edit the release pipeline 206 | 2. Click on the person icon on the right side of the Dev stage 207 | 3. Enable "Post-deployment approvals" 208 | 4. Enter your name 209 | 5. Click on the person icon on the right side of the QA stage 210 | 6. Enable "Post-deployment approvals" 211 | 7. Enter your name 212 | 8. Save and Run a release 213 | You can approve on https://dev.azure.com or click on the email 214 | 215 | #### Note 216 | You can add a stage that runs after the Prod Swap. On the prod swap add a post approval and if the approval is rejected, we want to do another slot swap to put back the old code. 217 | 218 | ### Change the code and enable automatic builds/releases 219 | 1. Edit the Build defination 220 | 2. Click on Triggers 221 | 3. Click the "Enable continuous integration" checkbox 222 | 4. Save the Build (do not Save and queue) 223 | 5. Edit the Release pipeline 224 | 6. Click on the Lightning bolt icon on the Artifacts 225 | 7. Enable "Continuous deployment trigger" 226 | 8. Save the Release 227 | 9. Click on Repo | Files 228 | 10. Edit the file SampleWebApp/Views/Home/Index.cshtml 229 | 11. At the bottom enter 230 | ``` 231 |
    232 |
    233 | New Release 234 |
    235 |
    236 | ``` 237 | 12. Save the file 238 | - The build should kick off automatically (click on bulids) 239 | - The release should kick off automatically (click on releases when build is done) 240 | - You can verify the new code is moving through environments by viewing the websites (the prod slot will have the changes, the old site will be in the preprod slot) 241 | 242 | ### Implement a Gate 243 | Gates allow you to apply custom business logic to your release pipeline. You can test current metrics to see if a site is too busy to deploy and query a REST endpoints to check on conditions that need to be tested. 244 | 1. Edit the Build 245 | 2. Clone the Copy Files task SampleWebApp-ARM-Templates 246 | 3. Edit the new task and change the folder to AzureFunction-ARM-Templates 247 | 4. Add a new task Archive Files 248 | - Root Folder: AzureFunction-Code 249 | - Uncheck "Prepend root folder name to archive paths" 250 | - Archive File to Create: $(Build.ArtifactStagingDirectory)/AzureFunction.zip 251 | 5. Save and Build 252 | 6. Edit the Prod stage in the Release pipeline 253 | 7. Clone the ARM task 254 | - Edit Template path (e.g. $(System.DefaultWorkingDirectory)/_Training-Azure-DevOps-Build/drop/arm-function-template.json) 255 | - Edit the Override template parameters 256 | - NOTE: You might need to change the storage account name (functionappstorage001) to be unique 257 | ``` 258 | -AzureFunctionPlanName $(AppServicePlan)-Function-$(Environment) -AzureFunctionAppName $(WebApp)-Function-$(Environment) -AzureFunctionStorageAccountName functionappstorage001 259 | ``` 260 | 8. Clone the App Service Deploy task 261 | - Change App Type to Function App 262 | - Uncheck deploy to slot 263 | - Change the App Service Name: $(WebApp)-Function-$(Environment) 264 | - Change the Package / Folder path (e.g. $(System.DefaultWorkingDirectory)/_Training-Azure-DevOps-Build/drop/AzureFunction.zip) 265 | - Under Additional Deployment Options check off "Publish using Web Deploy" 266 | - Check off Remove additional files at destination 267 | 9. Change the variable DeployARMTemplate to "true" (since we have a new ARM template). 268 | - You can disable the approvals as well to save time. 269 | 10. Save and run to make sure things are working, you should also check to make sure the Function App was deployed 270 | 11. Change the variable DeployARMTemplate to "false" if things worked 271 | 12. Go to the Function App in the Azure Portal and copy the URL / security code 272 | 13. Click on the Lightning bolt of the Prod Swap stage 273 | - Enable a gate 274 | - In the Azure Portal go to your fucntion app, you need to copy the URL for it 275 | - Enter the function url (e.g. https://trainingazuredevopswebapp-function-prod.azurewebsites.net/api/AzureDevOpsFunctionGate) 276 | - Enter the code (e.g. kw99xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx=) 277 | - Under advanced for Success Criteria enter "eq(root['status'], 'true')" 278 | - NOTE: If you have an apporval you can check the box "On successfuly gates, ask for approvals", this means your gate will be evaluated first and must be successful before asking for an approval 279 | - You can change the evaluation times as well for testing 280 | 14. Save and Run a release 281 | - You can watch the gate perform its test 282 | 283 | You can try additional releases and change the Azure Functions return value. You can unlock the function under Function App Settings | Function app edit mode. 284 | 285 | ## Notes 286 | - If your web app gets a DLL locked, then add a task (Azure App Service Manage) to restart the staging slot web app 287 | 288 | - Some customers like to tear down the staging slot after deployments. You can add this part of your pipeline. I usually wait 1 to 2 hours before tearing down staging since if something goes wrong with the release your can quick swap slots. You can add a step to perform the delete after a certain amount of time (e.g. Gate or an appoval with a deferred time). Instead of "deleting" you staging slow, you can deploy an index.html file that is just empty and for Azure Functions you can have an empty function. 289 | 290 | - My preprod staging slot is pointing to production resources and production databases 291 | 292 | - If you are using Web Apps and use Slot specific variables be-aware that after your slot swap the appliction must recycle to load the values! This negates most of the benefits of slots. If your application has a quick warm up time for the first call, you might be alright, but if not, your users can see a delay. 293 | 294 | - If your web app is event based, for instance an Azure Function that triggers based upon a blob, then when you deploy to a the preprod staging slot, this function is processing production data! 295 | - How do you handle this? For a blob trigger, if your preprod slot picks up the blob, it is not like it can pass the event to the production slot. You also cannot have the production slot monitoring one container and preprod monitoring a different one. 296 | - My perference for all event driven logic, have an Azure function that places items in a queue (or just place the item in the queue to begin with). Then process the queue only if you are in the production slot. You can test the URL of the application in some cases (not if you are directing 90% of your traffic the production slot and 10% to staging). Having an application configuration value will not help since when you swap slots the value will not change. I worry about missing events - especailly when new code is being deployed for an event based process. 297 | 298 | # Docker Build and Release 299 | 300 | ### Clean Up 301 | 1. Disable automatitic builds for pipeline. Since we are using a single repo for this training we want to stop automatic builds. 302 | 303 | 304 | ### Create a build pipeline 305 | This will create an Azure Container Registry, build a Docker image, push the Docker image and copy some template files. We are deploying an ARM template as part of a Build pipeline since our Docker tasks need this resource. 306 | 1. Create a new Build Defination (using the visual designer) and select "Docker Container" template 307 | 2. Add variables 308 | - ACR_Name -> TrainingAzureDevOpsContainerReg 309 | - AzureContainerRegistryConnection -> {"loginServer":"trainingazuredevopscontainerregdev.azurecr.io", "id" : "/subscriptions/{REPLACE ME SUBSCRIPTION GUID}/resourceGroups/Training-Azure-DevOps-Docker/providers/Microsoft.ContainerRegistry/registries/trainingazuredevopscontainerregdev"} 310 | - NOTE: This string has the world "dev" as a suffix. The string is $(ACR_Name)$(Environment) 311 | - Environment -> DEV (Note if you are working with several people all sharing a resource group them make this DEV-{yourname}) 312 | - ResourceGroup -> Training-Azure-DevOps-Docker 313 | 3. Add a task Azure Resource Group Deployment (move to the top of the task list) 314 | - Set the Azure subscription 315 | - Set the Resource Group: $(ResourceGroup) 316 | - Set the Location: East US 317 | - Set the ARM template: (e.g. Sample-Docker-ARM-Templates/arm-template-acr.json) 318 | - Set the Override template parameters: 319 | ``` 320 | -ACR_Name $(ACR_Name)$(Environment) 321 | ``` 322 | 4. Click on Build Docker task 323 | - Set the Azure subscription 324 | - Set the Azure Container Registry: $(AzureContainerRegistryConnection) 325 | 5. Click on Push Docker image task 326 | - Set the Azure subscription 327 | - Set the Azure Container Registry: $(AzureContainerRegistryConnection) 328 | 5. Add a Copy File task (at the bottom) 329 | - Set the Source folder: Sample-Docker-ARM-Templates 330 | - Set the Target folder: $(Build.ArtifactStagingDirectory) 331 | 5. Add a Publish Build Artifacts task 332 | 6. Save and Queue 333 | 334 | Check you build artifacts and make sure the Azure Container Registry was created. We need to create this as part of the build (versus release). You can view the image inthe repo via the portal. Note the image name, it will match your Azure DevOps repo. Also, your ACR name has been set to lower case. Mixed cases is not always supported with Docker. 335 | 336 | 337 | ### Create a Release pipeline 338 | 1. Create a new release pipeline 339 | 2. Select Empty job 340 | 3. Link your Artifacts 341 | 4. Add variables 342 | - ACR_Name -> TrainingAzureDevOpsContainerReg 343 | - ACR_Password -> Get from Azure Portal 344 | - AppServicePlan -> TrainingAzureDevOpsLinuxPlan (you can specify what you like - must be globally unique in Azure) 345 | - Environment -> DEV (Note if you are working with several people all sharing a resource group them make this DEV-{yourname}) 346 | - ResourceGroup -> Training-Azure-DevOps-Docker 347 | - WebApp -> TrainingAzureDevOpsLinuxApp (you can specify what you like - must be globally unique in Azure) 348 | 5. Add a task Azure Resource Group Deployment 349 | - Set the Azure subscription 350 | - Set the Resource group: $(ResourceGroup) 351 | - Set Location: "East US" 352 | - Set the Template location: $(System.DefaultWorkingDirectory)/_Training-Azure-DevOps-Docker/drop/arm-template-web.json 353 | - Set Override template parameters 354 | ``` 355 | -sites_azuretrainingdockerimageapp_name $(WebApp)-$(Environment) -serverfarms_azuretrainingdockerimageappservice_name $(AppServicePlan)-$(Environment) -ACR_Name $(ACR_Name)$(Environment) -ACR_Password $(ACR_Password) -DockerImageName $(Build.Repository.Name) -DockerTag $(Build.BuildId) 356 | ``` 357 | 6. Save and then Queue 358 | 359 | Verify that the website is working. If you get done the training early, then enhance this with QA and Prod stages. Review the Dockerfile. Note that it is totally self contained. If you use Docker, some customers bulid/compile their code within their Docker. They 360 | 361 | ### Notes 362 | - You have to create your ACR before building/pushing our Docker image. This is why the ACR ARM template is in the Build pipeline. I consider this a "build" resource which is why it is created here. Typically, I place all my ARM templates in my Release pipeline. 363 | - For best Docker build performance use your own agent so you do not need to rebuild every layer (which will happen on a hosted agent) 364 | - You might have noticed we are getting the ACR password by hand. A better apporach would be to save the password to Azure KeyVault when creating the ACR. Then reference the KeyVault value with a Variable Group. 365 | - When creating Docker image try to keep your layers < 100 MB. The layers are stored in Azure Blob storage and what you want is many layers being pulled in parallel. Having a 1 GB layer will have issues if 10 web servers are trying to pull at the same time. 366 | - If you notice we are not tagging the Docker image with Latest. We are using specific build numbers. This helps in Rollback and a lot of people just pull latest, but if are using Slots then this can cause confusion. 367 | 368 | 369 | # DevOps Best Practices 370 | https://github.com/AdamPaternostro/DevOps-Best-Practices 371 | -------------------------------------------------------------------------------- /SampleWebApp/wwwroot/lib/jquery-validation/dist/jquery.validate.min.js: -------------------------------------------------------------------------------- 1 | /*! jQuery Validation Plugin - v1.17.0 - 7/29/2017 2 | * https://jqueryvalidation.org/ 3 | * Copyright (c) 2017 Jörn Zaefferer; Licensed MIT */ 4 | !function(a){"function"==typeof define&&define.amd?define(["jquery"],a):"object"==typeof module&&module.exports?module.exports=a(require("jquery")):a(jQuery)}(function(a){a.extend(a.fn,{validate:function(b){if(!this.length)return void(b&&b.debug&&window.console&&console.warn("Nothing selected, can't validate, returning nothing."));var c=a.data(this[0],"validator");return c?c:(this.attr("novalidate","novalidate"),c=new a.validator(b,this[0]),a.data(this[0],"validator",c),c.settings.onsubmit&&(this.on("click.validate",":submit",function(b){c.submitButton=b.currentTarget,a(this).hasClass("cancel")&&(c.cancelSubmit=!0),void 0!==a(this).attr("formnovalidate")&&(c.cancelSubmit=!0)}),this.on("submit.validate",function(b){function d(){var d,e;return c.submitButton&&(c.settings.submitHandler||c.formSubmitted)&&(d=a("").attr("name",c.submitButton.name).val(a(c.submitButton).val()).appendTo(c.currentForm)),!c.settings.submitHandler||(e=c.settings.submitHandler.call(c,c.currentForm,b),d&&d.remove(),void 0!==e&&e)}return c.settings.debug&&b.preventDefault(),c.cancelSubmit?(c.cancelSubmit=!1,d()):c.form()?c.pendingRequest?(c.formSubmitted=!0,!1):d():(c.focusInvalid(),!1)})),c)},valid:function(){var b,c,d;return a(this[0]).is("form")?b=this.validate().form():(d=[],b=!0,c=a(this[0].form).validate(),this.each(function(){b=c.element(this)&&b,b||(d=d.concat(c.errorList))}),c.errorList=d),b},rules:function(b,c){var d,e,f,g,h,i,j=this[0];if(null!=j&&(!j.form&&j.hasAttribute("contenteditable")&&(j.form=this.closest("form")[0],j.name=this.attr("name")),null!=j.form)){if(b)switch(d=a.data(j.form,"validator").settings,e=d.rules,f=a.validator.staticRules(j),b){case"add":a.extend(f,a.validator.normalizeRule(c)),delete f.messages,e[j.name]=f,c.messages&&(d.messages[j.name]=a.extend(d.messages[j.name],c.messages));break;case"remove":return c?(i={},a.each(c.split(/\s/),function(a,b){i[b]=f[b],delete f[b]}),i):(delete e[j.name],f)}return g=a.validator.normalizeRules(a.extend({},a.validator.classRules(j),a.validator.attributeRules(j),a.validator.dataRules(j),a.validator.staticRules(j)),j),g.required&&(h=g.required,delete g.required,g=a.extend({required:h},g)),g.remote&&(h=g.remote,delete g.remote,g=a.extend(g,{remote:h})),g}}}),a.extend(a.expr.pseudos||a.expr[":"],{blank:function(b){return!a.trim(""+a(b).val())},filled:function(b){var c=a(b).val();return null!==c&&!!a.trim(""+c)},unchecked:function(b){return!a(b).prop("checked")}}),a.validator=function(b,c){this.settings=a.extend(!0,{},a.validator.defaults,b),this.currentForm=c,this.init()},a.validator.format=function(b,c){return 1===arguments.length?function(){var c=a.makeArray(arguments);return c.unshift(b),a.validator.format.apply(this,c)}:void 0===c?b:(arguments.length>2&&c.constructor!==Array&&(c=a.makeArray(arguments).slice(1)),c.constructor!==Array&&(c=[c]),a.each(c,function(a,c){b=b.replace(new RegExp("\\{"+a+"\\}","g"),function(){return c})}),b)},a.extend(a.validator,{defaults:{messages:{},groups:{},rules:{},errorClass:"error",pendingClass:"pending",validClass:"valid",errorElement:"label",focusCleanup:!1,focusInvalid:!0,errorContainer:a([]),errorLabelContainer:a([]),onsubmit:!0,ignore:":hidden",ignoreTitle:!1,onfocusin:function(a){this.lastActive=a,this.settings.focusCleanup&&(this.settings.unhighlight&&this.settings.unhighlight.call(this,a,this.settings.errorClass,this.settings.validClass),this.hideThese(this.errorsFor(a)))},onfocusout:function(a){this.checkable(a)||!(a.name in this.submitted)&&this.optional(a)||this.element(a)},onkeyup:function(b,c){var d=[16,17,18,20,35,36,37,38,39,40,45,144,225];9===c.which&&""===this.elementValue(b)||a.inArray(c.keyCode,d)!==-1||(b.name in this.submitted||b.name in this.invalid)&&this.element(b)},onclick:function(a){a.name in this.submitted?this.element(a):a.parentNode.name in this.submitted&&this.element(a.parentNode)},highlight:function(b,c,d){"radio"===b.type?this.findByName(b.name).addClass(c).removeClass(d):a(b).addClass(c).removeClass(d)},unhighlight:function(b,c,d){"radio"===b.type?this.findByName(b.name).removeClass(c).addClass(d):a(b).removeClass(c).addClass(d)}},setDefaults:function(b){a.extend(a.validator.defaults,b)},messages:{required:"This field is required.",remote:"Please fix this field.",email:"Please enter a valid email address.",url:"Please enter a valid URL.",date:"Please enter a valid date.",dateISO:"Please enter a valid date (ISO).",number:"Please enter a valid number.",digits:"Please enter only digits.",equalTo:"Please enter the same value again.",maxlength:a.validator.format("Please enter no more than {0} characters."),minlength:a.validator.format("Please enter at least {0} characters."),rangelength:a.validator.format("Please enter a value between {0} and {1} characters long."),range:a.validator.format("Please enter a value between {0} and {1}."),max:a.validator.format("Please enter a value less than or equal to {0}."),min:a.validator.format("Please enter a value greater than or equal to {0}."),step:a.validator.format("Please enter a multiple of {0}.")},autoCreateRanges:!1,prototype:{init:function(){function b(b){!this.form&&this.hasAttribute("contenteditable")&&(this.form=a(this).closest("form")[0],this.name=a(this).attr("name"));var c=a.data(this.form,"validator"),d="on"+b.type.replace(/^validate/,""),e=c.settings;e[d]&&!a(this).is(e.ignore)&&e[d].call(c,this,b)}this.labelContainer=a(this.settings.errorLabelContainer),this.errorContext=this.labelContainer.length&&this.labelContainer||a(this.currentForm),this.containers=a(this.settings.errorContainer).add(this.settings.errorLabelContainer),this.submitted={},this.valueCache={},this.pendingRequest=0,this.pending={},this.invalid={},this.reset();var c,d=this.groups={};a.each(this.settings.groups,function(b,c){"string"==typeof c&&(c=c.split(/\s/)),a.each(c,function(a,c){d[c]=b})}),c=this.settings.rules,a.each(c,function(b,d){c[b]=a.validator.normalizeRule(d)}),a(this.currentForm).on("focusin.validate focusout.validate keyup.validate",":text, [type='password'], [type='file'], select, textarea, [type='number'], [type='search'], [type='tel'], [type='url'], [type='email'], [type='datetime'], [type='date'], [type='month'], [type='week'], [type='time'], [type='datetime-local'], [type='range'], [type='color'], [type='radio'], [type='checkbox'], [contenteditable], [type='button']",b).on("click.validate","select, option, [type='radio'], [type='checkbox']",b),this.settings.invalidHandler&&a(this.currentForm).on("invalid-form.validate",this.settings.invalidHandler)},form:function(){return this.checkForm(),a.extend(this.submitted,this.errorMap),this.invalid=a.extend({},this.errorMap),this.valid()||a(this.currentForm).triggerHandler("invalid-form",[this]),this.showErrors(),this.valid()},checkForm:function(){this.prepareForm();for(var a=0,b=this.currentElements=this.elements();b[a];a++)this.check(b[a]);return this.valid()},element:function(b){var c,d,e=this.clean(b),f=this.validationTargetFor(e),g=this,h=!0;return void 0===f?delete this.invalid[e.name]:(this.prepareElement(f),this.currentElements=a(f),d=this.groups[f.name],d&&a.each(this.groups,function(a,b){b===d&&a!==f.name&&(e=g.validationTargetFor(g.clean(g.findByName(a))),e&&e.name in g.invalid&&(g.currentElements.push(e),h=g.check(e)&&h))}),c=this.check(f)!==!1,h=h&&c,c?this.invalid[f.name]=!1:this.invalid[f.name]=!0,this.numberOfInvalids()||(this.toHide=this.toHide.add(this.containers)),this.showErrors(),a(b).attr("aria-invalid",!c)),h},showErrors:function(b){if(b){var c=this;a.extend(this.errorMap,b),this.errorList=a.map(this.errorMap,function(a,b){return{message:a,element:c.findByName(b)[0]}}),this.successList=a.grep(this.successList,function(a){return!(a.name in b)})}this.settings.showErrors?this.settings.showErrors.call(this,this.errorMap,this.errorList):this.defaultShowErrors()},resetForm:function(){a.fn.resetForm&&a(this.currentForm).resetForm(),this.invalid={},this.submitted={},this.prepareForm(),this.hideErrors();var b=this.elements().removeData("previousValue").removeAttr("aria-invalid");this.resetElements(b)},resetElements:function(a){var b;if(this.settings.unhighlight)for(b=0;a[b];b++)this.settings.unhighlight.call(this,a[b],this.settings.errorClass,""),this.findByName(a[b].name).removeClass(this.settings.validClass);else a.removeClass(this.settings.errorClass).removeClass(this.settings.validClass)},numberOfInvalids:function(){return this.objectLength(this.invalid)},objectLength:function(a){var b,c=0;for(b in a)void 0!==a[b]&&null!==a[b]&&a[b]!==!1&&c++;return c},hideErrors:function(){this.hideThese(this.toHide)},hideThese:function(a){a.not(this.containers).text(""),this.addWrapper(a).hide()},valid:function(){return 0===this.size()},size:function(){return this.errorList.length},focusInvalid:function(){if(this.settings.focusInvalid)try{a(this.findLastActive()||this.errorList.length&&this.errorList[0].element||[]).filter(":visible").focus().trigger("focusin")}catch(b){}},findLastActive:function(){var b=this.lastActive;return b&&1===a.grep(this.errorList,function(a){return a.element.name===b.name}).length&&b},elements:function(){var b=this,c={};return a(this.currentForm).find("input, select, textarea, [contenteditable]").not(":submit, :reset, :image, :disabled").not(this.settings.ignore).filter(function(){var d=this.name||a(this).attr("name");return!d&&b.settings.debug&&window.console&&console.error("%o has no name assigned",this),this.hasAttribute("contenteditable")&&(this.form=a(this).closest("form")[0],this.name=d),!(d in c||!b.objectLength(a(this).rules()))&&(c[d]=!0,!0)})},clean:function(b){return a(b)[0]},errors:function(){var b=this.settings.errorClass.split(" ").join(".");return a(this.settings.errorElement+"."+b,this.errorContext)},resetInternals:function(){this.successList=[],this.errorList=[],this.errorMap={},this.toShow=a([]),this.toHide=a([])},reset:function(){this.resetInternals(),this.currentElements=a([])},prepareForm:function(){this.reset(),this.toHide=this.errors().add(this.containers)},prepareElement:function(a){this.reset(),this.toHide=this.errorsFor(a)},elementValue:function(b){var c,d,e=a(b),f=b.type;return"radio"===f||"checkbox"===f?this.findByName(b.name).filter(":checked").val():"number"===f&&"undefined"!=typeof b.validity?b.validity.badInput?"NaN":e.val():(c=b.hasAttribute("contenteditable")?e.text():e.val(),"file"===f?"C:\\fakepath\\"===c.substr(0,12)?c.substr(12):(d=c.lastIndexOf("/"),d>=0?c.substr(d+1):(d=c.lastIndexOf("\\"),d>=0?c.substr(d+1):c)):"string"==typeof c?c.replace(/\r/g,""):c)},check:function(b){b=this.validationTargetFor(this.clean(b));var c,d,e,f,g=a(b).rules(),h=a.map(g,function(a,b){return b}).length,i=!1,j=this.elementValue(b);if("function"==typeof g.normalizer?f=g.normalizer:"function"==typeof this.settings.normalizer&&(f=this.settings.normalizer),f){if(j=f.call(b,j),"string"!=typeof j)throw new TypeError("The normalizer should return a string value.");delete g.normalizer}for(d in g){e={method:d,parameters:g[d]};try{if(c=a.validator.methods[d].call(this,j,b,e.parameters),"dependency-mismatch"===c&&1===h){i=!0;continue}if(i=!1,"pending"===c)return void(this.toHide=this.toHide.not(this.errorsFor(b)));if(!c)return this.formatAndAdd(b,e),!1}catch(k){throw this.settings.debug&&window.console&&console.log("Exception occurred when checking element "+b.id+", check the '"+e.method+"' method.",k),k instanceof TypeError&&(k.message+=". Exception occurred when checking element "+b.id+", check the '"+e.method+"' method."),k}}if(!i)return this.objectLength(g)&&this.successList.push(b),!0},customDataMessage:function(b,c){return a(b).data("msg"+c.charAt(0).toUpperCase()+c.substring(1).toLowerCase())||a(b).data("msg")},customMessage:function(a,b){var c=this.settings.messages[a];return c&&(c.constructor===String?c:c[b])},findDefined:function(){for(var a=0;aWarning: No message defined for "+b.name+""),e=/\$?\{(\d+)\}/g;return"function"==typeof d?d=d.call(this,c.parameters,b):e.test(d)&&(d=a.validator.format(d.replace(e,"{$1}"),c.parameters)),d},formatAndAdd:function(a,b){var c=this.defaultMessage(a,b);this.errorList.push({message:c,element:a,method:b.method}),this.errorMap[a.name]=c,this.submitted[a.name]=c},addWrapper:function(a){return this.settings.wrapper&&(a=a.add(a.parent(this.settings.wrapper))),a},defaultShowErrors:function(){var a,b,c;for(a=0;this.errorList[a];a++)c=this.errorList[a],this.settings.highlight&&this.settings.highlight.call(this,c.element,this.settings.errorClass,this.settings.validClass),this.showLabel(c.element,c.message);if(this.errorList.length&&(this.toShow=this.toShow.add(this.containers)),this.settings.success)for(a=0;this.successList[a];a++)this.showLabel(this.successList[a]);if(this.settings.unhighlight)for(a=0,b=this.validElements();b[a];a++)this.settings.unhighlight.call(this,b[a],this.settings.errorClass,this.settings.validClass);this.toHide=this.toHide.not(this.toShow),this.hideErrors(),this.addWrapper(this.toShow).show()},validElements:function(){return this.currentElements.not(this.invalidElements())},invalidElements:function(){return a(this.errorList).map(function(){return this.element})},showLabel:function(b,c){var d,e,f,g,h=this.errorsFor(b),i=this.idOrName(b),j=a(b).attr("aria-describedby");h.length?(h.removeClass(this.settings.validClass).addClass(this.settings.errorClass),h.html(c)):(h=a("<"+this.settings.errorElement+">").attr("id",i+"-error").addClass(this.settings.errorClass).html(c||""),d=h,this.settings.wrapper&&(d=h.hide().show().wrap("<"+this.settings.wrapper+"/>").parent()),this.labelContainer.length?this.labelContainer.append(d):this.settings.errorPlacement?this.settings.errorPlacement.call(this,d,a(b)):d.insertAfter(b),h.is("label")?h.attr("for",i):0===h.parents("label[for='"+this.escapeCssMeta(i)+"']").length&&(f=h.attr("id"),j?j.match(new RegExp("\\b"+this.escapeCssMeta(f)+"\\b"))||(j+=" "+f):j=f,a(b).attr("aria-describedby",j),e=this.groups[b.name],e&&(g=this,a.each(g.groups,function(b,c){c===e&&a("[name='"+g.escapeCssMeta(b)+"']",g.currentForm).attr("aria-describedby",h.attr("id"))})))),!c&&this.settings.success&&(h.text(""),"string"==typeof this.settings.success?h.addClass(this.settings.success):this.settings.success(h,b)),this.toShow=this.toShow.add(h)},errorsFor:function(b){var c=this.escapeCssMeta(this.idOrName(b)),d=a(b).attr("aria-describedby"),e="label[for='"+c+"'], label[for='"+c+"'] *";return d&&(e=e+", #"+this.escapeCssMeta(d).replace(/\s+/g,", #")),this.errors().filter(e)},escapeCssMeta:function(a){return a.replace(/([\\!"#$%&'()*+,.\/:;<=>?@\[\]^`{|}~])/g,"\\$1")},idOrName:function(a){return this.groups[a.name]||(this.checkable(a)?a.name:a.id||a.name)},validationTargetFor:function(b){return this.checkable(b)&&(b=this.findByName(b.name)),a(b).not(this.settings.ignore)[0]},checkable:function(a){return/radio|checkbox/i.test(a.type)},findByName:function(b){return a(this.currentForm).find("[name='"+this.escapeCssMeta(b)+"']")},getLength:function(b,c){switch(c.nodeName.toLowerCase()){case"select":return a("option:selected",c).length;case"input":if(this.checkable(c))return this.findByName(c.name).filter(":checked").length}return b.length},depend:function(a,b){return!this.dependTypes[typeof a]||this.dependTypes[typeof a](a,b)},dependTypes:{"boolean":function(a){return a},string:function(b,c){return!!a(b,c.form).length},"function":function(a,b){return a(b)}},optional:function(b){var c=this.elementValue(b);return!a.validator.methods.required.call(this,c,b)&&"dependency-mismatch"},startRequest:function(b){this.pending[b.name]||(this.pendingRequest++,a(b).addClass(this.settings.pendingClass),this.pending[b.name]=!0)},stopRequest:function(b,c){this.pendingRequest--,this.pendingRequest<0&&(this.pendingRequest=0),delete this.pending[b.name],a(b).removeClass(this.settings.pendingClass),c&&0===this.pendingRequest&&this.formSubmitted&&this.form()?(a(this.currentForm).submit(),this.submitButton&&a("input:hidden[name='"+this.submitButton.name+"']",this.currentForm).remove(),this.formSubmitted=!1):!c&&0===this.pendingRequest&&this.formSubmitted&&(a(this.currentForm).triggerHandler("invalid-form",[this]),this.formSubmitted=!1)},previousValue:function(b,c){return c="string"==typeof c&&c||"remote",a.data(b,"previousValue")||a.data(b,"previousValue",{old:null,valid:!0,message:this.defaultMessage(b,{method:c})})},destroy:function(){this.resetForm(),a(this.currentForm).off(".validate").removeData("validator").find(".validate-equalTo-blur").off(".validate-equalTo").removeClass("validate-equalTo-blur")}},classRuleSettings:{required:{required:!0},email:{email:!0},url:{url:!0},date:{date:!0},dateISO:{dateISO:!0},number:{number:!0},digits:{digits:!0},creditcard:{creditcard:!0}},addClassRules:function(b,c){b.constructor===String?this.classRuleSettings[b]=c:a.extend(this.classRuleSettings,b)},classRules:function(b){var c={},d=a(b).attr("class");return d&&a.each(d.split(" "),function(){this in a.validator.classRuleSettings&&a.extend(c,a.validator.classRuleSettings[this])}),c},normalizeAttributeRule:function(a,b,c,d){/min|max|step/.test(c)&&(null===b||/number|range|text/.test(b))&&(d=Number(d),isNaN(d)&&(d=void 0)),d||0===d?a[c]=d:b===c&&"range"!==b&&(a[c]=!0)},attributeRules:function(b){var c,d,e={},f=a(b),g=b.getAttribute("type");for(c in a.validator.methods)"required"===c?(d=b.getAttribute(c),""===d&&(d=!0),d=!!d):d=f.attr(c),this.normalizeAttributeRule(e,g,c,d);return e.maxlength&&/-1|2147483647|524288/.test(e.maxlength)&&delete e.maxlength,e},dataRules:function(b){var c,d,e={},f=a(b),g=b.getAttribute("type");for(c in a.validator.methods)d=f.data("rule"+c.charAt(0).toUpperCase()+c.substring(1).toLowerCase()),this.normalizeAttributeRule(e,g,c,d);return e},staticRules:function(b){var c={},d=a.data(b.form,"validator");return d.settings.rules&&(c=a.validator.normalizeRule(d.settings.rules[b.name])||{}),c},normalizeRules:function(b,c){return a.each(b,function(d,e){if(e===!1)return void delete b[d];if(e.param||e.depends){var f=!0;switch(typeof e.depends){case"string":f=!!a(e.depends,c.form).length;break;case"function":f=e.depends.call(c,c)}f?b[d]=void 0===e.param||e.param:(a.data(c.form,"validator").resetElements(a(c)),delete b[d])}}),a.each(b,function(d,e){b[d]=a.isFunction(e)&&"normalizer"!==d?e(c):e}),a.each(["minlength","maxlength"],function(){b[this]&&(b[this]=Number(b[this]))}),a.each(["rangelength","range"],function(){var c;b[this]&&(a.isArray(b[this])?b[this]=[Number(b[this][0]),Number(b[this][1])]:"string"==typeof b[this]&&(c=b[this].replace(/[\[\]]/g,"").split(/[\s,]+/),b[this]=[Number(c[0]),Number(c[1])]))}),a.validator.autoCreateRanges&&(null!=b.min&&null!=b.max&&(b.range=[b.min,b.max],delete b.min,delete b.max),null!=b.minlength&&null!=b.maxlength&&(b.rangelength=[b.minlength,b.maxlength],delete b.minlength,delete b.maxlength)),b},normalizeRule:function(b){if("string"==typeof b){var c={};a.each(b.split(/\s/),function(){c[this]=!0}),b=c}return b},addMethod:function(b,c,d){a.validator.methods[b]=c,a.validator.messages[b]=void 0!==d?d:a.validator.messages[b],c.length<3&&a.validator.addClassRules(b,a.validator.normalizeRule(b))},methods:{required:function(b,c,d){if(!this.depend(d,c))return"dependency-mismatch";if("select"===c.nodeName.toLowerCase()){var e=a(c).val();return e&&e.length>0}return this.checkable(c)?this.getLength(b,c)>0:b.length>0},email:function(a,b){return this.optional(b)||/^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/.test(a)},url:function(a,b){return this.optional(b)||/^(?:(?:(?:https?|ftp):)?\/\/)(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,})).?)(?::\d{2,5})?(?:[\/?#]\S*)?$/i.test(a)},date:function(a,b){return this.optional(b)||!/Invalid|NaN/.test(new Date(a).toString())},dateISO:function(a,b){return this.optional(b)||/^\d{4}[\/\-](0?[1-9]|1[012])[\/\-](0?[1-9]|[12][0-9]|3[01])$/.test(a)},number:function(a,b){return this.optional(b)||/^(?:-?\d+|-?\d{1,3}(?:,\d{3})+)?(?:\.\d+)?$/.test(a)},digits:function(a,b){return this.optional(b)||/^\d+$/.test(a)},minlength:function(b,c,d){var e=a.isArray(b)?b.length:this.getLength(b,c);return this.optional(c)||e>=d},maxlength:function(b,c,d){var e=a.isArray(b)?b.length:this.getLength(b,c);return this.optional(c)||e<=d},rangelength:function(b,c,d){var e=a.isArray(b)?b.length:this.getLength(b,c);return this.optional(c)||e>=d[0]&&e<=d[1]},min:function(a,b,c){return this.optional(b)||a>=c},max:function(a,b,c){return this.optional(b)||a<=c},range:function(a,b,c){return this.optional(b)||a>=c[0]&&a<=c[1]},step:function(b,c,d){var e,f=a(c).attr("type"),g="Step attribute on input type "+f+" is not supported.",h=["text","number","range"],i=new RegExp("\\b"+f+"\\b"),j=f&&!i.test(h.join()),k=function(a){var b=(""+a).match(/(?:\.(\d+))?$/);return b&&b[1]?b[1].length:0},l=function(a){return Math.round(a*Math.pow(10,e))},m=!0;if(j)throw new Error(g);return e=k(d),(k(b)>e||l(b)%l(d)!==0)&&(m=!1),this.optional(c)||m},equalTo:function(b,c,d){var e=a(d);return this.settings.onfocusout&&e.not(".validate-equalTo-blur").length&&e.addClass("validate-equalTo-blur").on("blur.validate-equalTo",function(){a(c).valid()}),b===e.val()},remote:function(b,c,d,e){if(this.optional(c))return"dependency-mismatch";e="string"==typeof e&&e||"remote";var f,g,h,i=this.previousValue(c,e);return this.settings.messages[c.name]||(this.settings.messages[c.name]={}),i.originalMessage=i.originalMessage||this.settings.messages[c.name][e],this.settings.messages[c.name][e]=i.message,d="string"==typeof d&&{url:d}||d,h=a.param(a.extend({data:b},d.data)),i.old===h?i.valid:(i.old=h,f=this,this.startRequest(c),g={},g[c.name]=b,a.ajax(a.extend(!0,{mode:"abort",port:"validate"+c.name,dataType:"json",data:g,context:f.currentForm,success:function(a){var d,g,h,j=a===!0||"true"===a;f.settings.messages[c.name][e]=i.originalMessage,j?(h=f.formSubmitted,f.resetInternals(),f.toHide=f.errorsFor(c),f.formSubmitted=h,f.successList.push(c),f.invalid[c.name]=!1,f.showErrors()):(d={},g=a||f.defaultMessage(c,{method:e,parameters:b}),d[c.name]=i.message=g,f.invalid[c.name]=!0,f.showErrors(d)),i.valid=j,f.stopRequest(c,j)}},d)),"pending")}}});var b,c={};return a.ajaxPrefilter?a.ajaxPrefilter(function(a,b,d){var e=a.port;"abort"===a.mode&&(c[e]&&c[e].abort(),c[e]=d)}):(b=a.ajax,a.ajax=function(d){var e=("mode"in d?d:a.ajaxSettings).mode,f=("port"in d?d:a.ajaxSettings).port;return"abort"===e?(c[f]&&c[f].abort(),c[f]=b.apply(this,arguments),c[f]):b.apply(this,arguments)}),a}); --------------------------------------------------------------------------------