diff --git a/Slingshot.Api/Content/Styles/app.css b/Slingshot.Api/Content/Styles/app.css index 55646e0..e245c9e 100644 --- a/Slingshot.Api/Content/Styles/app.css +++ b/Slingshot.Api/Content/Styles/app.css @@ -48,6 +48,10 @@ pre { margin-bottom:20px; } overflow: hidden; } +input[type="text"]:disabled{ + background-color: #969393; +} + .code {font-size:12px; } .code-attrName{color:#ff0000;} diff --git a/Slingshot.Api/Controllers/ARMController.cs b/Slingshot.Api/Controllers/ARMController.cs index 441ceb1..bfb9a98 100644 --- a/Slingshot.Api/Controllers/ARMController.cs +++ b/Slingshot.Api/Controllers/ARMController.cs @@ -95,25 +95,27 @@ public async Task Get() [Authorize] [HttpPost] #pragma warning disable 4014 - public async Task Preview([FromBody] JObject parameters, string subscriptionId, string templateUrl) + public async Task Preview(DeployInputs inputs) { JObject responseObj = new JObject(); List providers = new List(32); HttpResponseMessage response = null; - using (var client = GetRMClient(subscriptionId)) + using (var client = GetRMClient(inputs.subscriptionId)) { ResourceGroupCreateOrUpdateResult resourceResult = null; string tempRGName = Guid.NewGuid().ToString(); try { - resourceResult = await client.ResourceGroups.CreateOrUpdateAsync(tempRGName, new BasicResourceGroup { Location = "East US" }); + resourceResult = await client.ResourceGroups.CreateOrUpdateAsync( + tempRGName, + new BasicResourceGroup { Location = inputs.resourceGroup.location }); // For now we just default to East US for the resource group location. var basicDeployment = new BasicDeployment { - Parameters = parameters.ToString(), - TemplateLink = new TemplateLink(new Uri(templateUrl)) + Parameters = inputs.parameters.ToString(), + TemplateLink = new TemplateLink(new Uri(inputs.templateUrl)) }; var deploymentResult = await client.Deployments.ValidateAsync(tempRGName, tempRGName, basicDeployment); @@ -146,7 +148,7 @@ public async Task Preview([FromBody] JObject parameters, st (resourceResult.StatusCode == HttpStatusCode.Created || resourceResult.StatusCode == HttpStatusCode.OK)) { string token = GetTokenFromHeader(); - Task.Run(() => { DeleteResourceGroup(subscriptionId, token, tempRGName); }); + Task.Run(() => { DeleteResourceGroup(inputs.subscriptionId, token, tempRGName); }); } } } @@ -171,25 +173,32 @@ private void DeleteResourceGroup(string subscriptionId, string token, string rgN [Authorize] [HttpPost] - public async Task Deploy([FromBody] JObject parameters, string subscriptionId, string resourceGroup, string templateUrl) + public async Task Deploy(DeployInputs inputs) { CreateDeploymentResponse responseObj = new CreateDeploymentResponse(); HttpResponseMessage response = null; try { - using (var client = GetRMClient(subscriptionId)) + using (var client = GetRMClient(inputs.subscriptionId)) { // For now we just default to East US for the resource group location. - var resourceResult = await client.ResourceGroups.CreateOrUpdateAsync(resourceGroup, new BasicResourceGroup { Location = "East US" }); - var templateParams = parameters.ToString(); + var resourceResult = await client.ResourceGroups.CreateOrUpdateAsync( + inputs.resourceGroup.name, + new BasicResourceGroup { Location = inputs.resourceGroup.location }); + + var templateParams = inputs.parameters.ToString(); var basicDeployment = new BasicDeployment { Parameters = templateParams, - TemplateLink = new TemplateLink(new Uri(templateUrl)) + TemplateLink = new TemplateLink(new Uri(inputs.templateUrl)) }; - var deploymentResult = await client.Deployments.CreateOrUpdateAsync(resourceGroup, resourceGroup, basicDeployment); + var deploymentResult = await client.Deployments.CreateOrUpdateAsync( + inputs.resourceGroup.name, + inputs.resourceGroup.name, + basicDeployment); + response = Request.CreateResponse(HttpStatusCode.OK, responseObj); } } @@ -340,7 +349,7 @@ public async Task GetTemplate(string repositoryUrl) returnObj["subscriptions"] = JArray.FromObject(subscriptions); returnObj["tenants"] = tenants; returnObj["userDisplayName"] = userDisplayName; - returnObj["resourceGroup"] = resourceGroupName; + returnObj["resourceGroupName"] = resourceGroupName; returnObj["template"] = template; returnObj["templateUrl"] = templateUrl; returnObj["repositoryUrl"] = repo.RepositoryUrl; diff --git a/Slingshot.Api/Helpers/Utils.cs b/Slingshot.Api/Helpers/Utils.cs index 6d2c8b8..07f7f55 100644 --- a/Slingshot.Api/Helpers/Utils.cs +++ b/Slingshot.Api/Helpers/Utils.cs @@ -4,17 +4,11 @@ using System.Net.Http; using System.Net.Http.Headers; using System.Threading.Tasks; +using System.Collections.Generic; +using Slingshot.Models; namespace Slingshot.Helpers { - public class SubscriptionInfo - { - public string id { get; set; } - public string subscriptionId { get; set; } - public string displayName { get; set; } - public string state { get; set; } - } - public class Utils { public class ResultOf @@ -41,8 +35,21 @@ public static async Task GetSubscriptionsAsync(string host, { if (response.IsSuccessStatusCode) { - var result = await response.Content.ReadAsAsync>(); - return result.value; + var subs = (await response.Content.ReadAsAsync>()).value; + + var getRgTasks = new List>(); + foreach (var sub in subs) + { + getRgTasks.Add(GetResourceGroups(client, host, sub.subscriptionId)); + } + + var rgsForAllSubs = await Task.WhenAll(getRgTasks.ToArray()); + for(int i = 0; i < rgsForAllSubs.Length; i ++) + { + subs[i].resourceGroups = rgsForAllSubs[i]; + } + + return subs; } var content = await response.Content.ReadAsStringAsync(); @@ -60,6 +67,39 @@ public static async Task GetSubscriptionsAsync(string host, } } + private static async Task GetResourceGroups( + HttpClient client, + string host, + string subscriptionId) + { + string url = string.Format( + "{0}/subscriptions/{1}/resourceGroups?api-version={2}", + Utils.GetCSMUrl(host), + subscriptionId, + Constants.CSM.ApiVersion); + + using (var response = await client.GetAsync(url)) + { + if (response.IsSuccessStatusCode) + { + return (await response.Content.ReadAsAsync>()).value; + } + + var content = await response.Content.ReadAsStringAsync(); + if (content.StartsWith("{")) + { + var error = (JObject)JObject.Parse(content)["error"]; + if (error != null) + { + throw new InvalidOperationException(String.Format("GetResourceGroups {0}, {1}", response.StatusCode, error.Value("message"))); + } + } + + throw new InvalidOperationException(String.Format("GetResourceGroups {0}, {1}", response.StatusCode, await response.Content.ReadAsStringAsync())); + + } + } + public static async Task Execute(Task task) { var watch = new Stopwatch(); diff --git a/Slingshot.Api/Models/Inputs.cs b/Slingshot.Api/Models/Inputs.cs new file mode 100644 index 0000000..c2bad23 --- /dev/null +++ b/Slingshot.Api/Models/Inputs.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Web; +using Newtonsoft.Json.Linq; + +namespace Slingshot.Models +{ + public class DeployInputs + { + public JObject parameters { get; set; } + public string subscriptionId { get; set; } + public ResourceGroupInfo resourceGroup { get; set; } + public string templateUrl { get; set; } + } +} \ No newline at end of file diff --git a/Slingshot.Api/Models/Subscriptions.cs b/Slingshot.Api/Models/Subscriptions.cs new file mode 100644 index 0000000..5da8619 --- /dev/null +++ b/Slingshot.Api/Models/Subscriptions.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Web; + +namespace Slingshot.Models +{ + public class SubscriptionInfo + { + public string id { get; set; } + public string subscriptionId { get; set; } + public string displayName { get; set; } + public string state { get; set; } + public ResourceGroupInfo[] resourceGroups { get; set; } + } + + public class ResourceGroupInfo + { + //public string id { get; set; } + public string name { get; set; } + public string location { get; set; } + } +} \ No newline at end of file diff --git a/Slingshot.Api/Slingshot.Api.csproj b/Slingshot.Api/Slingshot.Api.csproj index e562e3e..3055f56 100644 --- a/Slingshot.Api/Slingshot.Api.csproj +++ b/Slingshot.Api/Slingshot.Api.csproj @@ -154,6 +154,8 @@ + + diff --git a/Slingshot.Api/ng/Scripts/app.js b/Slingshot.Api/ng/Scripts/app.js index 0f25103..f06d43c 100644 --- a/Slingshot.Api/ng/Scripts/app.js +++ b/Slingshot.Api/ng/Scripts/app.js @@ -69,7 +69,24 @@ var telemetryObj = function(){ return that; }; +var contantsObj = function(){ + var that = {}; + var paramsObj = function(){ + var that = {}; + that.siteLocation = "siteLocation"; + that.siteLocationLower = that.siteLocation.toLowerCase(); + that.sqlServerLocation = "sqlServerLocation"; + that.sqlServerLocationLower = that.sqlServerLocation.toLowerCase(); + return that; + } + + that.params = paramsObj(); + + return that; +} + var telemetry = telemetryObj(); +var constants = contantsObj(); (function () { @@ -237,7 +254,6 @@ angular.module('formApp', ['ngAnimate', 'ui.router']) .then(function(result){ $scope.formData.userDisplayName = result.data.userDisplayName; $scope.formData.template = result.data.template; - $scope.formData.resourceGroup = result.data.resourceGroup; $scope.formData.subscriptions = result.data.subscriptions; $scope.formData.siteLocations = result.data.siteLocations; $scope.formData.sqlServerLocations = result.data.sqlServerLocations; @@ -248,6 +264,10 @@ angular.module('formApp', ['ngAnimate', 'ui.router']) $scope.formData.repositoryDisplayUrl = result.data.repositoryDisplayUrl; $scope.formData.siteName = result.data.siteName; $scope.formData.siteNameQuery = result.data.siteName; + $scope.formData.newResourceGroup = { + name: result.data.resourceGroupName, + location: "" + }; // Select current tenant var tenants = $scope.formData.tenants; @@ -259,10 +279,11 @@ angular.module('formApp', ['ngAnimate', 'ui.router']) // Select first subscription if($scope.formData.subscriptions && $scope.formData.subscriptions.length > 0){ - $scope.formData.subscription = $scope.formData.subscriptions[0]; + var sub = $scope.formData.subscriptions[0]; + $scope.formData.subscription = sub; + setDefaultRg(sub); } - // Pull out template parameters to show on UI $scope.formData.params = []; $scope.formData.repoParamFound = false; @@ -286,10 +307,10 @@ angular.module('formApp', ['ngAnimate', 'ui.router']) param.value = result.data.siteName; $scope.formData.siteNameAvailable = true; } - else if(paramName === "sitelocation" && $scope.formData.siteLocations && $scope.formData.siteLocations.length > 0 && !param.defaultValue){ + else if(paramName === constants.params.siteLocationLower && $scope.formData.siteLocations && $scope.formData.siteLocations.length > 0 && !param.defaultValue){ param.value = $scope.formData.siteLocations[0]; } - else if(paramName === "sqlserverlocation" && $scope.formData.sqlServerLocations && $scope.formData.sqlServerLocations.length > 0 && !param.defaultValue){ + else if(paramName === constants.params.sqlServerLocationLower && $scope.formData.sqlServerLocations && $scope.formData.sqlServerLocations.length > 0 && !param.defaultValue){ param.value = $scope.formData.sqlServerLocations[0]; } else if(paramName === "sqlservername" && $scope.formData.siteName && $scope.formData.siteName.length > 0 && !param.defaultValue){ @@ -311,7 +332,6 @@ angular.module('formApp', ['ngAnimate', 'ui.router']) if(!param.value){ param.value = param.defaultValue; } - } }, function(result){ @@ -321,11 +341,66 @@ angular.module('formApp', ['ngAnimate', 'ui.router']) }); } + function setDefaultRg(sub){ + var curRg = null; + + var rgs = sub.resourceGroups; + if(rgs.length === 0 || (rgs.length > 0 && rgs[0].location)){ + curRg = { + name: "Create New", + location: "" + }; + + sub.resourceGroups.unshift(curRg); + } + else{ + curRg = rgs[0]; + } + + $scope.formData.existingResourceGroup = curRg; + } + + function creatingNewRg(){ + return !$scope.formData.existingResourceGroup.location; + } + $scope.changeTenant = function(){ var tenantUrl = window.location.origin + window.location.pathname + "api/tenants/" + $scope.formData.tenant.TenantId; window.location = tenantUrl; } + $scope.changeSubscription = function(){ + setDefaultRg($scope.formData.subscription); + } + + $scope.changeResourceGroup = function(){ + if(creatingNewRg()){ + return; + } + + $scope.formData.params.forEach(function(param){ + var name = param.name.toLowerCase(); + var locations = null; + + if(name === constants.params.siteLocationLower){ + locations = $scope.formData.siteLocations; + } + else if(name === constants.params.sqlServerLocationLower){ + locations = $scope.formData.sqlServerLocations; + } + + if(locations){ + for(var i = 0; i < locations.length; i++){ + // Site/SQL locations have spaces in them + if(locations[i].replace(/ /g, "").toLowerCase() === $scope.formData.existingResourceGroup.location){ + param.value = locations[i]; + break; + } + } + } + }); + } + $scope.showParam = function(param){ var name = param.name.toLowerCase(); if(name === 'repourl' && $scope.formData.repositoryUrl){ @@ -393,10 +468,39 @@ angular.module('formApp', ['ngAnimate', 'ui.router']) $scope.canMoveToNextStep = function(){ var isValid = true; - if (!$scope.formData.tenant || !$scope.formData.subscription || !$scope.formData.params) { + if (!$scope.formData.tenant + || !$scope.formData.subscription + || !$scope.formData.params) { + return false; } + var rgs = $scope.formData.subscription.resourceGroups; + if(creatingNewRg()){ + + var regex = new RegExp("^[0-9a-zA-Z\\()._-]+[0-9a-zA-Z()_-]$"); + if(!regex.test($scope.formData.newResourceGroup.name)){ + $scope.formData.resourceGroupError = "Invalid Resource Group Name"; + isValid = false; + } + else{ + for(var i = 0; i < rgs.length; i++){ + if($scope.formData.newResourceGroup.name.toLowerCase() === rgs[i].name.toLowerCase()){ + + $scope.formData.resourceGroupError = "Resource Group Exists"; + isValid = false; + } + } + } + + if(isValid){ + $scope.formData.resourceGroupError = null; + } + } + else{ + $scope.formData.resourceGroupError = null; + } + // If we're dealing with a site, and the name is not available, we can't go to next step. // In the case of non-site template, siteName will be undefined. if ($scope.formData.siteName === "" || @@ -487,6 +591,9 @@ angular.module('formApp', ['ngAnimate', 'ui.router']) function getDeployPayload(params){ var dataParams = {} + var rg = creatingNewRg() ? $scope.formData.newResourceGroup : $scope.formData.existingResourceGroup; + $scope.formData.finalResourceGroup = rg; + for(var i = 0; i < params.length; i++){ var param = params[i]; @@ -494,7 +601,7 @@ angular.module('formApp', ['ngAnimate', 'ui.router']) if(param.name.toLowerCase() === "workersize"){ param.value = param.allowedValues.indexOf(param.value).toString(); } - + // JavaScript may convert string representations of numbers incorrectly if(typeof param.value === "number" && param.type.toLowerCase() === 'string'){ param.value = param.value.toString(); @@ -503,10 +610,26 @@ angular.module('formApp', ['ngAnimate', 'ui.router']) param.value = parseInt(param.value); } + if(creatingNewRg() && + (param.name.toLowerCase() === constants.params.siteLocationLower || + param.name.toLowerCase() === constants.params.sqlServerLocationLower)){ + + rg.location = param.value; + } + dataParams[param.name] = {value : param.value}; } - return dataParams; + if(!rg.location){ + rg.location = "East US"; + } + + return { + parameters: dataParams, + subscriptionId: $scope.formData.subscription.subscriptionId, + resourceGroup: rg, + templateUrl: $scope.formData.templateUrl + }; } initialize($scope, $http); @@ -521,9 +644,6 @@ angular.module('formApp', ['ngAnimate', 'ui.router']) $http({ method: "post", url: "api/preview/"+subscriptionId, - params:{ - "templateUrl" : $scope.formData.templateUrl, - }, data: $scope.formData.deployPayload }) .then(function(result){ @@ -574,10 +694,6 @@ angular.module('formApp', ['ngAnimate', 'ui.router']) $http({ method: "post", url: "api/deployments/"+subscriptionId, - params:{ - "templateUrl": $scope.formData.templateUrl, - "resourceGroup": $scope.formData.resourceGroup, - }, data: $scope.formData.deployPayload }) .then(function(result){ @@ -591,7 +707,7 @@ angular.module('formApp', ['ngAnimate', 'ui.router']) function getStatus($scope, $http, deploymentUrl){ var subscriptionId = $scope.formData.subscription.subscriptionId; - var resourceGroup = $scope.formData.resourceGroup; + var resourceGroup = $scope.formData.finalResourceGroup; var params; if ($scope.formData.repoParamFound) { @@ -602,7 +718,7 @@ angular.module('formApp', ['ngAnimate', 'ui.router']) $http({ method: "get", - url: "api/deployments/" + subscriptionId + "/rg/" + resourceGroup, + url: "api/deployments/" + subscriptionId + "/rg/" + resourceGroup.name, params: params, }) .then(function(result){ @@ -636,7 +752,7 @@ angular.module('formApp', ['ngAnimate', 'ui.router']) if ($scope.formData.repoParamFound){ $scope.formData.portalUrl = portalWebSiteFormat.format( $scope.formData.subscription.subscriptionId, - $scope.formData.resourceGroup, + $scope.formData.finalResourceGroup.name, $scope.formData.siteName); window.setTimeout(getGitStatus, 1000, $scope, $http); @@ -645,7 +761,7 @@ angular.module('formApp', ['ngAnimate', 'ui.router']) $scope.formData.deploymentSucceeded = true; $scope.formData.portalUrl = portalRGFormat.format( $scope.formData.subscription.subscriptionId, - $scope.formData.resourceGroup); + $scope.formData.finalResourceGroup.name); telemetry.logDeploySucceeded($scope.formData.repositoryUrl); } @@ -698,7 +814,7 @@ angular.module('formApp', ['ngAnimate', 'ui.router']) function getGitStatus($scope, $http){ var subscriptionId = $scope.formData.subscription.subscriptionId; var siteName = $scope.formData.siteName; - var resourceGroup = $scope.formData.resourceGroup; + var resourceGroup = $scope.formData.finalResourceGroup.name; $http({ method: "get", diff --git a/Slingshot.Api/ng/Views/form-setup.html b/Slingshot.Api/ng/Views/form-setup.html index 149c57b..7ad0216 100644 --- a/Slingshot.Api/ng/Views/form-setup.html +++ b/Slingshot.Api/ng/Views/form-setup.html @@ -21,13 +21,20 @@
-
- - + + +
+ +
+ + - {{formData.resourceGroupError}} +