Skip to content

Commit

Permalink
Initial structure of costByTag command
Browse files Browse the repository at this point in the history
  • Loading branch information
mivano committed Aug 4, 2023
1 parent 8852cc7 commit 19eead8
Show file tree
Hide file tree
Showing 9 changed files with 215 additions and 1 deletion.
146 changes: 146 additions & 0 deletions src/Commands/CostByTag/CostByTagCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
using System.Diagnostics;
using System.Text.Json;
using AzureCostCli.Commands.ShowCommand;
using AzureCostCli.Commands.ShowCommand.OutputFormatters;
using AzureCostCli.CostApi;
using AzureCostCli.Infrastructure;
using Spectre.Console;
using Spectre.Console.Cli;

namespace AzureCostCli.Commands.CostByResource;

public class CostByTagCommand : AsyncCommand<CostByTagSettings>
{
private readonly ICostRetriever _costRetriever;

private readonly Dictionary<OutputFormat, BaseOutputFormatter> _outputFormatters = new();

public CostByTagCommand(ICostRetriever costRetriever)
{
_costRetriever = costRetriever;

// Add the output formatters
_outputFormatters.Add(OutputFormat.Console, new ConsoleOutputFormatter());
_outputFormatters.Add(OutputFormat.Json, new JsonOutputFormatter());
_outputFormatters.Add(OutputFormat.Jsonc, new JsonOutputFormatter());
_outputFormatters.Add(OutputFormat.Text, new TextOutputFormatter());
_outputFormatters.Add(OutputFormat.Markdown, new MarkdownOutputFormatter());
_outputFormatters.Add(OutputFormat.Csv, new CsvOutputFormatter());
}

public override ValidationResult Validate(CommandContext context, CostByTagSettings settings)
{
// Validate if the timeframe is set to Custom, then the from and to dates must be specified and the from date must be before the to date
if (settings.Timeframe == TimeframeType.Custom)
{
if (settings.From == null)
{
return ValidationResult.Error("The from date must be specified when the timeframe is set to Custom.");
}

if (settings.To == null)
{
return ValidationResult.Error("The to date must be specified when the timeframe is set to Custom.");
}

if (settings.From > settings.To)
{
return ValidationResult.Error("The from date must be before the to date.");
}
}

return ValidationResult.Success();
}

public override async Task<int> ExecuteAsync(CommandContext context, CostByTagSettings settings)
{
// Show version
if (settings.Debug)
AnsiConsole.WriteLine($"Version: {typeof(CostByResourceCommand).Assembly.GetName().Version}");


// Get the subscription ID from the settings
var subscriptionId = settings.Subscription;

if (subscriptionId == Guid.Empty)
{
// Get the subscription ID from the Azure CLI
try
{
if (settings.Debug)
AnsiConsole.WriteLine(
"No subscription ID specified. Trying to retrieve the default subscription ID from Azure CLI.");

subscriptionId = Guid.Parse(AzCommand.GetDefaultAzureSubscriptionId());

if (settings.Debug)
AnsiConsole.WriteLine($"Default subscription ID retrieved from az cli: {subscriptionId}");

settings.Subscription = subscriptionId;
}
catch (Exception e)
{
AnsiConsole.WriteException(new ArgumentException(
"Missing subscription ID. Please specify a subscription ID or login to Azure CLI.", e));
return -1;
}
}

// Fetch the costs from the Azure Cost Management API
IEnumerable<CostResourceItem> resources = new List<CostResourceItem>();

await AnsiConsole.Status()
.StartAsync("Fetching cost data for resources...", async ctx =>
{
resources = await _costRetriever.RetrieveCostForResources(
settings.Debug,
subscriptionId, settings.Filter,
settings.Metric,
true,
settings.Timeframe,
settings.From,
settings.To);
});

var byTags = GetResourcesByTag(resources, settings.Tags.ToArray());

// Write the output
await _outputFormatters[settings.Output]
.WriteCostByTag(settings, byTags);

return 0;
}

private Dictionary<string, Dictionary<string, List<CostResourceItem>>> GetResourcesByTag(
IEnumerable<CostResourceItem> resources, params string[] tags)
{
var resourcesByTag =
new Dictionary<string, Dictionary<string, List<CostResourceItem>>>(StringComparer.OrdinalIgnoreCase);

foreach (var tag in tags)
{
resourcesByTag[tag] = new Dictionary<string, List<CostResourceItem>>(StringComparer.OrdinalIgnoreCase);
}

foreach (var resource in resources)
{
foreach (var tag in tags)
{
var resourceTags = new Dictionary<string, string>(resource.Tags, StringComparer.OrdinalIgnoreCase);

if (resourceTags.ContainsKey(tag))
{
var tagValue = resourceTags[tag];
if (!resourcesByTag[tag].ContainsKey(tagValue))
{
resourcesByTag[tag][tagValue] = new List<CostResourceItem>();
}

resourcesByTag[tag][tagValue].Add(resource);
}
}
}

return resourcesByTag;
}
}
12 changes: 12 additions & 0 deletions src/Commands/CostByTag/CostByTagSettings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using System.ComponentModel;
using Spectre.Console.Cli;

namespace AzureCostCli.Commands.ShowCommand;

public class CostByTagSettings : CostSettings
{

[CommandOption("--tag")]
[Description("The tags to return, for example: Cost Center or Owner. You can specify multiple tags by using the --tag option multiple times.")]
public string[] Tags { get; set; } = Array.Empty<string>();
}
1 change: 1 addition & 0 deletions src/OutputFormatters/BaseOutputFormatter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ public abstract class BaseOutputFormatter
public abstract Task WriteDailyCost(DailyCostSettings settings, IEnumerable<CostDailyItem> dailyCosts);
public abstract Task WriteAnomalyDetectionResults(DetectAnomalySettings settings, List<AnomalyDetectionResult> anomalies);
public abstract Task WriteRegions(RegionsSettings settings, IReadOnlyCollection<AzureRegion> regions);
public abstract Task WriteCostByTag(CostByTagSettings settings, Dictionary<string, Dictionary<string, List<CostResourceItem>>> byTags);
}

public record AccumulatedCostDetails(
Expand Down
5 changes: 5 additions & 0 deletions src/OutputFormatters/ConsoleOutputFormatter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -521,5 +521,10 @@ public override Task WriteRegions(RegionsSettings settings, IReadOnlyCollection<

return Task.CompletedTask;
}

public override Task WriteCostByTag(CostByTagSettings settings, Dictionary<string, Dictionary<string, List<CostResourceItem>>> byTags)
{
throw new NotImplementedException();
}
}

30 changes: 30 additions & 0 deletions src/OutputFormatters/CsvOutputFormatter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,36 @@ public override Task WriteRegions(RegionsSettings settings, IReadOnlyCollection<
return ExportToCsv(settings.SkipHeader, regions);
}

public override Task WriteCostByTag(CostByTagSettings settings, Dictionary<string, Dictionary<string, List<CostResourceItem>>> byTags)
{
// Flatten the hierarchy to a single list, including the tag and value
var resourcesWithTagAndValue = new List<dynamic>();
foreach (var (tag, value) in byTags)
{
foreach (var (tagValue, resources) in value)
{
foreach (var resource in resources)
{
dynamic expando = new ExpandoObject();
expando.Tag = tag;
expando.Value = tagValue;
expando.ResourceId = resource.ResourceId;
expando.ResourceType = resource.ResourceType;
expando.ResourceGroup = resource.ResourceGroupName;
expando.ResourceLocation = resource.ResourceLocation;
expando.Cost = resource.Cost;
expando.Currency = resource.Currency;
expando.CostUsd = resource.CostUSD;

resourcesWithTagAndValue.Add(expando);
}
}
}


return ExportToCsv(settings.SkipHeader, resourcesWithTagAndValue);
}

private static Task ExportToCsv(bool skipHeader, IEnumerable<object> resources)
{
var config = new CsvConfiguration(CultureInfo.CurrentCulture)
Expand Down
9 changes: 8 additions & 1 deletion src/OutputFormatters/JsonOutputFormatter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,14 @@ public override Task WriteRegions(RegionsSettings settings, IReadOnlyCollection<

return Task.CompletedTask;
}


public override Task WriteCostByTag(CostByTagSettings settings, Dictionary<string, Dictionary<string, List<CostResourceItem>>> byTags)
{
WriteJson(settings, byTags);

return Task.CompletedTask;
}


private static void WriteJson(CostSettings settings, object items)
{
Expand Down
5 changes: 5 additions & 0 deletions src/OutputFormatters/MarkdownOutputFormatter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -328,4 +328,9 @@ public override Task WriteRegions(RegionsSettings settings, IReadOnlyCollection<
{
throw new NotImplementedException();
}

public override Task WriteCostByTag(CostByTagSettings settings, Dictionary<string, Dictionary<string, List<CostResourceItem>>> byTags)
{
throw new NotImplementedException();
}
}
5 changes: 5 additions & 0 deletions src/OutputFormatters/TextOutputFormatter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -210,4 +210,9 @@ public override Task WriteRegions(RegionsSettings settings, IReadOnlyCollection<
{
throw new NotImplementedException();
}

public override Task WriteCostByTag(CostByTagSettings settings, Dictionary<string, Dictionary<string, List<CostResourceItem>>> byTags)
{
throw new NotImplementedException();
}
}
3 changes: 3 additions & 0 deletions src/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@
config.AddCommand<CostByResourceCommand>("costByResource")
.WithDescription("Show the cost details by resource.");
config.AddCommand<CostByTagCommand>("costByTag")
.WithDescription("Show the cost details by tag.");
config.AddCommand<DetectAnomalyCommand>("detectAnomalies")
.WithDescription("Detect anomalies and trends.");
Expand Down

0 comments on commit 19eead8

Please sign in to comment.