jpda / azman-v2 Goto Github PK
View Code? Open in Web Editor NEWLicense: MIT License
License: MIT License
move this as default to configuration
{
// connect to azure
var resourceManagerClient = new ResourcesManagementClient(request.SubscriptionId, _tokenCredential);
try
{
var resourceGroupRequest = await resourceManagerClient.ResourceGroups.GetAsync(request.ResourceGroupName);
if (resourceGroupRequest == null) return;
var resourceGroup = resourceGroupRequest.Value;
// todo: move this as default to configuration
var newDate = request.DateCreated.AddDays(30).Date;
resourceGroup.Tags.TryAdd("expires", newDate.ToString("yyyy-MM-dd"));
resourceGroup.Tags.TryAdd("tagged-by", "thrazman");
resourceGroup.Tags.TryAdd("owner", request.CreatedByUser);
// service-msft-prod-azman-main-compute
var groupNamePieces = resourceGroup.Name.Split('-');
if (groupNamePieces.Count() >= 3)
{
resourceGroup.Tags.TryAdd("function", groupNamePieces[0]); // service
resourceGroup.Tags.TryAdd("customer", groupNamePieces[1]); // msft
resourceGroup.Tags.TryAdd("env", groupNamePieces[2]); // prod
resourceGroup.Tags.TryAdd("project", string.Join('-', groupNamePieces.Skip(3))); //azman-main-compute
}
await resourceManagerClient.ResourceGroups.CreateOrUpdateAsync(resourceGroup.Name, resourceGroup);
}
catch (System.Exception ex)
{
_log.LogError(ex, ex.Message);
throw;
}
}
// todo: explore changes required for using resource ID for _any_ resource
7ab7f422c2a2f536a1b004f2f3d3b3e03b3f06f3
arbitrary, could be a set amount (E.g., +30 days from now)
what happens when tagging fails? what's recoverable? what's not?
// handles queueing up new resource groups to be tagged
return new TagSuiteModel(
groupName: alert.data.context.activityLog.resourceGroupName,
subscriptionId: alert.data.context.activityLog.subscriptionId,
user: alert.data.context.activityLog.caller,
managementDate: alert.data.context.activityLog.eventTimestamp
);
}
public async Task AddTagSuite(TagSuiteModel request)
{
request.GenerateBaseTags();
await AddTags(request);
}
public async Task AddTags(TagModel request)
{
var resourceManagerClient = new ResourcesManagementClient(request.SubscriptionId, _tokenCredential);
var resourceGroupRequest = await resourceManagerClient.ResourceGroups.GetAsync(request.ResourceGroupName);
if (resourceGroupRequest == null) return;
var resourceGroup = resourceGroupRequest.Value;
try
{
foreach (var t in request.Tags)
{
resourceGroup.Tags.TryAdd(t.Key, t.Value);
}
await resourceManagerClient.ResourceGroups.CreateOrUpdateAsync(resourceGroup.Name, resourceGroup);
}
catch (Exception ex)
{
// todo: what happens when tagging fails? what's recoverable? what's not?
_log.LogError(ex, ex.Message);
}
}
public async Task AddTags(string resourceGroup, string subscriptionId, params KeyValuePair<string, string>[] tags)
{
await AddTags(new TagModel(resourceGroup, subscriptionId, tags));
}
// todo: explore changes required for using resource ID for _any_ resource
public async Task DeleteResource(string subscriptionId, string resourceGroupName)
{
e765261e4f672928e5bd499f85931420db9c12d1
update the return types with more arbitrary data
Line 135 in c85cc14
return await QueryResourceGraph(expiredQuery);
}
public async Task<IEnumerable<ResourceSearchResult>> FindSpecificResources(string expression)
{ // https://docs.microsoft.com/en-us/azure/virtual-machines/states-lifecycle
var runningVmsQuery = $@"resources | where type == 'microsoft.compute/virtualmachines'
| project id, subscriptionId, resourceGroup, name,
properties.extended.instanceView.powerState.displayStatus";
// todo: update the return types with more arbitrary data
return await QueryResourceGraph(runningVmsQuery);
}
// public async Task<IEnumerable<T>> FindSpecificResources<T>(string expression, Func<)
// {
// }
}
}
No newline at end of file
52e3802884b4cde0bf5d4f4d950563583ca15d8e
parse the token for expiration? dunno
public AccessTokenResponse GetAccessToken(string[] scopes, bool forceRefresh = false)
{
// todo: parse the token for expiration? dunno
var resource = ScopeUtil.GetResourceFromScope(scopes);
_log.LogTrace($"Fetching access token via MSI for {resource} ({string.Join(',', scopes)}); forcedRefresh: {forceRefresh}");
return new AccessTokenResponse(resource, _tokenProvider.GetAccessTokenAsync(resource, forceRefresh).Result, DateTimeOffset.UtcNow.AddHours(1));
}
public async Task<AccessTokenResponse> GetAccessTokenAsync(string[] scopes, bool forceRefresh = false)
{
// todo: parse the token for expiration? dunno
var resource = ScopeUtil.GetResourceFromScope(scopes);
_log.LogTrace($"Fetching access token via MSI for {resource} ({string.Join(',', scopes)}); forcedRefresh: {forceRefresh}");
return new AccessTokenResponse(resource, await _tokenProvider.GetAccessTokenAsync(resource, forceRefresh), DateTimeOffset.UtcNow.AddHours(1));
}
}
}
No newline at end of file
ndex 1994dc7..80ffd98 100644
++ b/AzureResourceManagementService.cs
35fd39a9636fc96532248bcc170adf162360bd14
make sure i'm not missing something here
azman-v2/AzureCliTokenProvider.cs
Line 47 in bcceb05
var resource = ScopeUtil.GetResourceFromScope(scopes);
_log.LogTrace($"Fetching access token via CLI for {resource} ({string.Join(',', scopes)}); forcedRefresh: {forceRefresh}");
// todo: make sure i'm not missing something here
if (!_tokens.ContainsKey(resource) || forceRefresh)
{
_tokens.AddOrUpdate(resource, x => GetToken(resource), (y, z) => GetToken(resource));
}
return _tokens.GetOrAdd(resource, x => GetToken(resource));
}
public Task<AccessTokenResponse> GetAccessTokenAsync(string[] scopes, bool forceRefresh = false)
ndex 80ffd98..49f2528 100644
++ b/AzureResourceManagementService.cs
5ba38e3b71179f5fce02b8f46c4dd5aef3b9688d
tweak based on output and ease of re-deploy
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Azure.Identity;
using Azure.ResourceManager.Resources;
using Microsoft.Azure.Services.AppAuthentication;
using azman_v2.Model;
namespace azman_v2
{
public class AzureResourceManagementService : IResourceManagementService
{
private readonly HttpClient _httpClient;
private readonly ILogger<AzureResourceManagementService> _log;
public AzureResourceManagementService(IHttpClientFactory httpFactory, ILoggerFactory loggerFactory)
{
_httpClient = httpFactory.CreateClient();
_log = loggerFactory.CreateLogger<AzureResourceManagementService>();
}
public TaggingRequestModel? ProcessAlert(dynamic alert)
{
if (!string.Equals(alert.data.context.activityLog.status.ToString(), "Succeeded", StringComparison.OrdinalIgnoreCase) ||
!string.Equals(alert.data.context.activityLog.subStatus.ToString(), "Created", StringComparison.OrdinalIgnoreCase))
{
_log.LogTrace($"status: {alert.data.context.activityLog.status}");
_log.LogTrace($"subStatus: {alert.data.context.activityLog.subStatus}");
//return new OkResult(); // return 200, we're done here since it hasn't succeeded yet
return null;
}
_log.LogTrace($"{alert.data.context.activityLog.resourceGroupName}");
// handles queueing up new resource groups to be tagged
return new TaggingRequestModel(
groupName: alert.data.context.activityLog.resourceGroupName,
user: alert.data.context.activityLog.caller,
subscriptionId: alert.data.context.activityLog.subscriptionId,
created: alert.data.context.activityLog.eventTimestamp
);
}
public async Task TagResource(TaggingRequestModel request)
{
// connect to azure
var resourceManagerClient = new ResourcesManagementClient(request.SubscriptionId, new DefaultAzureCredential());
var resourceGroupRequest = await resourceManagerClient.ResourceGroups.GetAsync(request.ResourceGroupName);
if (resourceGroupRequest == null) return;
var resourceGroup = resourceGroupRequest.Value;
// todo: move this as default to configuration
var newDate = request.DateCreated.AddDays(30).Date;
resourceGroup.Tags.TryAdd("expires", newDate.ToString("yyyy-MM-dd"));
resourceGroup.Tags.TryAdd("tagged-by", "thrazman");
resourceGroup.Tags.TryAdd("owner", request.CreatedByUser);
// service-msft-prod-azman-main-compute
var groupNamePieces = resourceGroup.Name.Split('-');
if (groupNamePieces.Count() >= 3)
{
resourceGroup.Tags.TryAdd("function", groupNamePieces[0]); // service
resourceGroup.Tags.TryAdd("customer", groupNamePieces[1]); // msft
resourceGroup.Tags.TryAdd("env", groupNamePieces[2]); // prod
resourceGroup.Tags.TryAdd("project", string.Join('-', groupNamePieces.Skip(3))); //azman-main-compute
}
await resourceManagerClient.ResourceGroups.CreateOrUpdateAsync(resourceGroup.Name, resourceGroup);
}
// todo: explore changes required for using resource ID for _any_ resource
public async Task DeleteResource(string subscriptionId, string resourceGroupName)
{
// todo: what-if? --> log deletion, but don't execute
// connect to azure
_log.LogInformation($"Request to delete {resourceGroupName} from subscription {subscriptionId}");
var resourceManagerClient = new ResourcesManagementClient(subscriptionId, new DefaultAzureCredential());
await resourceManagerClient.ResourceGroups.StartDeleteAsync(resourceGroupName);
}
public async Task<string> ExportResourceGroupTemplateByName(string subscriptionId, string groupName)
{
// todo: tweak based on output and ease of re-deploy
var resourceManagerClient = new ResourcesManagementClient(subscriptionId, new DefaultAzureCredential());
var exportedTemplate = await resourceManagerClient.ResourceGroups.StartExportTemplateAsync(groupName,
new Azure.ResourceManager.Resources.Models.ExportTemplateRequest());
if (exportedTemplate.HasValue) return (string)exportedTemplate.Value.Template; // todo: y tho?
return string.Empty;
}
}
}
No newline at end of file
ew file mode 100644
ndex 0000000..2336ca1
++ b/IResourceManagementService.cs
45e33f7590fba875e15d2a775d0bbcc7a1a0207d
this is required but read-only? hmm
azman-v2/AzureResourceManagementService.cs
Line 111 in 9c46e4b
{
var token = await _tokenProvider.GetAccessTokenAsync(new[] { "https://management.azure.com/" });
_httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token.Token);
var body = new StringContent("{'resources':[ '*' ]}", System.Text.Encoding.UTF8, "application/json");
var request = await _httpClient.PostAsync($"https://management.azure.com/subscriptions/{subscriptionId}/resourcegroups/{groupName}/exportTemplate?api-version=2020-06-01", body);
if (!request.IsSuccessStatusCode) return string.Empty;
// todo: handle 202 accepted on export request, sigh
var templateData = await request.Content.ReadAsStringAsync();
return templateData;
// POST https://management.azure.com/subscriptions/{subscriptionId}/resourcegroups/{resourceGroupName}/exportTemplate?api-version=2020-06-01
// todo: tweak based on output and ease of re-deploy
// var resourceManagerClient = new ResourcesManagementClient(subscriptionId, _tokenCredential);
// var exportedTemplate = await resourceManagerClient.ResourceGroups.StartExportTemplateAsync(groupName,
// todo: this is required but read-only? hmm
//new Azure.ResourceManager.Resources.Models.ExportTemplateRequest() { Resources = new[] { "*" } });
// new Azure.ResourceManager.Resources.Models.ExportTemplateRequest() { });
// if (exportedTemplate.HasValue) return (string)exportedTemplate.Value.Template; // todo: y tho?
//return string.Empty;
}
}
}
No newline at end of file
ndex 85f3af9..3c4161a 100644
++ b/Functions.cs
867b86203e22dc1451c38468fbc8ab30d421d07c
y tho?
azman-v2/OnResourceGroupCreate.cs
Line 102 in e052c6a
{
// todo: tweak based on output and ease of re-deploy
var resourceManagerClient = new ResourcesManagementClient(subscriptionId, new DefaultAzureCredential());
var exportedTemplate = await resourceManagerClient.ResourceGroups.StartExportTemplateAsync(groupName,
new Azure.ResourceManager.Resources.Models.ExportTemplateRequest());
if (exportedTemplate.HasValue) return (string)exportedTemplate.Value.Template; // todo: y tho?
return string.Empty;
}
}
7f6b34a8a4f8b75c46de4daa7365b591ba0f9fdb
will DF be better? or do we stick to our own orchestration?
how well does DF scale?
is it overkill for what we need?
best candidate for durable functions
Line 99 in e052c6a
}
// todo: best candidate for durable functions
[FunctionName("ResourceGroupExpired")]
public async Task ResourceGroupExpired(
[QueueTrigger("%ResourceGroupExpiredQueueName%", Connection = "MainStorageConnection")] ResourceSearchResult request,
ndex 9937d22..5cb32e3 100644
++ b/OnResourceGroupCreate.cs
f37009bb7a6dadc2592c058549f29303e5a75bf0
parse the token for expiration? dunno
}
public AccessTokenResponse GetAccessToken(string[] scopes, bool forceRefresh = false)
{
// todo: parse the token for expiration? dunno
var resource = ScopeUtil.GetResourceFromScope(scopes);
_log.LogTrace($"Fetching access token via MSI for {resource} ({string.Join(',', scopes)}); forcedRefresh: {forceRefresh}");
return new AccessTokenResponse(resource, _tokenProvider.GetAccessTokenAsync(resource, forceRefresh).Result, DateTimeOffset.UtcNow.AddHours(1));
}
public async Task<AccessTokenResponse> GetAccessTokenAsync(string[] scopes, bool forceRefresh = false)
{
// todo: parse the token for expiration? dunno
var resource = ScopeUtil.GetResourceFromScope(scopes);
_log.LogTrace($"Fetching access token via MSI for {resource} ({string.Join(',', scopes)}); forcedRefresh: {forceRefresh}");
return new AccessTokenResponse(resource, await _tokenProvider.GetAccessTokenAsync(resource, forceRefresh), DateTimeOffset.UtcNow.AddHours(1));
}
}
}
No newline at end of file
ndex 1994dc7..80ffd98 100644
++ b/AzureResourceManagementService.cs
418ce66244aac9661212f6f23aa3f1ff31d792c0
Notification support
y tho?
azman-v2/AzureResourceManagementService.cs
Line 114 in 9c46e4b
var token = await _tokenProvider.GetAccessTokenAsync(new[] { "https://management.azure.com/" });
_httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token.Token);
var body = new StringContent("{'resources':[ '*' ]}", System.Text.Encoding.UTF8, "application/json");
var request = await _httpClient.PostAsync($"https://management.azure.com/subscriptions/{subscriptionId}/resourcegroups/{groupName}/exportTemplate?api-version=2020-06-01", body);
if (!request.IsSuccessStatusCode) return string.Empty;
// todo: handle 202 accepted on export request, sigh
var templateData = await request.Content.ReadAsStringAsync();
return templateData;
// POST https://management.azure.com/subscriptions/{subscriptionId}/resourcegroups/{resourceGroupName}/exportTemplate?api-version=2020-06-01
// todo: tweak based on output and ease of re-deploy
// var resourceManagerClient = new ResourcesManagementClient(subscriptionId, _tokenCredential);
// var exportedTemplate = await resourceManagerClient.ResourceGroups.StartExportTemplateAsync(groupName,
// todo: this is required but read-only? hmm
//new Azure.ResourceManager.Resources.Models.ExportTemplateRequest() { Resources = new[] { "*" } });
// new Azure.ResourceManager.Resources.Models.ExportTemplateRequest() { });
// if (exportedTemplate.HasValue) return (string)exportedTemplate.Value.Template; // todo: y tho?
//return string.Empty;
}
}
}
No newline at end of file
ndex 85f3af9..3c4161a 100644
++ b/Functions.cs
b4598f74e595b171d5c2e576e325b98db13e2194
hack for testing until configurable subscription list is available
azman-v2/OnResourceGroupCreate.cs
Line 137 in e052c6a
private async Task<IEnumerable<string>> FindAccessibleSubscriptions(bool forceRefresh = false)
{
// todo: hack for testing until configurable subscription list is available
_subscriptionIds.Add("e7048bdb-835c-440f-9304-aa4171382839");
if (!forceRefresh || _subscriptionIds.Any())
{
05da050c71063ea0326410206d81eb94db95a2a1
tweak based on output and ease of re-deploy
azman-v2/OnResourceGroupCreate.cs
Line 98 in e052c6a
public async Task<string> ExportResourceGroupTemplateByName(string subscriptionId, string groupName)
{
// todo: tweak based on output and ease of re-deploy
var resourceManagerClient = new ResourcesManagementClient(subscriptionId, new DefaultAzureCredential());
var exportedTemplate = await resourceManagerClient.ResourceGroups.StartExportTemplateAsync(groupName,
new Azure.ResourceManager.Resources.Models.ExportTemplateRequest());
if (exportedTemplate.HasValue) return (string)exportedTemplate.Value.Template; // todo: y tho?
return string.Empty;
}
}
b1929f3de25a03222d4a4c486eaf2bff5f3f26b2
configuration for national clouds, e.g., https://management.chinacloudapi.cn
Line 20 in 2768e6b
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Azure.Management.ResourceGraph;
using Microsoft.Azure.Management.ResourceGraph.Models;
namespace azman_v2
{
public class Scanner : IScanner
{
private readonly ITokenProvider _tokenProvider;
private readonly HttpClient _httpClient;
private readonly ILogger<Scanner> _log;
// todo: configuration for national clouds, e.g., https://management.chinacloudapi.cn
private readonly string _managementEndpoint = "https://management.azure.com/";
private readonly string _managementAzureAdResourceId = "https://management.azure.com/";
// todo: configuration to allow/deny specific subscriptions
private readonly List<string> _subscriptionIds;
public Scanner(ITokenProvider tokenProvider, IHttpClientFactory httpFactory, ILoggerFactory loggerFactory)
{
_tokenProvider = tokenProvider;
_httpClient = httpFactory.CreateClient();
_log = loggerFactory.CreateLogger<Scanner>();
_subscriptionIds = new List<string>();
}
private async Task<IEnumerable<string>> FindAccessibleSubscriptions(bool forceRefresh = false)
{
// todo: hack for testing until configurable subscription list is available
_subscriptionIds.Add("e7048bdb-835c-440f-9304-aa4171382839");
if (!forceRefresh || _subscriptionIds.Any())
{
return _subscriptionIds;
}
_log.LogTrace($"Getting subscription list starting at ${DateTime.UtcNow}");
_log.LogTrace($"Getting access token for ${_managementAzureAdResourceId}");
var token = await _tokenProvider.GetAccessTokenAsync(_managementAzureAdResourceId, false);
_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
// resource graph is expecting an array of subscriptions, so get the subscription list first
var subRequest = await _httpClient.GetAsync($"{_managementEndpoint}subscriptions?api-version=2020-01-01");
if (!subRequest.IsSuccessStatusCode)
{
_log.LogError(new EventId((int)subRequest.StatusCode, subRequest.StatusCode.ToString()), await subRequest.Content.ReadAsStringAsync());
return _subscriptionIds;
}
var data = await JsonDocument.ParseAsync(await subRequest.Content.ReadAsStreamAsync());
var subscriptionArray = data.RootElement.GetProperty("value").EnumerateArray();
var subscriptions = subscriptionArray.Select(x => x.GetProperty("subscriptionId").ToString());
_log.LogTrace($"Got subscription IDs: {string.Join(',', subscriptions)}");
_subscriptionIds.AddRange(subscriptions);
return _subscriptionIds;
}
private async Task<IEnumerable<ResourceSearchResult>> QueryResourceGraph(string queryText)
{
var subscriptions = await FindAccessibleSubscriptions();
var graphClient = new ResourceGraphClient(new Microsoft.Rest.TokenCredentials(await _tokenProvider.GetAccessTokenAsync(_managementAzureAdResourceId)));
var query = await graphClient.ResourcesAsync(new QueryRequest(subscriptions.ToList(), queryText));
var resources = new List<ResourceSearchResult>();
// the ResourceGraphClient uses Newtonsoft under the hood
if (((dynamic)query.Data).rows is Newtonsoft.Json.Linq.JArray j)
{
resources.AddRange(
j.Select(x => new ResourceSearchResult()
{
// I'm sure there is a better way here - looking at the columns property, for example,
// to find the position of the column in the row we're interested in - follows query order
// so for now, 0 & 1
ResourceId = x.ElementAt(0).ToString(),
SubscriptionId = x.ElementAt(1).ToString()
}));
}
return resources;
}
public async Task<IEnumerable<ResourceSearchResult>> ScanForUntaggedResources()
{
var untaggedQuery = @"resourcecontainers | where (isnull(tags.['expires'])) and
type == 'microsoft.resources/subscriptions/resourcegroups'
| project name, subscriptionId, id";
return await QueryResourceGraph(untaggedQuery);
}
public async Task<IEnumerable<ResourceSearchResult>> ScanForExpiredResources()
{
var expiredQuery = @"resourcecontainers | where (!isnull(tags.['expires']))
and type == 'microsoft.resources/subscriptions/resourcegroups'
and todatetime(tags['expires']) < now()
| project name, subscriptionId, id";
return await QueryResourceGraph(expiredQuery);
}
public async Task<IEnumerable<ResourceSearchResult>> ScanForExpiredResources(DateTimeOffset expirationDate)
{
// and todatetime(tags.['expires']) < now() + 14d
var expiredQuery = $@"resourcecontainers | where (!isnull(tags.['expires']))
and type == 'microsoft.resources/subscriptions/resourcegroups'
and todatetime(tags['expires']) < todatetime({expirationDate})
| project name, subscriptionId, id";
return await QueryResourceGraph(expiredQuery);
}
public async Task<IEnumerable<ResourceSearchResult>> ScanForExpiredResources(string kustoDateExpression)
{
// e.g., and todatetime(tags.['expires']) < now() + 3d
var expiredQuery = $@"resourcecontainers | where (!isnull(tags.['expires']))
and type == 'microsoft.resources/subscriptions/resourcegroups'
and todatetime(tags['expires']) < {kustoDateExpression}
| project name, subscriptionId, id";
return await QueryResourceGraph(expiredQuery);
}
}
}
No newline at end of file
b1bd7cab21b3f1ad7ca5cef17b74d862c084573d
what-if? --> log deletion, but don't execute
connect to azure
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Azure.Identity;
using Azure.ResourceManager.Resources;
using Microsoft.Azure.Services.AppAuthentication;
using azman_v2.Model;
namespace azman_v2
{
public class AzureResourceManagementService : IResourceManagementService
{
private readonly HttpClient _httpClient;
private readonly ILogger<AzureResourceManagementService> _log;
public AzureResourceManagementService(IHttpClientFactory httpFactory, ILoggerFactory loggerFactory)
{
_httpClient = httpFactory.CreateClient();
_log = loggerFactory.CreateLogger<AzureResourceManagementService>();
}
public TaggingRequestModel? ProcessAlert(dynamic alert)
{
if (!string.Equals(alert.data.context.activityLog.status.ToString(), "Succeeded", StringComparison.OrdinalIgnoreCase) ||
!string.Equals(alert.data.context.activityLog.subStatus.ToString(), "Created", StringComparison.OrdinalIgnoreCase))
{
_log.LogTrace($"status: {alert.data.context.activityLog.status}");
_log.LogTrace($"subStatus: {alert.data.context.activityLog.subStatus}");
//return new OkResult(); // return 200, we're done here since it hasn't succeeded yet
return null;
}
_log.LogTrace($"{alert.data.context.activityLog.resourceGroupName}");
// handles queueing up new resource groups to be tagged
return new TaggingRequestModel(
groupName: alert.data.context.activityLog.resourceGroupName,
user: alert.data.context.activityLog.caller,
subscriptionId: alert.data.context.activityLog.subscriptionId,
created: alert.data.context.activityLog.eventTimestamp
);
}
public async Task TagResource(TaggingRequestModel request)
{
// connect to azure
var resourceManagerClient = new ResourcesManagementClient(request.SubscriptionId, new DefaultAzureCredential());
var resourceGroupRequest = await resourceManagerClient.ResourceGroups.GetAsync(request.ResourceGroupName);
if (resourceGroupRequest == null) return;
var resourceGroup = resourceGroupRequest.Value;
// todo: move this as default to configuration
var newDate = request.DateCreated.AddDays(30).Date;
resourceGroup.Tags.TryAdd("expires", newDate.ToString("yyyy-MM-dd"));
resourceGroup.Tags.TryAdd("tagged-by", "thrazman");
resourceGroup.Tags.TryAdd("owner", request.CreatedByUser);
// service-msft-prod-azman-main-compute
var groupNamePieces = resourceGroup.Name.Split('-');
if (groupNamePieces.Count() >= 3)
{
resourceGroup.Tags.TryAdd("function", groupNamePieces[0]); // service
resourceGroup.Tags.TryAdd("customer", groupNamePieces[1]); // msft
resourceGroup.Tags.TryAdd("env", groupNamePieces[2]); // prod
resourceGroup.Tags.TryAdd("project", string.Join('-', groupNamePieces.Skip(3))); //azman-main-compute
}
await resourceManagerClient.ResourceGroups.CreateOrUpdateAsync(resourceGroup.Name, resourceGroup);
}
// todo: explore changes required for using resource ID for _any_ resource
public async Task DeleteResource(string subscriptionId, string resourceGroupName)
{
// todo: what-if? --> log deletion, but don't execute
// connect to azure
_log.LogInformation($"Request to delete {resourceGroupName} from subscription {subscriptionId}");
var resourceManagerClient = new ResourcesManagementClient(subscriptionId, new DefaultAzureCredential());
await resourceManagerClient.ResourceGroups.StartDeleteAsync(resourceGroupName);
}
public async Task<string> ExportResourceGroupTemplateByName(string subscriptionId, string groupName)
{
// todo: tweak based on output and ease of re-deploy
var resourceManagerClient = new ResourcesManagementClient(subscriptionId, new DefaultAzureCredential());
var exportedTemplate = await resourceManagerClient.ResourceGroups.StartExportTemplateAsync(groupName,
new Azure.ResourceManager.Resources.Models.ExportTemplateRequest());
if (exportedTemplate.HasValue) return (string)exportedTemplate.Value.Template; // todo: y tho?
return string.Empty;
}
}
}
No newline at end of file
ew file mode 100644
ndex 0000000..2336ca1
++ b/IResourceManagementService.cs
39eac02d018fd2d61d8c130fa9614832f21b6bd1
if location == null or empty, POST to export Uri
azman-v2/AzureResourceManagementService.cs
Line 133 in c85cc14
}
await Task.Delay(delayInSec * 1000);
// todo: if location == null or empty, POST to export Uri
request = await _httpClient.GetAsync(request.Headers.Location ?? exportUri);
}
10da3d852f4f90314390b7d5b976991e91eaefd4
start/stop/modify
deploy templates, etc.
A self-hosted github action runner would let us deploy via MSI, meaning no static credentials to rotate/manage in github
Deploy via self-hosted only on merge
Use cloud runners for PRs
this is required but read-only? hmm
new Azure.ResourceManager.Resources.Models.ExportTemplateRequest() { Resources = new[] { "*" } });
new Azure.ResourceManager.Resources.Models.ExportTemplateRequest() { });
if (exportedTemplate.HasValue) return (string)exportedTemplate.Value.Template; // todo: y tho?
azman-v2/AzureResourceManagementService.cs
Line 146 in c85cc14
// var resourceManagerClient = new ResourcesManagementClient(subscriptionId, _tokenCredential);
// var exportedTemplate = await resourceManagerClient.ResourceGroups.StartExportTemplateAsync(groupName,
// // todo: this is required but read-only? hmm
// new Azure.ResourceManager.Resources.Models.ExportTemplateRequest() { Resources = new[] { "*" } });
// new Azure.ResourceManager.Resources.Models.ExportTemplateRequest() { });
// if (exportedTemplate.HasValue) return (string)exportedTemplate.Value.Template; // todo: y tho?
// return string.Empty;
}
}
}
No newline at end of file
ndex 9181db4..9054115 100644
++ b/Functions.cs
39487691903813505a6bd5a4a56ed50f43a731bb
have a lot of checks to make sure deletion is enabled, actually what a user wants to do, etc.
what-if? --> log deletion, but don't execute
connect to azure
azman-v2/OnResourceGroupCreate.cs
Line 89 in e052c6a
// todo: explore changes required for using resource ID for _any_ resource
public async Task DeleteResource(string subscriptionId, string resourceGroupName)
{
// todo: what-if? --> log deletion, but don't execute
// connect to azure
_log.LogInformation($"Request to delete {resourceGroupName} from subscription {subscriptionId}");
var resourceManagerClient = new ResourcesManagementClient(subscriptionId, new DefaultAzureCredential());
4337719531cd61e4df28323998c14d7297ddba34
hack for testing until configurable subscription list is available
Line 36 in 2768e6b
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Azure.Management.ResourceGraph;
using Microsoft.Azure.Management.ResourceGraph.Models;
namespace azman_v2
{
public class Scanner : IScanner
{
private readonly ITokenProvider _tokenProvider;
private readonly HttpClient _httpClient;
private readonly ILogger<Scanner> _log;
// todo: configuration for national clouds, e.g., https://management.chinacloudapi.cn
private readonly string _managementEndpoint = "https://management.azure.com/";
private readonly string _managementAzureAdResourceId = "https://management.azure.com/";
// todo: configuration to allow/deny specific subscriptions
private readonly List<string> _subscriptionIds;
public Scanner(ITokenProvider tokenProvider, IHttpClientFactory httpFactory, ILoggerFactory loggerFactory)
{
_tokenProvider = tokenProvider;
_httpClient = httpFactory.CreateClient();
_log = loggerFactory.CreateLogger<Scanner>();
_subscriptionIds = new List<string>();
}
private async Task<IEnumerable<string>> FindAccessibleSubscriptions(bool forceRefresh = false)
{
// todo: hack for testing until configurable subscription list is available
_subscriptionIds.Add("e7048bdb-835c-440f-9304-aa4171382839");
if (!forceRefresh || _subscriptionIds.Any())
{
return _subscriptionIds;
}
_log.LogTrace($"Getting subscription list starting at ${DateTime.UtcNow}");
_log.LogTrace($"Getting access token for ${_managementAzureAdResourceId}");
var token = await _tokenProvider.GetAccessTokenAsync(_managementAzureAdResourceId, false);
_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
// resource graph is expecting an array of subscriptions, so get the subscription list first
var subRequest = await _httpClient.GetAsync($"{_managementEndpoint}subscriptions?api-version=2020-01-01");
if (!subRequest.IsSuccessStatusCode)
{
_log.LogError(new EventId((int)subRequest.StatusCode, subRequest.StatusCode.ToString()), await subRequest.Content.ReadAsStringAsync());
return _subscriptionIds;
}
var data = await JsonDocument.ParseAsync(await subRequest.Content.ReadAsStreamAsync());
var subscriptionArray = data.RootElement.GetProperty("value").EnumerateArray();
var subscriptions = subscriptionArray.Select(x => x.GetProperty("subscriptionId").ToString());
_log.LogTrace($"Got subscription IDs: {string.Join(',', subscriptions)}");
_subscriptionIds.AddRange(subscriptions);
return _subscriptionIds;
}
private async Task<IEnumerable<ResourceSearchResult>> QueryResourceGraph(string queryText)
{
var subscriptions = await FindAccessibleSubscriptions();
var graphClient = new ResourceGraphClient(new Microsoft.Rest.TokenCredentials(await _tokenProvider.GetAccessTokenAsync(_managementAzureAdResourceId)));
var query = await graphClient.ResourcesAsync(new QueryRequest(subscriptions.ToList(), queryText));
var resources = new List<ResourceSearchResult>();
// the ResourceGraphClient uses Newtonsoft under the hood
if (((dynamic)query.Data).rows is Newtonsoft.Json.Linq.JArray j)
{
resources.AddRange(
j.Select(x => new ResourceSearchResult()
{
// I'm sure there is a better way here - looking at the columns property, for example,
// to find the position of the column in the row we're interested in - follows query order
// so for now, 0 & 1
ResourceId = x.ElementAt(0).ToString(),
SubscriptionId = x.ElementAt(1).ToString()
}));
}
return resources;
}
public async Task<IEnumerable<ResourceSearchResult>> ScanForUntaggedResources()
{
var untaggedQuery = @"resourcecontainers | where (isnull(tags.['expires'])) and
type == 'microsoft.resources/subscriptions/resourcegroups'
| project name, subscriptionId, id";
return await QueryResourceGraph(untaggedQuery);
}
public async Task<IEnumerable<ResourceSearchResult>> ScanForExpiredResources()
{
var expiredQuery = @"resourcecontainers | where (!isnull(tags.['expires']))
and type == 'microsoft.resources/subscriptions/resourcegroups'
and todatetime(tags['expires']) < now()
| project name, subscriptionId, id";
return await QueryResourceGraph(expiredQuery);
}
public async Task<IEnumerable<ResourceSearchResult>> ScanForExpiredResources(DateTimeOffset expirationDate)
{
// and todatetime(tags.['expires']) < now() + 14d
var expiredQuery = $@"resourcecontainers | where (!isnull(tags.['expires']))
and type == 'microsoft.resources/subscriptions/resourcegroups'
and todatetime(tags['expires']) < todatetime({expirationDate})
| project name, subscriptionId, id";
return await QueryResourceGraph(expiredQuery);
}
public async Task<IEnumerable<ResourceSearchResult>> ScanForExpiredResources(string kustoDateExpression)
{
// e.g., and todatetime(tags.['expires']) < now() + 3d
var expiredQuery = $@"resourcecontainers | where (!isnull(tags.['expires']))
and type == 'microsoft.resources/subscriptions/resourcegroups'
and todatetime(tags['expires']) < {kustoDateExpression}
| project name, subscriptionId, id";
return await QueryResourceGraph(expiredQuery);
}
}
}
No newline at end of file
02f7c572919e1cccc7b77442c5e743720532e0f0
Need to delete these resources post-expiration
configuration to allow/deny specific subscriptions
azman-v2/OnResourceGroupCreate.cs
Line 124 in e052c6a
// todo: configuration for national clouds, e.g., https://management.chinacloudapi.cn
private readonly string _managementEndpoint = "https://management.azure.com/";
private readonly string _managementAzureAdResourceId = "https://management.azure.com/";
// todo: configuration to allow/deny specific subscriptions
private readonly List<string> _subscriptionIds;
public Scanner(ITokenProvider tokenProvider, IHttpClientFactory httpFactory, ILoggerFactory loggerFactory)
5e4120d00b8582357e22961f6a01f2d17d72ed9c
move this as default to configuration
azman-v2/OnResourceGroupCreate.cs
Line 67 in e052c6a
var resourceGroup = resourceGroupRequest.Value;
// todo: move this as default to configuration
var newDate = request.DateCreated.AddDays(30).Date;
resourceGroup.Tags.TryAdd("expires", newDate.ToString("yyyy-MM-dd"));
b6059d328e2aa1a610b8d6a0eeb7d5e8327f90a6
y tho?
using Microsoft.Extensions.Logging;
using Azure.Identity;
using Azure.ResourceManager.Resources;
using Microsoft.Azure.Services.AppAuthentication;
using azman_v2.Model;
namespace azman_v2
{
public class AzureResourceManagementService : IResourceManagementService
{
private readonly HttpClient _httpClient;
private readonly ILogger<AzureResourceManagementService> _log;
public AzureResourceManagementService(IHttpClientFactory httpFactory, ILoggerFactory loggerFactory)
{
_httpClient = httpFactory.CreateClient();
_log = loggerFactory.CreateLogger<AzureResourceManagementService>();
}
public TaggingRequestModel? ProcessAlert(dynamic alert)
{
if (!string.Equals(alert.data.context.activityLog.status.ToString(), "Succeeded", StringComparison.OrdinalIgnoreCase) ||
!string.Equals(alert.data.context.activityLog.subStatus.ToString(), "Created", StringComparison.OrdinalIgnoreCase))
{
_log.LogTrace($"status: {alert.data.context.activityLog.status}");
_log.LogTrace($"subStatus: {alert.data.context.activityLog.subStatus}");
//return new OkResult(); // return 200, we're done here since it hasn't succeeded yet
return null;
}
_log.LogTrace($"{alert.data.context.activityLog.resourceGroupName}");
// handles queueing up new resource groups to be tagged
return new TaggingRequestModel(
groupName: alert.data.context.activityLog.resourceGroupName,
user: alert.data.context.activityLog.caller,
subscriptionId: alert.data.context.activityLog.subscriptionId,
created: alert.data.context.activityLog.eventTimestamp
);
}
public async Task TagResource(TaggingRequestModel request)
{
// connect to azure
var resourceManagerClient = new ResourcesManagementClient(request.SubscriptionId, new DefaultAzureCredential());
var resourceGroupRequest = await resourceManagerClient.ResourceGroups.GetAsync(request.ResourceGroupName);
if (resourceGroupRequest == null) return;
var resourceGroup = resourceGroupRequest.Value;
// todo: move this as default to configuration
var newDate = request.DateCreated.AddDays(30).Date;
resourceGroup.Tags.TryAdd("expires", newDate.ToString("yyyy-MM-dd"));
resourceGroup.Tags.TryAdd("tagged-by", "thrazman");
resourceGroup.Tags.TryAdd("owner", request.CreatedByUser);
// service-msft-prod-azman-main-compute
var groupNamePieces = resourceGroup.Name.Split('-');
if (groupNamePieces.Count() >= 3)
{
resourceGroup.Tags.TryAdd("function", groupNamePieces[0]); // service
resourceGroup.Tags.TryAdd("customer", groupNamePieces[1]); // msft
resourceGroup.Tags.TryAdd("env", groupNamePieces[2]); // prod
resourceGroup.Tags.TryAdd("project", string.Join('-', groupNamePieces.Skip(3))); //azman-main-compute
}
await resourceManagerClient.ResourceGroups.CreateOrUpdateAsync(resourceGroup.Name, resourceGroup);
}
// todo: explore changes required for using resource ID for _any_ resource
public async Task DeleteResource(string subscriptionId, string resourceGroupName)
{
// todo: what-if? --> log deletion, but don't execute
// connect to azure
_log.LogInformation($"Request to delete {resourceGroupName} from subscription {subscriptionId}");
var resourceManagerClient = new ResourcesManagementClient(subscriptionId, new DefaultAzureCredential());
await resourceManagerClient.ResourceGroups.StartDeleteAsync(resourceGroupName);
}
public async Task<string> ExportResourceGroupTemplateByName(string subscriptionId, string groupName)
{
// todo: tweak based on output and ease of re-deploy
var resourceManagerClient = new ResourcesManagementClient(subscriptionId, new DefaultAzureCredential());
var exportedTemplate = await resourceManagerClient.ResourceGroups.StartExportTemplateAsync(groupName,
new Azure.ResourceManager.Resources.Models.ExportTemplateRequest());
if (exportedTemplate.HasValue) return (string)exportedTemplate.Value.Template; // todo: y tho?
return string.Empty;
}
}
}
No newline at end of file
ew file mode 100644
ndex 0000000..2336ca1
++ b/IResourceManagementService.cs
fbd94632e5078c53f67ec03bbee3e5b285809a9b
explore changes required for using resource ID for any resource
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Azure.Identity;
using Azure.ResourceManager.Resources;
using Microsoft.Azure.Services.AppAuthentication;
using azman_v2.Model;
namespace azman_v2
{
public class AzureResourceManagementService : IResourceManagementService
{
private readonly HttpClient _httpClient;
private readonly ILogger<AzureResourceManagementService> _log;
public AzureResourceManagementService(IHttpClientFactory httpFactory, ILoggerFactory loggerFactory)
{
_httpClient = httpFactory.CreateClient();
_log = loggerFactory.CreateLogger<AzureResourceManagementService>();
}
public TaggingRequestModel? ProcessAlert(dynamic alert)
{
if (!string.Equals(alert.data.context.activityLog.status.ToString(), "Succeeded", StringComparison.OrdinalIgnoreCase) ||
!string.Equals(alert.data.context.activityLog.subStatus.ToString(), "Created", StringComparison.OrdinalIgnoreCase))
{
_log.LogTrace($"status: {alert.data.context.activityLog.status}");
_log.LogTrace($"subStatus: {alert.data.context.activityLog.subStatus}");
//return new OkResult(); // return 200, we're done here since it hasn't succeeded yet
return null;
}
_log.LogTrace($"{alert.data.context.activityLog.resourceGroupName}");
// handles queueing up new resource groups to be tagged
return new TaggingRequestModel(
groupName: alert.data.context.activityLog.resourceGroupName,
user: alert.data.context.activityLog.caller,
subscriptionId: alert.data.context.activityLog.subscriptionId,
created: alert.data.context.activityLog.eventTimestamp
);
}
public async Task TagResource(TaggingRequestModel request)
{
// connect to azure
var resourceManagerClient = new ResourcesManagementClient(request.SubscriptionId, new DefaultAzureCredential());
var resourceGroupRequest = await resourceManagerClient.ResourceGroups.GetAsync(request.ResourceGroupName);
if (resourceGroupRequest == null) return;
var resourceGroup = resourceGroupRequest.Value;
// todo: move this as default to configuration
var newDate = request.DateCreated.AddDays(30).Date;
resourceGroup.Tags.TryAdd("expires", newDate.ToString("yyyy-MM-dd"));
resourceGroup.Tags.TryAdd("tagged-by", "thrazman");
resourceGroup.Tags.TryAdd("owner", request.CreatedByUser);
// service-msft-prod-azman-main-compute
var groupNamePieces = resourceGroup.Name.Split('-');
if (groupNamePieces.Count() >= 3)
{
resourceGroup.Tags.TryAdd("function", groupNamePieces[0]); // service
resourceGroup.Tags.TryAdd("customer", groupNamePieces[1]); // msft
resourceGroup.Tags.TryAdd("env", groupNamePieces[2]); // prod
resourceGroup.Tags.TryAdd("project", string.Join('-', groupNamePieces.Skip(3))); //azman-main-compute
}
await resourceManagerClient.ResourceGroups.CreateOrUpdateAsync(resourceGroup.Name, resourceGroup);
}
// todo: explore changes required for using resource ID for _any_ resource
public async Task DeleteResource(string subscriptionId, string resourceGroupName)
{
// todo: what-if? --> log deletion, but don't execute
// connect to azure
_log.LogInformation($"Request to delete {resourceGroupName} from subscription {subscriptionId}");
var resourceManagerClient = new ResourcesManagementClient(subscriptionId, new DefaultAzureCredential());
await resourceManagerClient.ResourceGroups.StartDeleteAsync(resourceGroupName);
}
public async Task<string> ExportResourceGroupTemplateByName(string subscriptionId, string groupName)
{
// todo: tweak based on output and ease of re-deploy
var resourceManagerClient = new ResourcesManagementClient(subscriptionId, new DefaultAzureCredential());
var exportedTemplate = await resourceManagerClient.ResourceGroups.StartExportTemplateAsync(groupName,
new Azure.ResourceManager.Resources.Models.ExportTemplateRequest());
if (exportedTemplate.HasValue) return (string)exportedTemplate.Value.Template; // todo: y tho?
return string.Empty;
}
}
}
No newline at end of file
ew file mode 100644
ndex 0000000..2336ca1
++ b/IResourceManagementService.cs
79d52bd959dc51fbf46989d665e38dedaa17b6ef
handle 202 accepted on export request, sigh
azman-v2/AzureResourceManagementService.cs
Line 101 in 9c46e4b
public async Task<string> ExportResourceGroupTemplateByName(string subscriptionId, string groupName)
{
var token = await _tokenProvider.GetAccessTokenAsync(new[] { "https://management.azure.com/" });
_httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token.Token);
var body = new StringContent("{'resources':[ '*' ]}", System.Text.Encoding.UTF8, "application/json");
var request = await _httpClient.PostAsync($"https://management.azure.com/subscriptions/{subscriptionId}/resourcegroups/{groupName}/exportTemplate?api-version=2020-06-01", body);
if (!request.IsSuccessStatusCode) return string.Empty;
// todo: handle 202 accepted on export request, sigh
var templateData = await request.Content.ReadAsStringAsync();
return templateData;
// POST https://management.azure.com/subscriptions/{subscriptionId}/resourcegroups/{resourceGroupName}/exportTemplate?api-version=2020-06-01
// todo: tweak based on output and ease of re-deploy
// var resourceManagerClient = new ResourcesManagementClient(subscriptionId, _tokenCredential);
// var exportedTemplate = await resourceManagerClient.ResourceGroups.StartExportTemplateAsync(groupName,
// todo: this is required but read-only? hmm
//new Azure.ResourceManager.Resources.Models.ExportTemplateRequest() { Resources = new[] { "*" } });
// new Azure.ResourceManager.Resources.Models.ExportTemplateRequest() { });
// if (exportedTemplate.HasValue) return (string)exportedTemplate.Value.Template; // todo: y tho?
//return string.Empty;
}
}
}
No newline at end of file
ndex 85f3af9..3c4161a 100644
++ b/Functions.cs
bdd7b1a0a68b153437df0535ab6c28a13a58bd67
move this as default to configuration
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Azure.Identity;
using Azure.ResourceManager.Resources;
using Microsoft.Azure.Services.AppAuthentication;
using azman_v2.Model;
namespace azman_v2
{
public class AzureResourceManagementService : IResourceManagementService
{
private readonly HttpClient _httpClient;
private readonly ILogger<AzureResourceManagementService> _log;
public AzureResourceManagementService(IHttpClientFactory httpFactory, ILoggerFactory loggerFactory)
{
_httpClient = httpFactory.CreateClient();
_log = loggerFactory.CreateLogger<AzureResourceManagementService>();
}
public TaggingRequestModel? ProcessAlert(dynamic alert)
{
if (!string.Equals(alert.data.context.activityLog.status.ToString(), "Succeeded", StringComparison.OrdinalIgnoreCase) ||
!string.Equals(alert.data.context.activityLog.subStatus.ToString(), "Created", StringComparison.OrdinalIgnoreCase))
{
_log.LogTrace($"status: {alert.data.context.activityLog.status}");
_log.LogTrace($"subStatus: {alert.data.context.activityLog.subStatus}");
//return new OkResult(); // return 200, we're done here since it hasn't succeeded yet
return null;
}
_log.LogTrace($"{alert.data.context.activityLog.resourceGroupName}");
// handles queueing up new resource groups to be tagged
return new TaggingRequestModel(
groupName: alert.data.context.activityLog.resourceGroupName,
user: alert.data.context.activityLog.caller,
subscriptionId: alert.data.context.activityLog.subscriptionId,
created: alert.data.context.activityLog.eventTimestamp
);
}
public async Task TagResource(TaggingRequestModel request)
{
// connect to azure
var resourceManagerClient = new ResourcesManagementClient(request.SubscriptionId, new DefaultAzureCredential());
var resourceGroupRequest = await resourceManagerClient.ResourceGroups.GetAsync(request.ResourceGroupName);
if (resourceGroupRequest == null) return;
var resourceGroup = resourceGroupRequest.Value;
// todo: move this as default to configuration
var newDate = request.DateCreated.AddDays(30).Date;
resourceGroup.Tags.TryAdd("expires", newDate.ToString("yyyy-MM-dd"));
resourceGroup.Tags.TryAdd("tagged-by", "thrazman");
resourceGroup.Tags.TryAdd("owner", request.CreatedByUser);
// service-msft-prod-azman-main-compute
var groupNamePieces = resourceGroup.Name.Split('-');
if (groupNamePieces.Count() >= 3)
{
resourceGroup.Tags.TryAdd("function", groupNamePieces[0]); // service
resourceGroup.Tags.TryAdd("customer", groupNamePieces[1]); // msft
resourceGroup.Tags.TryAdd("env", groupNamePieces[2]); // prod
resourceGroup.Tags.TryAdd("project", string.Join('-', groupNamePieces.Skip(3))); //azman-main-compute
}
await resourceManagerClient.ResourceGroups.CreateOrUpdateAsync(resourceGroup.Name, resourceGroup);
}
// todo: explore changes required for using resource ID for _any_ resource
public async Task DeleteResource(string subscriptionId, string resourceGroupName)
{
// todo: what-if? --> log deletion, but don't execute
// connect to azure
_log.LogInformation($"Request to delete {resourceGroupName} from subscription {subscriptionId}");
var resourceManagerClient = new ResourcesManagementClient(subscriptionId, new DefaultAzureCredential());
await resourceManagerClient.ResourceGroups.StartDeleteAsync(resourceGroupName);
}
public async Task<string> ExportResourceGroupTemplateByName(string subscriptionId, string groupName)
{
// todo: tweak based on output and ease of re-deploy
var resourceManagerClient = new ResourcesManagementClient(subscriptionId, new DefaultAzureCredential());
var exportedTemplate = await resourceManagerClient.ResourceGroups.StartExportTemplateAsync(groupName,
new Azure.ResourceManager.Resources.Models.ExportTemplateRequest());
if (exportedTemplate.HasValue) return (string)exportedTemplate.Value.Template; // todo: y tho?
return string.Empty;
}
}
}
No newline at end of file
ew file mode 100644
ndex 0000000..2336ca1
++ b/IResourceManagementService.cs
3d16d3cf2860618cb508f12f96bbb72538c80664
explore changes required for using resource ID for any resource
azman-v2/OnResourceGroupCreate.cs
Line 86 in e052c6a
}
// todo: explore changes required for using resource ID for _any_ resource
public async Task DeleteResource(string subscriptionId, string resourceGroupName)
{
// todo: what-if? --> log deletion, but don't execute
// connect to azure
_log.LogInformation($"Request to delete {resourceGroupName} from subscription {subscriptionId}");
var resourceManagerClient = new ResourcesManagementClient(subscriptionId, new DefaultAzureCredential());
2b7f8e7ed6ed8172856da8fef25639b808e1b40f
configuration to allow/deny specific subscriptions
Line 23 in 2768e6b
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Azure.Management.ResourceGraph;
using Microsoft.Azure.Management.ResourceGraph.Models;
namespace azman_v2
{
public class Scanner : IScanner
{
private readonly ITokenProvider _tokenProvider;
private readonly HttpClient _httpClient;
private readonly ILogger<Scanner> _log;
// todo: configuration for national clouds, e.g., https://management.chinacloudapi.cn
private readonly string _managementEndpoint = "https://management.azure.com/";
private readonly string _managementAzureAdResourceId = "https://management.azure.com/";
// todo: configuration to allow/deny specific subscriptions
private readonly List<string> _subscriptionIds;
public Scanner(ITokenProvider tokenProvider, IHttpClientFactory httpFactory, ILoggerFactory loggerFactory)
{
_tokenProvider = tokenProvider;
_httpClient = httpFactory.CreateClient();
_log = loggerFactory.CreateLogger<Scanner>();
_subscriptionIds = new List<string>();
}
private async Task<IEnumerable<string>> FindAccessibleSubscriptions(bool forceRefresh = false)
{
// todo: hack for testing until configurable subscription list is available
_subscriptionIds.Add("e7048bdb-835c-440f-9304-aa4171382839");
if (!forceRefresh || _subscriptionIds.Any())
{
return _subscriptionIds;
}
_log.LogTrace($"Getting subscription list starting at ${DateTime.UtcNow}");
_log.LogTrace($"Getting access token for ${_managementAzureAdResourceId}");
var token = await _tokenProvider.GetAccessTokenAsync(_managementAzureAdResourceId, false);
_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
// resource graph is expecting an array of subscriptions, so get the subscription list first
var subRequest = await _httpClient.GetAsync($"{_managementEndpoint}subscriptions?api-version=2020-01-01");
if (!subRequest.IsSuccessStatusCode)
{
_log.LogError(new EventId((int)subRequest.StatusCode, subRequest.StatusCode.ToString()), await subRequest.Content.ReadAsStringAsync());
return _subscriptionIds;
}
var data = await JsonDocument.ParseAsync(await subRequest.Content.ReadAsStreamAsync());
var subscriptionArray = data.RootElement.GetProperty("value").EnumerateArray();
var subscriptions = subscriptionArray.Select(x => x.GetProperty("subscriptionId").ToString());
_log.LogTrace($"Got subscription IDs: {string.Join(',', subscriptions)}");
_subscriptionIds.AddRange(subscriptions);
return _subscriptionIds;
}
private async Task<IEnumerable<ResourceSearchResult>> QueryResourceGraph(string queryText)
{
var subscriptions = await FindAccessibleSubscriptions();
var graphClient = new ResourceGraphClient(new Microsoft.Rest.TokenCredentials(await _tokenProvider.GetAccessTokenAsync(_managementAzureAdResourceId)));
var query = await graphClient.ResourcesAsync(new QueryRequest(subscriptions.ToList(), queryText));
var resources = new List<ResourceSearchResult>();
// the ResourceGraphClient uses Newtonsoft under the hood
if (((dynamic)query.Data).rows is Newtonsoft.Json.Linq.JArray j)
{
resources.AddRange(
j.Select(x => new ResourceSearchResult()
{
// I'm sure there is a better way here - looking at the columns property, for example,
// to find the position of the column in the row we're interested in - follows query order
// so for now, 0 & 1
ResourceId = x.ElementAt(0).ToString(),
SubscriptionId = x.ElementAt(1).ToString()
}));
}
return resources;
}
public async Task<IEnumerable<ResourceSearchResult>> ScanForUntaggedResources()
{
var untaggedQuery = @"resourcecontainers | where (isnull(tags.['expires'])) and
type == 'microsoft.resources/subscriptions/resourcegroups'
| project name, subscriptionId, id";
return await QueryResourceGraph(untaggedQuery);
}
public async Task<IEnumerable<ResourceSearchResult>> ScanForExpiredResources()
{
var expiredQuery = @"resourcecontainers | where (!isnull(tags.['expires']))
and type == 'microsoft.resources/subscriptions/resourcegroups'
and todatetime(tags['expires']) < now()
| project name, subscriptionId, id";
return await QueryResourceGraph(expiredQuery);
}
public async Task<IEnumerable<ResourceSearchResult>> ScanForExpiredResources(DateTimeOffset expirationDate)
{
// and todatetime(tags.['expires']) < now() + 14d
var expiredQuery = $@"resourcecontainers | where (!isnull(tags.['expires']))
and type == 'microsoft.resources/subscriptions/resourcegroups'
and todatetime(tags['expires']) < todatetime({expirationDate})
| project name, subscriptionId, id";
return await QueryResourceGraph(expiredQuery);
}
public async Task<IEnumerable<ResourceSearchResult>> ScanForExpiredResources(string kustoDateExpression)
{
// e.g., and todatetime(tags.['expires']) < now() + 3d
var expiredQuery = $@"resourcecontainers | where (!isnull(tags.['expires']))
and type == 'microsoft.resources/subscriptions/resourcegroups'
and todatetime(tags['expires']) < {kustoDateExpression}
| project name, subscriptionId, id";
return await QueryResourceGraph(expiredQuery);
}
}
}
No newline at end of file
e4c965ed15d1dd00a5256803313bc416ea8b5042
if location == null or empty, POST to export Uri
request = await _httpClient.GetAsync(request.Headers.Location ?? exportUri);
}
azman-v2/AzureResourceManagementService.cs
Line 133 in 4e76cb1
public async Task<string> ExportResourceGroupTemplateByName(string subscriptionId, string groupName)
{
// var token = await _tokenProvider.GetAccessTokenAsync(new[] { "https://management.azure.com/" });
// _httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token.Token);
// var exportUri = new Uri($"https://management.azure.com/subscriptions/{subscriptionId}/resourcegroups/{groupName}/exportTemplate?api-version=2020-06-01");
// var body = new StringContent("{'resources':[ '*' ]}", System.Text.Encoding.UTF8, "application/json");
// var request = await _httpClient.PostAsync(exportUri, body);
// if (!request.IsSuccessStatusCode) return string.Empty;
// while (request.StatusCode == System.Net.HttpStatusCode.Accepted)
// {
// var delayInSec = 15;
// if (request.Headers.RetryAfter.Delta != null)
// {
// delayInSec = Convert.ToInt32(request.Headers.RetryAfter.Delta.Value.TotalSeconds);
// }
// await Task.Delay(delayInSec * 1000);
// // todo: if location == null or empty, POST to export Uri
// request = await _httpClient.GetAsync(request.Headers.Location ?? exportUri);
// }
// if (!request.IsSuccessStatusCode) return string.Empty;
// var templateData = await request.Content.ReadAsStringAsync();
// return templateData;
// POST https://management.azure.com/subscriptions/{subscriptionId}/resourcegroups/{resourceGroupName}/exportTemplate?api-version=2020-06-01
// todo: tweak based on output and ease of re-deploy
var resourceManagerClient = new ResourcesManagementClient(subscriptionId, _tokenCredential);
var resourceTypesToExport = new Azure.ResourceManager.Resources.Models.ExportTemplateRequest();
resourceTypesToExport.Resources.Add("*");
var exportedTemplate = await resourceManagerClient.ResourceGroups.StartExportTemplateAsync(groupName, resourceTypesToExport);
if (exportedTemplate.HasValue) return (string)exportedTemplate.Value.Template;
return string.Empty;
}
}
}
No newline at end of file
ndex 9054115..1e73363 100644
++ b/Functions.cs
ed0f675aab0b47df224508e2297221e56e79e368
configuration for national clouds, e.g., https://management.chinacloudapi.cn
azman-v2/OnResourceGroupCreate.cs
Line 121 in e052c6a
private readonly ILogger<Scanner> _log;
// todo: configuration for national clouds, e.g., https://management.chinacloudapi.cn
private readonly string _managementEndpoint = "https://management.azure.com/";
private readonly string _managementAzureAdResourceId = "https://management.azure.com/";
// todo: configuration to allow/deny specific subscriptions
private readonly List<string> _subscriptionIds;
public Scanner(ITokenProvider tokenProvider, IHttpClientFactory httpFactory, ILoggerFactory loggerFactory)
6555143074d3881821541aa42d6ddaa6e6974db0
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.