Skip to content

Commit

Permalink
Merge pull request #80 from mivano/cvs79-enrollments
Browse files Browse the repository at this point in the history
Billing account support
  • Loading branch information
mivano authored Nov 3, 2023
2 parents 85ab9c7 + c779fe6 commit 7a4a8e0
Show file tree
Hide file tree
Showing 31 changed files with 913 additions and 176 deletions.
411 changes: 411 additions & 0 deletions .gitignore

Large diffs are not rendered by default.

8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,10 @@ EXAMPLES:
OPTIONS:
-h, --help Prints help information
--debug Increase logging verbosity to show all debug logs
-s, --subscription The subscription id to use. Will try to fetch the active id if not specified.
-s, --subscription The subscription id to use. Will try to fetch the active id if not specified
-g, --resource-group The resource group to scope the request to. Need to be used in combination with the subscription id
-b, --billing-account The billing account id to use
-e, --enrollment-account The enrollment account id to use
-o, --output The output format to use. Defaults to Console (Console, Json, JsonC, Markdown, Text, Csv)
-t, --timeframe The timeframe to use for the costs. Defaults to BillingMonthToDate. When set to Custom, specify the from and to dates using the --from and --to options
--from The start date to use for the costs. Defaults to the first day of the previous month
Expand All @@ -88,6 +91,9 @@ COMMANDS:

```
Starting from version `0.35`, you can select a different scope besides only subscription. Specify subscription id and resourcegroup name, billing account and/or enrollment account to scope the request to that level.
> When you do not specify a subscription id, it will fetch the actively selected one of the `az cli` instead.
> If the application is not working properly, you can use the `--debug` parameter to increase the logging verbosity and see more details.
Expand Down
54 changes: 34 additions & 20 deletions src/Commands/AccumulatedCost/AccumulatedCostCommand.cs
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@
using System.Diagnostics;
using System.Text.Json;
using AzureCostCli.Commands.ShowCommand.OutputFormatters;
using AzureCostCli.CostApi;
using AzureCostCli.Infrastructure;
using AzureCostCli.OutputFormatters;
using Spectre.Console;
using Spectre.Console.Cli;

namespace AzureCostCli.Commands.ShowCommand;
namespace AzureCostCli.Commands.AccumulatedCost;

public class AccumulatedCostCommand : AsyncCommand<AccumulatedCostSettings>
{
private readonly ICostRetriever _costRetriever;

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

public AccumulatedCostCommand(ICostRetriever costRetriever)
Expand Down Expand Up @@ -56,12 +54,11 @@ public override async Task<int> ExecuteAsync(CommandContext context, Accumulated
// Show version
if (settings.Debug)
AnsiConsole.WriteLine($"Version: {typeof(AccumulatedCostCommand).Assembly.GetName().Version}");



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

if (subscriptionId == Guid.Empty)
if (subscriptionId.HasValue == false && (settings.GetScope.IsSubscriptionBased))
{
// Get the subscription ID from the Azure CLI
try
Expand All @@ -85,18 +82,27 @@ public override async Task<int> ExecuteAsync(CommandContext context, Accumulated
}

AccumulatedCostDetails accumulatedCost = null;


Subscription subscription = null;
await AnsiConsole.Status()
.StartAsync("Fetching cost data...", async ctx =>
{

ctx.Status = "Fetching subscription details...";
// Fetch the subscription details
var subscription = await _costRetriever.RetrieveSubscription(settings.Debug, subscriptionId);
if (settings.GetScope.IsSubscriptionBased)
{
ctx.Status = "Fetching subscription details...";
// Fetch the subscription details
subscription = await _costRetriever.RetrieveSubscription(settings.Debug, subscriptionId.Value);
}
else
{
ctx.Status = "Fetching Enrollment details...";
// Fetch the enrollment details //TODO
subscription = new Subscription(string.Empty, string.Empty, Array.Empty<object>(), "Enrollment", "Enrollment", $"Enrollment {settings.EnrollmentAccountId}", "Active", new SubscriptionPolicies(string.Empty, string.Empty, string.Empty));
}

ctx.Status = "Fetching cost data...";
// Fetch the costs from the Azure Cost Management API
var costs = await _costRetriever.RetrieveCosts(settings.Debug, subscriptionId,
var costs = await _costRetriever.RetrieveCosts(settings.Debug, settings.GetScope,
settings.Filter,
settings.Metric,
settings.Timeframe,
Expand Down Expand Up @@ -137,33 +143,41 @@ await AnsiConsole.Status()
DateTime.DaysInMonth(settings.To.Year, settings.To.Month));

ctx.Status = "Fetching forecasted cost data...";
forecastedCosts = (await _costRetriever.RetrieveForecastedCosts(settings.Debug, subscriptionId,
forecastedCosts = (await _costRetriever.RetrieveForecastedCosts(settings.Debug, settings.GetScope,
settings.Filter,
settings.Metric,
TimeframeType.Custom,
forecastStartDate,
forecastEndDate)).ToList();
}

IEnumerable<CostNamedItem> bySubscriptionCosts = null;
if (settings.GetScope.IsSubscriptionBased==false)
{
ctx.Status = "Fetching cost data by subscription...";
bySubscriptionCosts = await _costRetriever.RetrieveCostBySubscription(settings.Debug,
settings.GetScope, settings.Filter, settings.Metric, settings.Timeframe, settings.From, settings.To);
}

ctx.Status = "Fetching cost data by service name...";
var byServiceNameCosts = await _costRetriever.RetrieveCostByServiceName(settings.Debug,
subscriptionId, settings.Filter, settings.Metric, settings.Timeframe, settings.From, settings.To);
settings.GetScope, settings.Filter, settings.Metric, settings.Timeframe, settings.From, settings.To);

ctx.Status = "Fetching cost data by location...";
var byLocationCosts = await _costRetriever.RetrieveCostByLocation(settings.Debug, subscriptionId,
var byLocationCosts = await _costRetriever.RetrieveCostByLocation(settings.Debug, settings.GetScope,
settings.Filter,
settings.Metric,
settings.Timeframe, settings.From, settings.To);

ctx.Status= "Fetching cost data by resource group...";
var byResourceGroupCosts = await _costRetriever.RetrieveCostByResourceGroup(settings.Debug,
subscriptionId,
settings.GetScope,
settings.Filter,
settings.Metric,
settings.Timeframe, settings.From, settings.To);

accumulatedCost = new AccumulatedCostDetails(subscription, costs, forecastedCosts, byServiceNameCosts,
byLocationCosts, byResourceGroupCosts);
accumulatedCost = new AccumulatedCostDetails(subscription, null, costs, forecastedCosts, byServiceNameCosts,
byLocationCosts, byResourceGroupCosts, bySubscriptionCosts);

});

Expand Down
7 changes: 1 addition & 6 deletions src/Commands/AccumulatedCost/AccumulatedCostSettings.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,5 @@
using System.ComponentModel;
using Spectre.Console.Cli;

namespace AzureCostCli.Commands.ShowCommand;
namespace AzureCostCli.Commands.AccumulatedCost;

public class AccumulatedCostSettings : CostSettings
{


}
12 changes: 5 additions & 7 deletions src/Commands/Budgets/BudgetsCommand.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
using System.Diagnostics;
using System.Text.Json;
using AzureCostCli.Commands.ShowCommand;
using AzureCostCli.Commands.ShowCommand.OutputFormatters;
using AzureCostCli.Commands.CostByResource;
using AzureCostCli.CostApi;
using AzureCostCli.Infrastructure;
using AzureCostCli.OutputFormatters;
using Spectre.Console;
using Spectre.Console.Cli;

namespace AzureCostCli.Commands.CostByResource;
namespace AzureCostCli.Commands.Budgets;

public class BudgetsCommand: AsyncCommand<BudgetsSettings>
{
Expand Down Expand Up @@ -38,7 +36,7 @@ public override async Task<int> ExecuteAsync(CommandContext context, BudgetsSett
// Get the subscription ID from the settings
var subscriptionId = settings.Subscription;

if (subscriptionId == Guid.Empty)
if (subscriptionId.GetValueOrDefault() == Guid.Empty)
{
// Get the subscription ID from the Azure CLI
try
Expand All @@ -62,7 +60,7 @@ public override async Task<int> ExecuteAsync(CommandContext context, BudgetsSett
}

// Fetch the details from the Azure Cost Management API
var budgets = await _costRetriever.RetrieveBudgets(settings.Debug, subscriptionId);
var budgets = await _costRetriever.RetrieveBudgets(settings.Debug, settings.GetScope);

// Write the output
await _outputFormatters[settings.Output]
Expand Down
5 changes: 1 addition & 4 deletions src/Commands/Budgets/BudgetsSettings.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
using System.ComponentModel;
using Spectre.Console.Cli;

namespace AzureCostCli.Commands.ShowCommand;
namespace AzureCostCli.Commands.Budgets;

public class BudgetsSettings : CostSettings
{
Expand Down
7 changes: 3 additions & 4 deletions src/Commands/CostByResource/CostByResourceCommand.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
using System.Diagnostics;
using System.Text.Json;
using AzureCostCli.Commands.ShowCommand;
using AzureCostCli.Commands.ShowCommand.OutputFormatters;
using AzureCostCli.CostApi;
using AzureCostCli.Infrastructure;
using AzureCostCli.OutputFormatters;
using Spectre.Console;
using Spectre.Console.Cli;

Expand Down Expand Up @@ -62,7 +61,7 @@ public override async Task<int> ExecuteAsync(CommandContext context, CostByResou
// Get the subscription ID from the settings
var subscriptionId = settings.Subscription;

if (subscriptionId == Guid.Empty)
if (subscriptionId.GetValueOrDefault() == Guid.Empty)
{
// Get the subscription ID from the Azure CLI
try
Expand Down Expand Up @@ -94,7 +93,7 @@ await AnsiConsole.Status()
{
resources = await _costRetriever.RetrieveCostForResources(
settings.Debug,
subscriptionId, settings.Filter,
settings.GetScope, settings.Filter,
settings.Metric,
settings.ExcludeMeterDetails,
settings.Timeframe,
Expand Down
2 changes: 1 addition & 1 deletion src/Commands/CostByResource/CostByResourceSettings.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
using System.ComponentModel;
using Spectre.Console.Cli;

namespace AzureCostCli.Commands.ShowCommand;
namespace AzureCostCli.Commands.CostByResource;

public class CostByResourceSettings : CostSettings
{
Expand Down
15 changes: 6 additions & 9 deletions src/Commands/CostByTag/CostByTagCommand.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
using System.Diagnostics;
using System.Text.Json;
using AzureCostCli.Commands.ShowCommand;
using AzureCostCli.Commands.ShowCommand.OutputFormatters;
using AzureCostCli.Commands.CostByResource;
using AzureCostCli.CostApi;
using AzureCostCli.Infrastructure;
using AzureCostCli.OutputFormatters;
using Spectre.Console;
using Spectre.Console.Cli;

namespace AzureCostCli.Commands.CostByResource;
namespace AzureCostCli.Commands.CostByTag;

public class CostByTagCommand : AsyncCommand<CostByTagSettings>
{
Expand Down Expand Up @@ -62,7 +60,7 @@ public override async Task<int> ExecuteAsync(CommandContext context, CostByTagSe
// Get the subscription ID from the settings
var subscriptionId = settings.Subscription;

if (subscriptionId == Guid.Empty)
if (subscriptionId.GetValueOrDefault() == Guid.Empty)
{
// Get the subscription ID from the Azure CLI
try
Expand Down Expand Up @@ -94,7 +92,7 @@ await AnsiConsole.Status()
{
resources = await _costRetriever.RetrieveCostForResources(
settings.Debug,
subscriptionId, settings.Filter,
settings.GetScope, settings.Filter,
settings.Metric,
true,
settings.Timeframe,
Expand Down Expand Up @@ -128,9 +126,8 @@ private Dictionary<string, Dictionary<string, List<CostResourceItem>>> GetResour
{
var resourceTags = new Dictionary<string, string>(resource.Tags, StringComparer.OrdinalIgnoreCase);

if (resourceTags.ContainsKey(tag))
if (resourceTags.TryGetValue(tag, out var tagValue))
{
var tagValue = resourceTags[tag];
if (!resourcesByTag[tag].ContainsKey(tagValue))
{
resourcesByTag[tag][tagValue] = new List<CostResourceItem>();
Expand Down
2 changes: 1 addition & 1 deletion src/Commands/CostByTag/CostByTagSettings.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
using System.ComponentModel;
using Spectre.Console.Cli;

namespace AzureCostCli.Commands.ShowCommand;
namespace AzureCostCli.Commands.CostByTag;

public class CostByTagSettings : CostSettings
{
Expand Down
79 changes: 77 additions & 2 deletions src/Commands/CostSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,19 @@ public class CostSettings : LogCommandSettings, ICostSettings
{
[CommandOption("-s|--subscription")]
[Description("The subscription id to use. Will try to fetch the active id if not specified.")]
public Guid Subscription { get; set; }
public Guid? Subscription { get; set; }

[CommandOption("-g|--resource-group")]
[Description("The resource group to scope the request to. Need to be used in combination with the subscription id.")]
public string? ResourceGroup { get; set; }

[CommandOption("-b|--billing-account")]
[Description("The billing account id to use.")]
public int? BillingAccountId { get; set; }

[CommandOption("-e|--enrollment-account")]
[Description("The enrollment account id to use.")]
public int? EnrollmentAccountId { get; set; }

[CommandOption("-o|--output")]
[Description("The output format to use. Defaults to Console (Console, Json, JsonC, Text, Markdown, Csv)")]
Expand Down Expand Up @@ -53,14 +65,77 @@ public class CostSettings : LogCommandSettings, ICostSettings

[CommandOption("--filter")]
[Description("Filter the output by the specified properties. Defaults to no filtering and can be multiple values.")]
public string[] Filter { get; set; }
public string[] Filter { get; set; } = Array.Empty<string>();

[CommandOption("-m|--metric")]
[Description("The metric to use for the costs. Defaults to ActualCost. (ActualCost, AmortizedCost)")]
[DefaultValue(MetricType.ActualCost)]
public MetricType Metric { get; set; } = MetricType.ActualCost;


public Scope GetScope
{
get {
if ((Subscription==null || Subscription == Guid.Empty) && EnrollmentAccountId != null && BillingAccountId != null)
{
return Scope.EnrollmentAccount(BillingAccountId.Value, EnrollmentAccountId.Value);
}
else if (Subscription != null && !string.IsNullOrWhiteSpace(ResourceGroup))
{
return Scope.ResourceGroup(Subscription.Value, ResourceGroup);
}
else if (BillingAccountId.HasValue)
{
return Scope.BillingAccount(BillingAccountId.Value);
}
else // default to subscription
{
return Scope.Subscription(Subscription.GetValueOrDefault(Guid.Empty));
}
}
}

}

/// <summary>
/// The scope associated with query and export operations.
/// This includes '/subscriptions/{subscriptionId}/' for subscription scope,
/// '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}' for resourceGroup scope,
/// '/providers/Microsoft.Billing/billingAccounts/{billingAccountId}' for Billing Account scope and
/// '/providers/Microsoft.Billing/billingAccounts/{billingAccountId}/departments/{departmentId}' for Department scope,
/// '/providers/Microsoft.Billing/billingAccounts/{billingAccountId}/enrollmentAccounts/{enrollmentAccountId}' for EnrollmentAccount scope,
/// '/providers/Microsoft.Management/managementGroups/{managementGroupId} for Management Group scope,
/// '/providers/Microsoft.Billing/billingAccounts/{billingAccountId}/billingProfiles/{billingProfileId}' for billingProfile scope,
/// '/providers/Microsoft.Billing/billingAccounts/{billingAccountId}/billingProfiles/{billingProfileId}/invoiceSections/{invoiceSectionId}' for invoiceSection scope, and
/// '/providers/Microsoft.Billing/billingAccounts/{billingAccountId}/customers/{customerId}' specific for partners.
///
/// Note; not all are implemented
/// </summary>
public class Scope
{
public static Scope Subscription(Guid subscriptionId) => new("Subscription", "/subscriptions/" + subscriptionId, true);
public static Scope ResourceGroup(Guid subscriptionId, string resourceGroup) => new("ResourceGroup", $"/subscriptions/{subscriptionId}/resourceGroups/{resourceGroup}", true);
public static Scope EnrollmentAccount(int billingAccountId, int enrollmentAccountId) => new("EnrollmentAccount", $"/providers/Microsoft.Billing/billingAccounts/{billingAccountId}/enrollmentAccounts/{enrollmentAccountId}", false);
public static Scope BillingAccount(int billingAccountId) => new("BillingAccount", $"/providers/Microsoft.Billing/billingAccounts/{billingAccountId}", false);

private Scope(string name, string path, bool isSubscriptionBased)
{
Name = name;
ScopePath = path;
IsSubscriptionBased = isSubscriptionBased;
}

public string Name { get; init; }

public string ScopePath {
get;
init;
}

public bool IsSubscriptionBased { get; set; }
}


public enum MetricType
{
ActualCost,
Expand Down
Loading

0 comments on commit 7a4a8e0

Please sign in to comment.