Webapp/SDAF/Controllers/RestHelper.cs (374 lines of code) (raw):

// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. using Azure.Core; using Azure.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.Extensions.Configuration; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using SDAFWebApp.Models; using System; using System.Collections.Generic; using System.Net.Http; using System.Net.Http.Headers; using System.Text; using System.Text.Json; using System.Threading; using System.Threading.Tasks; using Microsoft.VisualStudio.Services.Client; using Microsoft.VisualStudio.Services.Common; using Microsoft.VisualStudio.Services.TenantPolicy; using Microsoft.VisualStudio.Services.WebApi; using JsonSerializer = System.Text.Json.JsonSerializer; #pragma warning disable SYSLIB0020 namespace SDAFWebApp.Controllers { public class RestHelper : Controller { private readonly string collectionUri; private readonly string project; private readonly string repositoryId; private readonly string PAT; private readonly string branch; private readonly string sdafGeneralId; private readonly string sdafControlPlaneEnvironment; private readonly string sdafControlPlaneLocation; private readonly string tenantId; private readonly string managedIdentityClientId; private readonly Azure.Identity.DefaultAzureCredential credential; private readonly string sampleUrl = "https://api.github.com/repos/Azure/SAP-automation-samples"; private HttpClient client; private JsonSerializerOptions jsonSerializerOptions; public RestHelper(IConfiguration configuration, string type = "ADO") { collectionUri = configuration["CollectionUri"]; project = configuration["ProjectName"]; repositoryId = configuration["RepositoryId"]; PAT = configuration["PAT"]; string devops_authentication = configuration["AUTHENTICATION_TYPE"]; branch = configuration["SourceBranch"]; sdafGeneralId = configuration["SDAF_GENERAL_GROUP_ID"]; sdafControlPlaneEnvironment = configuration["CONTROLPLANE_ENV"]; sdafControlPlaneLocation = configuration["CONTROLPLANE_LOC"]; tenantId = configuration["AZURE_TENANT_ID"]; managedIdentityClientId = configuration["OVERRIDE_USE_MI_FIC_ASSERTION_CLIENTID"]; jsonSerializerOptions = new JsonSerializerOptions() { IgnoreNullValues = true }; if (type == "ADO") { if (devops_authentication == "PAT") { client = new HttpClient(); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", Convert.ToBase64String( System.Text.ASCIIEncoding.ASCII.GetBytes( string.Format("{0}:{1}", "", PAT)))); } else { if (string.IsNullOrEmpty(tenantId) || string.IsNullOrEmpty(managedIdentityClientId)) { throw new ArgumentNullException("TenantId and ManagedIdentityClientId must be provided for Managed Identity authentication."); } credential = new DefaultAzureCredential( new DefaultAzureCredentialOptions { TenantId = tenantId, ManagedIdentityClientId = managedIdentityClientId }); //var tokenRequestContext = new TokenRequestContext(new[] { "https://management.azure.com/.default", "499b84ac-1321-427f-aa17-267ca6975798/.default" }); var tokenRequestContext = new TokenRequestContext(VssAadSettings.DefaultScopes); var token = credential.GetToken(tokenRequestContext, CancellationToken.None); var accessToken = token.Token; client = new HttpClient(); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); } client.DefaultRequestHeaders.Accept.Add( new MediaTypeWithQualityHeaderValue("application/json")); client.DefaultRequestHeaders.Add("User-Agent", "sap-automation"); } else { client = new HttpClient(); client.DefaultRequestHeaders.Accept.Add( new MediaTypeWithQualityHeaderValue("application/json")); client.DefaultRequestHeaders.Add("User-Agent", "sap-automation"); } } // Get ADO project id public async Task<string> GetProjectId() { string getUri = $"{collectionUri}_apis/projects/{project}?api-version=7.1"; using HttpResponseMessage response = client.GetAsync(getUri).Result; string responseBody = await response.Content.ReadAsStringAsync(); HandleResponse(response, responseBody); return JsonDocument.Parse(responseBody).RootElement.GetProperty("id").GetString(); } // Add or edit a file in ADO public async Task UpdateRepo(string path, string content) { string getUri = $"{collectionUri}{project}/_apis/git/repositories/{repositoryId}/refs/?filter=heads/{branch}"; string postUri = $"{collectionUri}{project}/_apis/git/repositories/{repositoryId}/pushes?api-version=5.1"; string ooId; using HttpResponseMessage response = client.GetAsync(getUri).Result; string responseBody = await response.Content.ReadAsStringAsync(); HandleResponse(response, responseBody); ooId = JsonDocument.Parse(responseBody).RootElement.GetProperty("value")[0].GetProperty("objectId").GetString(); // Dynamically retrieve path string pathBase = await GetVariableFromVariableGroup(sdafGeneralId, "SDAF-General", "Deployment_Configuration_Path"); path = pathBase + path; // Create request body Refupdate refUpdate = new() { name = $"refs/heads/{branch}", oldObjectId = ooId }; GitRequestBody requestBody = new() { refUpdates = new Refupdate[] { refUpdate }, }; StringContent editContent = Helper.CreateHttpContent("edit", path, content, requestBody); // try to edit file (if it exists) HttpResponseMessage editResponse = await client.PostAsync(postUri, editContent); // add file on unsuccessful edit (because it does not exist) if (!editResponse.IsSuccessStatusCode) { StringContent addContent = Helper.CreateHttpContent("add", path, content, requestBody); HttpResponseMessage addResponse = await client.PostAsync(postUri, addContent); string addResponseBody = await addResponse.Content.ReadAsStringAsync(); HandleResponse(addResponse, addResponseBody); } } // Trigger a pipeline in azure devops public async Task TriggerPipeline(string pipelineId, PipelineRequestBody requestBody) { string getUri = $"{collectionUri}{project}/_apis/pipelines/{pipelineId}"; using HttpResponseMessage getResponse = client.GetAsync(getUri).Result; string getResponseBody = await getResponse.Content.ReadAsStringAsync(); HandleResponse(getResponse, getResponseBody); string postUri = $"{collectionUri}{project}/_apis/pipelines/{pipelineId}/runs?api-version=7.1"; string requestJson = JsonSerializer.Serialize(requestBody, typeof(PipelineRequestBody), jsonSerializerOptions); StringContent content = new(requestJson, Encoding.UTF8, "application/json"); HttpResponseMessage response = await client.PostAsync(postUri, content); string responseBody = await response.Content.ReadAsStringAsync(); HandleResponse(response, responseBody); } // Get an array of file names from azure sap-automation region given a directory public async Task<string[]> GetTemplateFileNames(string scopePath) { string getUri = $"{sampleUrl}/contents/{scopePath}?ref=main"; using HttpResponseMessage response = client.GetAsync(getUri).Result; string responseBody = await response.Content.ReadAsStringAsync(); HandleResponse(response, responseBody); List<string> fileNames = []; JsonElement values = JsonDocument.Parse(responseBody).RootElement; foreach (var value in values.EnumerateArray()) { string type = value.GetProperty("type").GetString(); string path = value.GetProperty("path").GetString(); if (type == "dir") { string[] subFiles = await GetTemplateFileNames(path); foreach (string subFile in subFiles) { fileNames.Add(subFile); } } else if (type == "file") { if (path.EndsWith(".tfvars")) { fileNames.Add(path); } } } return fileNames.ToArray(); } // Get a file from azure sap-automation repository public async Task<string> GetTemplateFile(string path) { string getUri = $"{sampleUrl}/contents/{path}?ref=main"; using HttpResponseMessage response = client.GetAsync(getUri).Result; string responseBody = await response.Content.ReadAsStringAsync(); HandleResponse(response, responseBody); string bitstring = JsonDocument.Parse(responseBody).RootElement.GetProperty("content").GetString(); return Encoding.UTF8.GetString(Convert.FromBase64String(bitstring)); } // Get the json response for all variable groups in an ado project public async Task<JsonElement> GetVariableGroupsJson() { string getUri = $"{collectionUri}{project}/_apis/distributedtask/variablegroups?api-version=7.1"; using HttpResponseMessage response = client.GetAsync(getUri).Result; string responseBody = await response.Content.ReadAsStringAsync(); HandleResponse(response, responseBody); JsonElement values = JsonDocument.Parse(responseBody).RootElement.GetProperty("value"); return values; } // List all variable groups from azure devops public async Task<EnvironmentModel[]> GetVariableGroups() { JsonElement values = await GetVariableGroupsJson(); List<EnvironmentModel> variableGroups = []; foreach (var value in values.EnumerateArray()) { EnvironmentModel environment = JsonSerializer.Deserialize<EnvironmentModel>(value.ToString()); environment.sdafControlPlaneEnvironment = sdafControlPlaneEnvironment; if (!environment.name.EndsWith("-" + sdafControlPlaneEnvironment)) { if (environment.name.StartsWith("SDAF-")) { environment.name = environment.name.Replace("SDAF-", ""); variableGroups.Add(environment); } } } return variableGroups.ToArray(); } // Get a list of all variable group names for use in a dropdown public async Task<List<SelectListItem>> GetEnvironmentsList() { JsonElement values = await GetVariableGroupsJson(); List<SelectListItem> variableGroups = [ new SelectListItem { Text = "", Value = "" } ]; foreach (var value in values.EnumerateArray()) { string groupName = value.GetProperty("name").ToString(); if (groupName.StartsWith("SDAF-")) { string text = value.GetProperty("name").ToString().Replace("SDAF-", ""); variableGroups.Add(new SelectListItem { Text = text, Value = text }); } } return variableGroups; } // Get a specific variable group from azure devops public async Task<EnvironmentModel> GetVariableGroup(int id) { string getUri = $"{collectionUri}{project}/_apis/distributedtask/variablegroups/{id}?api-version=7.1"; using HttpResponseMessage response = client.GetAsync(getUri).Result; string responseBody = await response.Content.ReadAsStringAsync(); HandleResponse(response, responseBody); EnvironmentModel environment = JsonSerializer.Deserialize<EnvironmentModel>(responseBody); environment.name = environment.name.Replace("SDAF-", ""); return environment; } // Get a variable group id by name in ado public async Task<string> GetVariableGroupIdFromName(string name) { JsonElement values = await GetVariableGroupsJson(); foreach (var value in values.EnumerateArray()) { if (value.GetProperty("name").ToString() == name) { return value.GetProperty("id").ToString(); } } return null; } // Get a specific variables value from a variable group in ado public async Task<string> GetVariableFromVariableGroup(string id, string variableGroupName, string variableName) { try { if (id == null || id == "") { id = await GetVariableGroupIdFromName(variableGroupName); if (id == null) { throw new Exception(); } } string getUri = $"{collectionUri}{project}/_apis/distributedtask/variablegroups/{id}?api-version=7.1"; using HttpResponseMessage response = client.GetAsync(getUri).Result; string responseBody = await response.Content.ReadAsStringAsync(); HandleResponse(response, responseBody); JsonElement variables = JsonDocument.Parse(responseBody).RootElement.GetProperty("variables"); string value = variables.GetProperty(variableName).GetProperty("value").GetString(); if (value.EndsWith('/')) { value = value.Remove(value.Length - 1); } return value; } catch { return "WORKSPACES"; } } // Create a variable group in azure devops public async Task CreateVariableGroup(EnvironmentModel environment, string newName, string description) { string postUri = $"{collectionUri}{project}/_apis/distributedtask/variablegroups?api-version=7.1"; string projectId = GetProjectId().Result; newName = "SDAF-" + newName.Replace("SDAF-", ""); environment.name = newName; environment.variableGroupProjectReferences = new VariableGroupProjectReference[] { new VariableGroupProjectReference { name = newName, description = description, projectReference = new ProjectReference { id = projectId, name = project } } }; string requestJson = JsonSerializer.Serialize(environment, typeof(EnvironmentModel), jsonSerializerOptions); StringContent content = new(requestJson, Encoding.ASCII, "application/json"); HttpResponseMessage response = await client.PostAsync(postUri, content); string responseBody = await response.Content.ReadAsStringAsync(); HandleResponse(response, responseBody); } // Update a variable group in azure devops public async Task UpdateVariableGroup(EnvironmentModel environment, string newName, string description) { string uri = $"{collectionUri}{project}/_apis/distributedtask/variablegroups/{environment.id}?api-version=7.1"; // Get the existing environment using HttpResponseMessage getResponse = client.GetAsync(uri).Result; string getResponseBody = await getResponse.Content.ReadAsStringAsync(); HandleResponse(getResponse, getResponseBody); EnvironmentModel existingEnvironment = JsonSerializer.Deserialize<EnvironmentModel>(getResponseBody); // Persist and update the project reference environment.variableGroupProjectReferences = existingEnvironment.variableGroupProjectReferences; if (environment.variableGroupProjectReferences != null && environment.variableGroupProjectReferences.Length > 0) { newName = "SDAF-" + newName.Replace("SDAF-", ""); environment.variableGroupProjectReferences[0].name = newName; environment.variableGroupProjectReferences[0].description = description; } else { throw new Exception("Existing environment project reference was empty"); } // Persist any existing variables string environmentJsonString = JsonConvert.SerializeObject(environment); string variablesJsonString = JsonDocument.Parse(getResponseBody).RootElement.GetProperty("variables").ToString(); dynamic dynamicEnvironment = JsonConvert.DeserializeObject(environmentJsonString); dynamic dynamicVariables = JsonConvert.DeserializeObject(variablesJsonString); dynamicVariables.Agent = JToken.FromObject(environment.variables.Agent); dynamicVariables.ARM_CLIENT_ID = JToken.FromObject(environment.variables.ARM_CLIENT_ID); dynamicVariables.ARM_CLIENT_SECRET = JToken.FromObject(environment.variables.ARM_CLIENT_SECRET); dynamicVariables.ARM_TENANT_ID = JToken.FromObject(environment.variables.ARM_TENANT_ID); dynamicVariables.ARM_SUBSCRIPTION_ID = JToken.FromObject(environment.variables.ARM_SUBSCRIPTION_ID); dynamicVariables.sap_fqdn = JToken.FromObject(environment.variables.sap_fqdn); dynamicVariables.POOL = JToken.FromObject(environment.variables.POOL); dynamicEnvironment.variables = dynamicVariables; // Make the put call string requestJson = JsonConvert.SerializeObject(dynamicEnvironment, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore }); StringContent content = new(requestJson, Encoding.ASCII, "application/json"); HttpResponseMessage putResponse = await client.PutAsync(uri, content); string putResponseBody = await putResponse.Content.ReadAsStringAsync(); HandleResponse(putResponse, putResponseBody); } static private void HandleResponse(HttpResponseMessage response, string responseBody) { if (!response.IsSuccessStatusCode) { string errorMessage; switch (response.StatusCode) { case System.Net.HttpStatusCode.Unauthorized: errorMessage = "Unauthorized, please ensure that the MSI/Personal Access Token has sufficient permissions and that it has not expired."; break; case System.Net.HttpStatusCode.NotFound: errorMessage = "Could not find the template."; break; default: errorMessage = JsonDocument.Parse(responseBody).RootElement.GetProperty("message").ToString(); break; } throw new HttpRequestException(errorMessage); } } public static List<ProductInfoHeaderValue> AppUserAgent { get; } = [ new ProductInfoHeaderValue("SDAF") ]; } } #pragma warning restore SYSLIB0020