Added Creatondate consoleapp and tagging policy

This commit is contained in:
Jurjen Ladenius
2022-04-20 11:03:12 +02:00
parent 28b4a0807c
commit 20331593e7
24 changed files with 1887 additions and 0 deletions

View File

@@ -0,0 +1,25 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.1.32328.378
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AzureRestApi", "AzureRestApi\AzureRestApi.csproj", "{DCEBAB23-4E5B-4FBE-9752-EC9C22E5C394}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{DCEBAB23-4E5B-4FBE-9752-EC9C22E5C394}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{DCEBAB23-4E5B-4FBE-9752-EC9C22E5C394}.Debug|Any CPU.Build.0 = Debug|Any CPU
{DCEBAB23-4E5B-4FBE-9752-EC9C22E5C394}.Release|Any CPU.ActiveCfg = Release|Any CPU
{DCEBAB23-4E5B-4FBE-9752-EC9C22E5C394}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {9F84ED05-3BCF-4BA7-B4ED-FEF688F8A2D0}
EndGlobalSection
EndGlobal

View File

@@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.Identity" Version="1.6.0" />
<PackageReference Include="Azure.Security.KeyVault.Secrets" Version="4.3.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="6.0.1" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" />
<PackageReference Include="Microsoft.IdentityModel.Clients.ActiveDirectory" Version="5.2.9" />
</ItemGroup>
<ItemGroup>
<None Update="appsettings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

View File

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

View File

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

View File

@@ -0,0 +1,9 @@
using AzureRestApi.Models.Resources;
namespace AzureRestApi.Models.Api
{
public class TagProperties
{
public TagTags properties { get; set; } = new TagTags();
}
}

View File

@@ -0,0 +1,28 @@
namespace AzureRestApi.Models.Resources
{
public class Resource
{
public string? id { get; set; }
public string? name { get; set; }
public string? type { get; set; }
public string? location { get; set; }
public string? createdTime { get; set; }
public string? changedTime { get; set; }
public Dictionary<string, string> tags { get; set; } = new Dictionary<string, string>();
public string? CreatedOn
{
get
{
if (string.IsNullOrWhiteSpace(createdTime))
{
return changedTime;
}
else
{
return createdTime;
}
}
}
}
}

View File

@@ -0,0 +1,20 @@
namespace AzureRestApi.Models.Resources
{
public class Subscription
{
public string? id { get; set; }
public string? authorizationSource { get; set; }
public string? subscriptionId { get; set; }
public string? tenantId { get; set; }
public string? displayName { get; set; }
public string? state { get; set; }
public bool Enabled
{
get
{
return state == "Enabled";
}
}
}
}

View File

@@ -0,0 +1,7 @@
namespace AzureRestApi.Models.Resources
{
public class TagTags
{
public Dictionary<string, string> tags { get; set; } = new Dictionary<string, string>();
}
}

View File

@@ -0,0 +1,8 @@
namespace AzureRestApi.Models
{
public class Settings
{
public string KeyVaultName { get; set; }
public string AzureTenantId { get; set; }
}
}

View File

@@ -0,0 +1,38 @@
using AzureRestApi.Models;
using AzureRestApi.Repositories;
using AzureRestApi.Services;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace AzureRestApi
{
class Program
{
static Task Main(string[] args) =>
CreateHostBuilder(args).Build().RunAsync();
static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureServices((builder, services) =>
{
IConfiguration config = new ConfigurationBuilder()
.AddJsonFile("appsettings.json")
.AddEnvironmentVariables()
.Build();
Settings settings = config.GetRequiredSection("Settings").Get<Settings>();
services.AddSingleton<AccessTokenRepository>();
services.AddSingleton(settings);
services.AddHttpClient<AzureSubscriptionRepository>();
services.AddHttpClient<AzureResourceRepository>();
services.AddHttpClient<AzureTagRepository>();
services.AddScoped<CreatedOnDateService>();
services.AddHostedService<OptionService>();
} );
}
}

View File

@@ -0,0 +1,40 @@
using Azure.Identity;
using Azure.Security.KeyVault.Secrets;
using AzureRestApi.Models;
using Microsoft.IdentityModel.Clients.ActiveDirectory;
namespace AzureRestApi.Repositories
{
public class AccessTokenRepository
{
private readonly Settings _settings;
private string? _accessToken;
public AccessTokenRepository(Settings settings)
{
_settings = settings;
}
public async Task<string> GetAccessToken()
{
if (!string.IsNullOrWhiteSpace(_accessToken)) return _accessToken;
var keyvaultUri = "https://" + _settings.KeyVaultName + ".vault.azure.net";
var credential = new DefaultAzureCredential();
var client = new SecretClient(new Uri(keyvaultUri), credential);
var clientId = (await client.GetSecretAsync("ClientID")).Value.Value;
var clientSecret = (await client.GetSecretAsync("ClientSecret")).Value.Value;
ClientCredential cc = new(clientId, clientSecret);
var context = new AuthenticationContext("https://login.microsoftonline.com/" + _settings.AzureTenantId);
var result = context.AcquireTokenAsync("https://management.azure.com/", cc);
if (result == null)
{
throw new InvalidOperationException("Failed to obtain the Access token");
}
_accessToken = result.Result.AccessToken;
return _accessToken;
}
}
}

View File

@@ -0,0 +1,38 @@
using AzureRestApi.Models.Api;
using AzureRestApi.Models.Resources;
using System.Net.Http.Headers;
using System.Text.Json;
namespace AzureRestApi.Repositories
{
public class AzureResourceRepository
{
private readonly HttpClient _httpClient;
private readonly AccessTokenRepository _accessTokenRepository;
public AzureResourceRepository(HttpClient httpClient, AccessTokenRepository accessTokenRepository)
{
_httpClient = httpClient;
_accessTokenRepository = accessTokenRepository;
}
public async Task<List<Resource>?> GetAllResources(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<ResourcesResponse>(responseString)?.value;
}
}
}

View File

@@ -0,0 +1,39 @@
using AzureRestApi.Models.Api;
using AzureRestApi.Models.Resources;
using System.Net.Http.Headers;
using System.Text.Json;
namespace AzureRestApi.Repositories
{
public class AzureSubscriptionRepository
{
private readonly HttpClient _httpClient;
private readonly AccessTokenRepository _accessTokenRepository;
public AzureSubscriptionRepository(HttpClient httpClient, AccessTokenRepository accessTokenRepository)
{
_httpClient = httpClient;
_accessTokenRepository = accessTokenRepository;
}
public async Task<List<Subscription>?> GetAllSubscriptions()
{
// GET https://management.azure.com/subscriptions?api-version=2020-01-01
var accessToken = await _accessTokenRepository.GetAccessToken();
var uri = new Uri("https://management.azure.com/subscriptions?api-version=2020-01-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<SubscriptionResponse>(responseString)?.value;
}
}
}

View File

@@ -0,0 +1,53 @@
using AzureRestApi.Models.Api;
using Microsoft.Extensions.Logging;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
namespace AzureRestApi.Repositories
{
public class AzureTagRepository
{
private readonly HttpClient _httpClient;
private readonly AccessTokenRepository _accessTokenRepository;
private readonly ILogger<AzureTagRepository> _logger;
public AzureTagRepository(HttpClient httpClient, AccessTokenRepository accessTokenRepository, ILogger<AzureTagRepository> logger)
{
_httpClient = httpClient;
_accessTokenRepository = accessTokenRepository;
_logger = logger;
}
public async Task<bool> SetTag(string scope, string name, string value)
{
// PUT https://management.azure.com/{scope}/providers/Microsoft.Resources/tags/default?api-version=2021-04-01
var result = true;
var accessToken = await _accessTokenRepository.GetAccessToken();
var uri = new Uri("https://management.azure.com/" + scope + "/providers/Microsoft.Resources/tags/default?api-version=2021-04-01");
var tagProperties = new TagProperties();
tagProperties.properties.tags.Add(name, value);
var body = JsonSerializer.Serialize(tagProperties);
HttpRequestMessage request = new(HttpMethod.Put, uri);
request.Headers.Accept.Clear();
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
request.Content = new StringContent(body, Encoding.UTF8, "application/json");
var response = await _httpClient.SendAsync(request).ConfigureAwait(false);
try
{
response.EnsureSuccessStatusCode();
await response.Content.ReadAsStringAsync().ConfigureAwait(false);
}
catch
{
result = false;
}
return result;
}
}
}

View File

@@ -0,0 +1,61 @@
using AzureRestApi.Repositories;
namespace AzureRestApi.Services
{
public class CreatedOnDateService
{
private const string TagName = "CreatedOnDate";
private readonly AzureSubscriptionRepository _azureSubscriptionRepository;
private readonly AzureResourceRepository _azureResourceRepository;
private readonly AzureTagRepository _azureTagRepository;
public CreatedOnDateService(AzureSubscriptionRepository azureSubscriptionRepository, AzureResourceRepository azureResourceRepository, AzureTagRepository azureTagRepository)
{
_azureSubscriptionRepository = azureSubscriptionRepository;
_azureResourceRepository = azureResourceRepository;
_azureTagRepository = azureTagRepository;
}
public async Task SetCreatedOnDateTags(bool skipDone = true)
{
var defaultForeGround = Console.ForegroundColor;
Console.WriteLine("==============================================");
Console.WriteLine("Setting CreatedOnDateTags");
Console.WriteLine("==============================================");
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)
{
resources = resources.Where(r => !r.tags.Any(t => t.Key == TagName && !string.IsNullOrWhiteSpace(t.Value))).ToList();
}
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;
Console.WriteLine();
}
}
Console.WriteLine("==============================================");
}
}
}

View File

@@ -0,0 +1,62 @@
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace AzureRestApi.Services
{
public class OptionService : IHostedService
{
private readonly ILogger<OptionService> _logger;
private readonly CreatedOnDateService _createdOnDateService;
public OptionService(ILogger<OptionService> logger, CreatedOnDateService createdOnDateService)
{
_logger = logger;
_createdOnDateService = createdOnDateService;
}
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.");
string? result = Console.ReadLine();
bool parsed = int.TryParse(result, out userInput);
validatedInput = (parsed && (new[] { 1, 0 }).Contains(userInput));
if (!validatedInput)
{
Console.WriteLine("Please enter a valid choice");
}
}
if (userInput == 1)
{
await _createdOnDateService.SetCreatedOnDateTags(true);
}
Console.WriteLine("Bye.");
}
public Task StopAsync(CancellationToken cancellationToken)
{
throw new NotImplementedException();
}
}
}

View File

@@ -0,0 +1,13 @@
{
"Settings": {
"KeyVaultName": "consoleapp",
"AzureTenantId": "e9792fd7-4044-47e7-a40d-3fba46f1cd09"
},
"Logging": {
"LogLevel": {
"Default": "Warning",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "None"
}
}
}

View File

@@ -0,0 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3.1</TargetFramework>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,12 @@
using System;
namespace ConsoleApp1
{
internal class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello World!");
}
}
}