- Added deployments

- Cleanup up code to generic responses and http methods
- Added ansiconsole
This commit is contained in:
Jurjen Ladenius
2022-04-21 11:30:11 +02:00
parent 20a2f78757
commit 0a422b3c98
18 changed files with 292 additions and 103 deletions

View File

@@ -15,6 +15,7 @@
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="6.0.0" /> <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" /> <PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" />
<PackageReference Include="Microsoft.IdentityModel.Clients.ActiveDirectory" Version="5.2.9" /> <PackageReference Include="Microsoft.IdentityModel.Clients.ActiveDirectory" Version="5.2.9" />
<PackageReference Include="Spectre.Console" Version="0.44.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@@ -0,0 +1,7 @@
namespace AzureRestApi.Models.Api
{
public class ApiResponse<T>
{
public List<T>? value { get; set; }
}
}

View File

@@ -1,9 +0,0 @@
using AzureRestApi.Models.Resources;
namespace AzureRestApi.Models.Api
{
public class ResourcesResponse
{
public List<Resource>? value { get; set; }
}
}

View File

@@ -1,9 +0,0 @@
using AzureRestApi.Models.Resources;
namespace AzureRestApi.Models.Api
{
public class SubscriptionResponse
{
public List<Subscription>? value { get; set; }
}
}

View File

@@ -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; }
}
}

View File

@@ -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<Resource>? outputResources { get; set; }
}
}

View File

@@ -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<string, string> tags { get; set; } = new Dictionary<string, string>();
public DeploymentProperties? properties { get; set; }
}
}

View File

@@ -0,0 +1,7 @@
namespace AzureRestApi.Models.Resources
{
public class ResourceGroupProperties
{
public string? provisioningState { get; set; }
}
}

View File

@@ -2,7 +2,7 @@
{ {
public class Settings public class Settings
{ {
public string KeyVaultName { get; set; } public string KeyVaultName { get; set; } = string.Empty;
public string AzureTenantId { get; set; } public string AzureTenantId { get; set; } = string.Empty;
} }
} }

View File

@@ -27,10 +27,13 @@ namespace AzureRestApi
services.AddSingleton(settings); services.AddSingleton(settings);
services.AddHttpClient<AzureSubscriptionRepository>(); services.AddHttpClient<AzureSubscriptionRepository>();
services.AddHttpClient<AzureDeploymentRepository>();
services.AddHttpClient<AzureResourceRepository>(); services.AddHttpClient<AzureResourceRepository>();
services.AddHttpClient<AzureTagRepository>(); services.AddHttpClient<AzureTagRepository>();
services.AddHttpClient<AzureResourceGroupRepository>();
services.AddScoped<CreatedOnDateService>(); services.AddScoped<CreatedOnDateService>();
services.AddScoped<DeploymentTypeService>();
services.AddHostedService<OptionService>(); services.AddHostedService<OptionService>();
} ); } );

View File

@@ -0,0 +1,44 @@
using AzureRestApi.Models.Api;
using System.Net.Http.Headers;
using System.Text.Json;
namespace AzureRestApi.Repositories
{
public abstract class AzureBaseRepository<T>
{
private readonly HttpClient _httpClient;
private readonly AccessTokenRepository _accessTokenRepository;
public AzureBaseRepository(HttpClient httpClient, AccessTokenRepository accessTokenRepository)
{
_httpClient = httpClient;
_accessTokenRepository = accessTokenRepository;
}
protected async Task<List<T>> 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<ApiResponse<T>>(responseString)?.value;
return result ?? new List<T>();
}
catch
{
return new List<T>();
}
}
}
}

View File

@@ -0,0 +1,25 @@
using AzureRestApi.Models.Resources;
namespace AzureRestApi.Repositories
{
public class AzureDeploymentRepository : AzureBaseRepository<Deployment>
{
public AzureDeploymentRepository(HttpClient httpClient, AccessTokenRepository accessTokenRepository) : base(httpClient, accessTokenRepository)
{
}
public async Task<List<Deployment>> 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<List<Deployment>> 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);
}
}
}

View File

@@ -0,0 +1,18 @@
using AzureRestApi.Models.Resources;
namespace AzureRestApi.Repositories
{
public class AzureResourceGroupRepository : AzureBaseRepository<ResourceGroup>
{
public AzureResourceGroupRepository(HttpClient httpClient, AccessTokenRepository accessTokenRepository) : base(httpClient, accessTokenRepository)
{
}
public async Task<List<ResourceGroup>> 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);
}
}
}

View File

@@ -5,34 +5,17 @@ using System.Text.Json;
namespace AzureRestApi.Repositories namespace AzureRestApi.Repositories
{ {
public class AzureResourceRepository public class AzureResourceRepository : AzureBaseRepository<Resource>
{ {
private readonly HttpClient _httpClient; public AzureResourceRepository(HttpClient httpClient, AccessTokenRepository accessTokenRepository) : base(httpClient, accessTokenRepository)
private readonly AccessTokenRepository _accessTokenRepository;
public AzureResourceRepository(HttpClient httpClient, AccessTokenRepository accessTokenRepository)
{ {
_httpClient = httpClient;
_accessTokenRepository = accessTokenRepository;
} }
public async Task<List<Resource>?> GetAllResources(string subscriptionId) public async Task<List<Resource>> GetAll(string subscriptionId)
{ {
// GET https://management.azure.com/subscriptions/{subscriptionId}/resources?$filter={$filter}&$expand={$expand}&$top={$top}&api-version=2021-04-01 // 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");
var uri = new Uri("https://management.azure.com/subscriptions/" + subscriptionId + "/resources?$expand=createdTime,changedTime&api-version=2021-04-01"); return await GetAllByUri(uri);
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<ResourcesResponse>(responseString)?.value;
} }
} }
} }

View File

@@ -16,7 +16,7 @@ namespace AzureRestApi.Repositories
_accessTokenRepository = accessTokenRepository; _accessTokenRepository = accessTokenRepository;
} }
public async Task<List<Subscription>?> GetAllSubscriptions() public async Task<List<Subscription>?> GetAll()
{ {
// GET https://management.azure.com/subscriptions?api-version=2020-01-01 // 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); var responseString = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
return JsonSerializer.Deserialize<SubscriptionResponse>(responseString)?.value; return JsonSerializer.Deserialize< ApiResponse<Subscription>>(responseString)?.value;
} }
} }
} }

View File

@@ -1,4 +1,5 @@
using AzureRestApi.Repositories; using AzureRestApi.Repositories;
using Spectre.Console;
namespace AzureRestApi.Services namespace AzureRestApi.Services
{ {
@@ -18,44 +19,47 @@ namespace AzureRestApi.Services
public async Task SetCreatedOnDateTags(bool skipDone = true) public async Task SetCreatedOnDateTags(bool skipDone = true)
{ {
var defaultForeGround = Console.ForegroundColor; var rule = new Rule("[skyblue1]Setting CreatedOnDateTags[/]");
Console.WriteLine("=============================================="); rule.Alignment = Justify.Left;
Console.WriteLine("Setting CreatedOnDateTags"); rule.Style = Style.Parse("skyblue1");
Console.WriteLine("=============================================="); AnsiConsole.Write(rule);
var subscriptions = await _azureSubscriptionRepository.GetAllSubscriptions(); await AnsiConsole.Status()
if (subscriptions == null || !subscriptions.Any()) throw new Exception("No subscriptions found"); .AutoRefresh(true)
subscriptions = subscriptions.Where(s => s.Enabled && !string.IsNullOrWhiteSpace(s.subscriptionId)).ToList(); .Spinner(Spinner.Known.Default)
if (!subscriptions.Any()) throw new Exception("No subscriptions found"); .StartAsync("Retrieving subscriptions...", async ctx =>
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)
{ {
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) foreach (var subscription in subscriptions)
{ {
Console.Write(resource.id); rule = new Rule($"[skyblue1]{subscription.displayName}[/]");
var tagSuccess = await _azureTagRepository.SetTag(resource.id, TagName, resource.CreatedOn); rule.Alignment = Justify.Left;
rule.Style = Style.Parse("skyblue1 dim");
AnsiConsole.Write(rule);
Console.ForegroundColor = tagSuccess ? ConsoleColor.DarkGreen : ConsoleColor.Red; ctx.Status($"Getting the resources for subscription '{subscription.displayName}'");
Console.Write(tagSuccess ? " V" : " X");
Console.ForegroundColor = defaultForeGround;
Console.WriteLine(); 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();
Console.WriteLine("==============================================");
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[/]"));
}
}
});
} }
} }
} }

View File

@@ -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}"));
}
}
}
});
}
}
}

View File

@@ -1,5 +1,6 @@
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Spectre.Console;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
@@ -12,45 +13,46 @@ namespace AzureRestApi.Services
{ {
private readonly ILogger<OptionService> _logger; private readonly ILogger<OptionService> _logger;
private readonly CreatedOnDateService _createdOnDateService; private readonly CreatedOnDateService _createdOnDateService;
private readonly DeploymentTypeService _deploymentTypeService;
public OptionService(ILogger<OptionService> logger, CreatedOnDateService createdOnDateService) public OptionService(ILogger<OptionService> logger, CreatedOnDateService createdOnDateService, DeploymentTypeService deploymentTypeService)
{ {
_logger = logger; _logger = logger;
_createdOnDateService = createdOnDateService; _createdOnDateService = createdOnDateService;
_deploymentTypeService = deploymentTypeService;
} }
public async Task StartAsync(CancellationToken cancellationToken) public async Task StartAsync(CancellationToken cancellationToken)
{ {
int userInput = 0; var rule = new Rule("[yellow]Cloud Egineering Console App[/]");
bool validatedInput = false; AnsiConsole.Write(rule);
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.");
string? result = Console.ReadLine(); Console.WriteLine("-- This couldn't be done with Powershell, so here we are.... ");
bool parsed = int.TryParse(result, out userInput); 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<string>()
.Title("Select what you want to do:")
.PageSize(10)
.MoreChoicesText("[grey](Move up and down to reveal more choices)[/]")
.AddChoices(choices));
if (!validatedInput) if (result == choices[0])
{
Console.WriteLine("Please enter a valid choice");
}
}
if (userInput == 1)
{ {
await _createdOnDateService.SetCreatedOnDateTags(true); 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) public Task StopAsync(CancellationToken cancellationToken)