diff --git a/ConsoleApps/AzureRestApi/AzureRestApi/AzureRestApi.csproj b/ConsoleApps/AzureRestApi/AzureRestApi/AzureRestApi.csproj index 1e2833c..63f6036 100644 --- a/ConsoleApps/AzureRestApi/AzureRestApi/AzureRestApi.csproj +++ b/ConsoleApps/AzureRestApi/AzureRestApi/AzureRestApi.csproj @@ -15,6 +15,7 @@ + diff --git a/ConsoleApps/AzureRestApi/AzureRestApi/Models/Api/ApiResponse.cs b/ConsoleApps/AzureRestApi/AzureRestApi/Models/Api/ApiResponse.cs new file mode 100644 index 0000000..7424f55 --- /dev/null +++ b/ConsoleApps/AzureRestApi/AzureRestApi/Models/Api/ApiResponse.cs @@ -0,0 +1,7 @@ +namespace AzureRestApi.Models.Api +{ + public class ApiResponse + { + public List? value { get; set; } + } +} diff --git a/ConsoleApps/AzureRestApi/AzureRestApi/Models/Api/ResourcesResponse.cs b/ConsoleApps/AzureRestApi/AzureRestApi/Models/Api/ResourcesResponse.cs deleted file mode 100644 index a3dac25..0000000 --- a/ConsoleApps/AzureRestApi/AzureRestApi/Models/Api/ResourcesResponse.cs +++ /dev/null @@ -1,9 +0,0 @@ -using AzureRestApi.Models.Resources; - -namespace AzureRestApi.Models.Api -{ - public class ResourcesResponse - { - public List? value { get; set; } - } -} diff --git a/ConsoleApps/AzureRestApi/AzureRestApi/Models/Api/SubscriptionResponse.cs b/ConsoleApps/AzureRestApi/AzureRestApi/Models/Api/SubscriptionResponse.cs deleted file mode 100644 index 8c4c913..0000000 --- a/ConsoleApps/AzureRestApi/AzureRestApi/Models/Api/SubscriptionResponse.cs +++ /dev/null @@ -1,9 +0,0 @@ -using AzureRestApi.Models.Resources; - -namespace AzureRestApi.Models.Api -{ - public class SubscriptionResponse - { - public List? value { get; set; } - } -} diff --git a/ConsoleApps/AzureRestApi/AzureRestApi/Models/Resources/Deployment.cs b/ConsoleApps/AzureRestApi/AzureRestApi/Models/Resources/Deployment.cs new file mode 100644 index 0000000..8f985a8 --- /dev/null +++ b/ConsoleApps/AzureRestApi/AzureRestApi/Models/Resources/Deployment.cs @@ -0,0 +1,11 @@ +namespace AzureRestApi.Models.Resources +{ + public class Deployment + { + public string? id { get; set; } + public string? name { get; set; } + public string? type { get; set; } + public string? location { get; set; } + public DeploymentProperties? properties { get; set; } + } +} diff --git a/ConsoleApps/AzureRestApi/AzureRestApi/Models/Resources/DeploymentProperties.cs b/ConsoleApps/AzureRestApi/AzureRestApi/Models/Resources/DeploymentProperties.cs new file mode 100644 index 0000000..b4c1267 --- /dev/null +++ b/ConsoleApps/AzureRestApi/AzureRestApi/Models/Resources/DeploymentProperties.cs @@ -0,0 +1,17 @@ +namespace AzureRestApi.Models.Resources +{ + public class DeploymentProperties + { + public string? templateHash { get; set; } + public string? mode { get; set; } + //public string? parameters { get; set; } + public string? provisioningState { get; set; } + public DateTimeOffset? timestamp { get; set; } + public string? duration { get; set; } + public string? correlationId { get; set; } + // public string? providers { get; set; } + //public string? dependencies { get; set; } + //public string? outputs { get; set; } + public List? outputResources { get; set; } + } +} diff --git a/ConsoleApps/AzureRestApi/AzureRestApi/Models/Resources/ResourceGroup.cs b/ConsoleApps/AzureRestApi/AzureRestApi/Models/Resources/ResourceGroup.cs new file mode 100644 index 0000000..a0f242e --- /dev/null +++ b/ConsoleApps/AzureRestApi/AzureRestApi/Models/Resources/ResourceGroup.cs @@ -0,0 +1,12 @@ +namespace AzureRestApi.Models.Resources +{ + public class ResourceGroup + { + public string? id { get; set; } + public string? name { get; set; } + public string? managedBy { get; set; } + public string? location { get; set; } + public Dictionary tags { get; set; } = new Dictionary(); + public DeploymentProperties? properties { get; set; } + } +} diff --git a/ConsoleApps/AzureRestApi/AzureRestApi/Models/Resources/ResourceGroupProperties.cs b/ConsoleApps/AzureRestApi/AzureRestApi/Models/Resources/ResourceGroupProperties.cs new file mode 100644 index 0000000..4ebdd1a --- /dev/null +++ b/ConsoleApps/AzureRestApi/AzureRestApi/Models/Resources/ResourceGroupProperties.cs @@ -0,0 +1,7 @@ +namespace AzureRestApi.Models.Resources +{ + public class ResourceGroupProperties + { + public string? provisioningState { get; set; } + } +} diff --git a/ConsoleApps/AzureRestApi/AzureRestApi/Models/Settings.cs b/ConsoleApps/AzureRestApi/AzureRestApi/Models/Settings.cs index c718cc4..27dda3d 100644 --- a/ConsoleApps/AzureRestApi/AzureRestApi/Models/Settings.cs +++ b/ConsoleApps/AzureRestApi/AzureRestApi/Models/Settings.cs @@ -2,7 +2,7 @@ { public class Settings { - public string KeyVaultName { get; set; } - public string AzureTenantId { get; set; } + public string KeyVaultName { get; set; } = string.Empty; + public string AzureTenantId { get; set; } = string.Empty; } } diff --git a/ConsoleApps/AzureRestApi/AzureRestApi/Program.cs b/ConsoleApps/AzureRestApi/AzureRestApi/Program.cs index 4f10372..e6f3bf1 100644 --- a/ConsoleApps/AzureRestApi/AzureRestApi/Program.cs +++ b/ConsoleApps/AzureRestApi/AzureRestApi/Program.cs @@ -27,10 +27,13 @@ namespace AzureRestApi services.AddSingleton(settings); services.AddHttpClient(); + services.AddHttpClient(); services.AddHttpClient(); services.AddHttpClient(); - + services.AddHttpClient(); + services.AddScoped(); + services.AddScoped(); services.AddHostedService(); } ); diff --git a/ConsoleApps/AzureRestApi/AzureRestApi/Repositories/AzureBaseRepository.cs b/ConsoleApps/AzureRestApi/AzureRestApi/Repositories/AzureBaseRepository.cs new file mode 100644 index 0000000..e1226bb --- /dev/null +++ b/ConsoleApps/AzureRestApi/AzureRestApi/Repositories/AzureBaseRepository.cs @@ -0,0 +1,44 @@ +using AzureRestApi.Models.Api; +using System.Net.Http.Headers; +using System.Text.Json; + +namespace AzureRestApi.Repositories +{ + public abstract class AzureBaseRepository + { + private readonly HttpClient _httpClient; + private readonly AccessTokenRepository _accessTokenRepository; + + public AzureBaseRepository(HttpClient httpClient, AccessTokenRepository accessTokenRepository) + { + _httpClient = httpClient; + _accessTokenRepository = accessTokenRepository; + } + + protected async Task> GetAllByUri(Uri uri) + { + var accessToken = await _accessTokenRepository.GetAccessToken(); + + HttpRequestMessage request = new(HttpMethod.Get, uri); + request.Headers.Accept.Clear(); + request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); + + var response = await _httpClient.SendAsync(request).ConfigureAwait(false); + + try + { + response.EnsureSuccessStatusCode(); + + var responseString = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + var result = JsonSerializer.Deserialize>(responseString)?.value; + return result ?? new List(); + + } + catch + { + return new List(); + } + } + } +} diff --git a/ConsoleApps/AzureRestApi/AzureRestApi/Repositories/AzureDeploymentRepository.cs b/ConsoleApps/AzureRestApi/AzureRestApi/Repositories/AzureDeploymentRepository.cs new file mode 100644 index 0000000..ed959af --- /dev/null +++ b/ConsoleApps/AzureRestApi/AzureRestApi/Repositories/AzureDeploymentRepository.cs @@ -0,0 +1,25 @@ +using AzureRestApi.Models.Resources; + +namespace AzureRestApi.Repositories +{ + public class AzureDeploymentRepository : AzureBaseRepository + { + public AzureDeploymentRepository(HttpClient httpClient, AccessTokenRepository accessTokenRepository) : base(httpClient, accessTokenRepository) + { + } + + public async Task> GetAllBySubscription(string subscriptionId) + { + // GET https://management.azure.com/subscriptions/{subscriptionId}/providers/Microsoft.Resources/deployments/?api-version=2021-04-01 + var uri = new Uri($"https://management.azure.com/subscriptions/{subscriptionId}/providers/Microsoft.Resources/deployments/?api-version=2021-04-01"); + return await GetAllByUri(uri); + } + public async Task> GetAllByResourceGroup(string subscriptionId, string resourceGroupName) + { + // GET https://management.azure.com/subscriptions/{subscriptionId}/resourcegroups/{resourceGroupName}/providers/Microsoft.Resources/deployments/?api-version=2021-04-01 + var uri = new Uri($"https://management.azure.com/subscriptions/{subscriptionId}/resourcegroups/{resourceGroupName}/providers/Microsoft.Resources/deployments/?api-version=2021-04-01"); + return await GetAllByUri(uri); + } + + } +} diff --git a/ConsoleApps/AzureRestApi/AzureRestApi/Repositories/AzureResourceGroupRepository.cs b/ConsoleApps/AzureRestApi/AzureRestApi/Repositories/AzureResourceGroupRepository.cs new file mode 100644 index 0000000..48ebbec --- /dev/null +++ b/ConsoleApps/AzureRestApi/AzureRestApi/Repositories/AzureResourceGroupRepository.cs @@ -0,0 +1,18 @@ +using AzureRestApi.Models.Resources; + +namespace AzureRestApi.Repositories +{ + public class AzureResourceGroupRepository : AzureBaseRepository + { + public AzureResourceGroupRepository(HttpClient httpClient, AccessTokenRepository accessTokenRepository) : base(httpClient, accessTokenRepository) + { + } + + public async Task> GetAll(string subscriptionId) + { + // GET https://management.azure.com/subscriptions/{subscriptionId}/resourcegroups?api-version=2021-04-01 + var uri = new Uri($"https://management.azure.com/subscriptions/{subscriptionId}/resourcegroups?api-version=2021-04-01"); + return await GetAllByUri(uri); + } + } +} diff --git a/ConsoleApps/AzureRestApi/AzureRestApi/Repositories/AzureResourceRepository.cs b/ConsoleApps/AzureRestApi/AzureRestApi/Repositories/AzureResourceRepository.cs index 880e8cb..dbd605d 100644 --- a/ConsoleApps/AzureRestApi/AzureRestApi/Repositories/AzureResourceRepository.cs +++ b/ConsoleApps/AzureRestApi/AzureRestApi/Repositories/AzureResourceRepository.cs @@ -5,34 +5,17 @@ using System.Text.Json; namespace AzureRestApi.Repositories { - public class AzureResourceRepository + public class AzureResourceRepository : AzureBaseRepository { - private readonly HttpClient _httpClient; - private readonly AccessTokenRepository _accessTokenRepository; - - public AzureResourceRepository(HttpClient httpClient, AccessTokenRepository accessTokenRepository) + public AzureResourceRepository(HttpClient httpClient, AccessTokenRepository accessTokenRepository) : base(httpClient, accessTokenRepository) { - _httpClient = httpClient; - _accessTokenRepository = accessTokenRepository; } - public async Task?> GetAllResources(string subscriptionId) + public async Task> GetAll(string subscriptionId) { // GET https://management.azure.com/subscriptions/{subscriptionId}/resources?$filter={$filter}&$expand={$expand}&$top={$top}&api-version=2021-04-01 - var accessToken = await _accessTokenRepository.GetAccessToken(); - var uri = new Uri("https://management.azure.com/subscriptions/" + subscriptionId + "/resources?$expand=createdTime,changedTime&api-version=2021-04-01"); - - HttpRequestMessage request = new(HttpMethod.Get, uri); - request.Headers.Accept.Clear(); - request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); - request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); - - var response = await _httpClient.SendAsync(request).ConfigureAwait(false); - response.EnsureSuccessStatusCode(); - - var responseString = await response.Content.ReadAsStringAsync().ConfigureAwait(false); - - return JsonSerializer.Deserialize(responseString)?.value; + var uri = new Uri($"https://management.azure.com/subscriptions/{subscriptionId}/resources?$expand=createdTime,changedTime&api-version=2021-04-01"); + return await GetAllByUri(uri); } } } diff --git a/ConsoleApps/AzureRestApi/AzureRestApi/Repositories/AzureSubscriptionRepository.cs b/ConsoleApps/AzureRestApi/AzureRestApi/Repositories/AzureSubscriptionRepository.cs index c71a758..730ca5c 100644 --- a/ConsoleApps/AzureRestApi/AzureRestApi/Repositories/AzureSubscriptionRepository.cs +++ b/ConsoleApps/AzureRestApi/AzureRestApi/Repositories/AzureSubscriptionRepository.cs @@ -16,7 +16,7 @@ namespace AzureRestApi.Repositories _accessTokenRepository = accessTokenRepository; } - public async Task?> GetAllSubscriptions() + public async Task?> GetAll() { // GET https://management.azure.com/subscriptions?api-version=2020-01-01 @@ -33,7 +33,7 @@ namespace AzureRestApi.Repositories var responseString = await response.Content.ReadAsStringAsync().ConfigureAwait(false); - return JsonSerializer.Deserialize(responseString)?.value; + return JsonSerializer.Deserialize< ApiResponse>(responseString)?.value; } } } diff --git a/ConsoleApps/AzureRestApi/AzureRestApi/Services/CreatedOnDateService.cs b/ConsoleApps/AzureRestApi/AzureRestApi/Services/CreatedOnDateService.cs index 44e0689..00e9c35 100644 --- a/ConsoleApps/AzureRestApi/AzureRestApi/Services/CreatedOnDateService.cs +++ b/ConsoleApps/AzureRestApi/AzureRestApi/Services/CreatedOnDateService.cs @@ -1,4 +1,5 @@ using AzureRestApi.Repositories; +using Spectre.Console; namespace AzureRestApi.Services { @@ -18,44 +19,47 @@ namespace AzureRestApi.Services public async Task SetCreatedOnDateTags(bool skipDone = true) { - var defaultForeGround = Console.ForegroundColor; - Console.WriteLine("=============================================="); - Console.WriteLine("Setting CreatedOnDateTags"); - Console.WriteLine("=============================================="); + var rule = new Rule("[skyblue1]Setting CreatedOnDateTags[/]"); + rule.Alignment = Justify.Left; + rule.Style = Style.Parse("skyblue1"); + AnsiConsole.Write(rule); - var subscriptions = await _azureSubscriptionRepository.GetAllSubscriptions(); - if (subscriptions == null || !subscriptions.Any()) throw new Exception("No subscriptions found"); - subscriptions = subscriptions.Where(s => s.Enabled && !string.IsNullOrWhiteSpace(s.subscriptionId)).ToList(); - if (!subscriptions.Any()) throw new Exception("No subscriptions found"); - - foreach (var subscription in subscriptions) - { - Console.WriteLine("----------------------------------------------"); - Console.WriteLine(subscription.displayName); - Console.WriteLine("----------------------------------------------"); - - var resources = await _azureResourceRepository.GetAllResources(subscription.subscriptionId); - if (resources == null || !resources.Any()) continue; - resources = resources.Where(r => !string.IsNullOrWhiteSpace(r.CreatedOn) && !string.IsNullOrWhiteSpace(r.id)).ToList(); - - if (skipDone) + await AnsiConsole.Status() + .AutoRefresh(true) + .Spinner(Spinner.Known.Default) + .StartAsync("Retrieving subscriptions...", async ctx => { - resources = resources.Where(r => !r.tags.Any(t => t.Key == TagName && !string.IsNullOrWhiteSpace(t.Value))).ToList(); - } + var subscriptions = await _azureSubscriptionRepository.GetAll(); + if (subscriptions == null || !subscriptions.Any()) throw new Exception("No subscriptions found"); + subscriptions = subscriptions.Where(s => s.Enabled && !string.IsNullOrWhiteSpace(s.subscriptionId)).ToList(); + if (!subscriptions.Any()) throw new Exception("No subscriptions found"); - foreach (var resource in resources) - { - Console.Write(resource.id); - var tagSuccess = await _azureTagRepository.SetTag(resource.id, TagName, resource.CreatedOn); - - Console.ForegroundColor = tagSuccess ? ConsoleColor.DarkGreen : ConsoleColor.Red; - Console.Write(tagSuccess ? " V" : " X"); - Console.ForegroundColor = defaultForeGround; + foreach (var subscription in subscriptions) + { + rule = new Rule($"[skyblue1]{subscription.displayName}[/]"); + rule.Alignment = Justify.Left; + rule.Style = Style.Parse("skyblue1 dim"); + AnsiConsole.Write(rule); - Console.WriteLine(); - } - } - Console.WriteLine("=============================================="); + ctx.Status($"Getting the resources for subscription '{subscription.displayName}'"); + + var resources = await _azureResourceRepository.GetAll(subscription.subscriptionId); + if (resources == null || !resources.Any()) continue; + resources = resources.Where(r => !string.IsNullOrWhiteSpace(r.CreatedOn) && !string.IsNullOrWhiteSpace(r.id)).ToList(); + + if (skipDone) + { + resources = resources.Where(r => !r.tags.Any(t => t.Key == TagName && !string.IsNullOrWhiteSpace(t.Value))).ToList(); + } + + ctx.Status($"Setting created date tag on resources in subscription '{subscription.displayName}'"); + foreach (var resource in resources) + { + var tagSuccess = await _azureTagRepository.SetTag(resource.id, TagName, resource.CreatedOn); + AnsiConsole.MarkupLine($"{resource.id} " + (tagSuccess ? "[green]V[/]" : "[red]X[/]")); + } + } + }); } } } diff --git a/ConsoleApps/AzureRestApi/AzureRestApi/Services/DeploymentTypeService.cs b/ConsoleApps/AzureRestApi/AzureRestApi/Services/DeploymentTypeService.cs new file mode 100644 index 0000000..7687507 --- /dev/null +++ b/ConsoleApps/AzureRestApi/AzureRestApi/Services/DeploymentTypeService.cs @@ -0,0 +1,73 @@ +using AzureRestApi.Repositories; +using Spectre.Console; + +namespace AzureRestApi.Services +{ + public class DeploymentTypeService + { + private readonly AzureSubscriptionRepository _azureSubscriptionRepository; + private readonly AzureDeploymentRepository _azureDeploymentRepository; + private readonly AzureResourceGroupRepository _azureResourceGroupRepository; + + public DeploymentTypeService(AzureSubscriptionRepository azureSubscriptionRepository, AzureDeploymentRepository azureDeploymentRepository, AzureResourceGroupRepository azureResourceGroupRepository) + { + _azureSubscriptionRepository = azureSubscriptionRepository; + _azureDeploymentRepository = azureDeploymentRepository; + _azureResourceGroupRepository = azureResourceGroupRepository; + } + + public async Task CheckDeploymentTypes() + { + var rule = new Rule("[skyblue1]Checking deployment types[/]"); + rule.Alignment = Justify.Left; + rule.Style = Style.Parse("skyblue1"); + AnsiConsole.Write(rule); + + await AnsiConsole.Status() + .AutoRefresh(true) + .Spinner(Spinner.Known.Default) + .StartAsync("Retrieving subscriptions...", async ctx => + { + // Retrieve subscriptions + var subscriptions = await _azureSubscriptionRepository.GetAll(); + if (subscriptions == null || !subscriptions.Any()) throw new Exception("No subscriptions found"); + subscriptions = subscriptions.Where(s => s.Enabled && !string.IsNullOrWhiteSpace(s.subscriptionId)).ToList(); + if (!subscriptions.Any()) throw new Exception("No subscriptions found"); + + foreach (var subscription in subscriptions) + { + rule = new Rule($"[skyblue1]{subscription.displayName}[/]"); + rule.Alignment = Justify.Left; + rule.Style = Style.Parse("skyblue1 dim"); + AnsiConsole.Write(rule); + + ctx.Status($"Getting the deployments for subscription '{subscription.displayName}'"); + var deployments = await _azureDeploymentRepository.GetAllBySubscription(subscription.subscriptionId); + + if (deployments == null || !deployments.Any()) continue; + + foreach (var deployment in deployments) + { + AnsiConsole.WriteLine(Markup.Escape(deployment?.name)); + } + + ctx.Status($"Getting the resource groups for subscription '{subscription.displayName}'"); + var resourceGroups = await _azureResourceGroupRepository.GetAll(subscription.subscriptionId); + foreach (var resourceGroup in resourceGroups) + { + ctx.Status($"Getting the deployments for resourcegroup {resourceGroup.name} in subscription '{subscription.displayName}'"); + + var resourceGroupDeployments = await _azureDeploymentRepository.GetAllByResourceGroup(subscription.subscriptionId, resourceGroup.name); + foreach (var deployment in resourceGroupDeployments) + { + AnsiConsole.WriteLine(Markup.Escape($"{resourceGroup.name} - {deployment?.name}")); + } + } + + + + } + }); + } + } +} diff --git a/ConsoleApps/AzureRestApi/AzureRestApi/Services/OptionService.cs b/ConsoleApps/AzureRestApi/AzureRestApi/Services/OptionService.cs index 7682390..f39d931 100644 --- a/ConsoleApps/AzureRestApi/AzureRestApi/Services/OptionService.cs +++ b/ConsoleApps/AzureRestApi/AzureRestApi/Services/OptionService.cs @@ -1,5 +1,6 @@ using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; +using Spectre.Console; using System; using System.Collections.Generic; using System.Linq; @@ -12,45 +13,46 @@ namespace AzureRestApi.Services { private readonly ILogger _logger; private readonly CreatedOnDateService _createdOnDateService; + private readonly DeploymentTypeService _deploymentTypeService; - public OptionService(ILogger logger, CreatedOnDateService createdOnDateService) + public OptionService(ILogger logger, CreatedOnDateService createdOnDateService, DeploymentTypeService deploymentTypeService) { _logger = logger; _createdOnDateService = createdOnDateService; + _deploymentTypeService = deploymentTypeService; } public async Task StartAsync(CancellationToken cancellationToken) { - int userInput = 0; - bool validatedInput = false; - while (!validatedInput) - { - Console.WriteLine("================================================================"); - Console.WriteLine("Cloud Egineering Console App"); - Console.WriteLine("================================================================"); - Console.WriteLine("-- This couldn't be done with Powershell, so here we are.... "); - Console.WriteLine(); - Console.WriteLine("Select what you want to do:"); - Console.WriteLine("1. Update missing CreatedOnDate tags."); - Console.WriteLine("0. Exit."); + var rule = new Rule("[yellow]Cloud Egineering Console App[/]"); + AnsiConsole.Write(rule); - string? result = Console.ReadLine(); - bool parsed = int.TryParse(result, out userInput); + Console.WriteLine("-- This couldn't be done with Powershell, so here we are.... "); + Console.WriteLine(); - validatedInput = (parsed && (new[] { 1, 0 }).Contains(userInput)); + var choices = new[] + { + "Update missing CreatedOnDate tags.", + "Update missing IaC Deployment type tags.", + "Exit" + }; + var result = AnsiConsole.Prompt(new SelectionPrompt() + .Title("Select what you want to do:") + .PageSize(10) + .MoreChoicesText("[grey](Move up and down to reveal more choices)[/]") + .AddChoices(choices)); - if (!validatedInput) - { - Console.WriteLine("Please enter a valid choice"); - } - } - - if (userInput == 1) + if (result == choices[0]) { await _createdOnDateService.SetCreatedOnDateTags(true); } + else if (result == choices[1]) + { + await _deploymentTypeService.CheckDeploymentTypes(); + } - Console.WriteLine("Bye."); + rule = new Rule("[yellow]Done. Bye.[/]"); + AnsiConsole.Write(rule); } public Task StopAsync(CancellationToken cancellationToken)