Skip to content

Commit

Permalink
feat(estimation): ✨ Added support for Static Web Apps
Browse files Browse the repository at this point in the history
  • Loading branch information
kamil-mrzyglod committed Feb 14, 2024
1 parent 307f444 commit 99d778c
Show file tree
Hide file tree
Showing 11 changed files with 252 additions and 18 deletions.
69 changes: 69 additions & 0 deletions ace-tests/Reworked/StaticWebApp/StaticWebAppTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
using System.Text.Json;
using ACE;

namespace ACE_Tests.Reworked.StaticWebApp
{
[Parallelizable(ParallelScope.Self)]
public class StaticWebAppTests
{
[Test]
public void StaticWebApp_WhenFreeTierIsUsed_NoCostShouldBeReported()
{
var outputFilename = $"ace_test_{DateTime.Now.Ticks}";
var exitCode = Program.Main([
"templates/reworked/static-web-app/static-web-app-free.bicep",
"cf70b558-b930-45e4-9048-ebcefb926adf",
"arm-estimator-tests-rg",
"--generateJsonOutput",
"--jsonOutputFilename",
outputFilename,
"--inline",
"parLocation=westeurope",
"--debug"
]);

Assert.That(exitCode, Is.EqualTo(0));

var outputFile = File.ReadAllText($"{outputFilename}.json");
var output = JsonSerializer.Deserialize<EstimationOutput>(outputFile, Shared.JsonSerializerOptions);

Assert.That(output, Is.Not.Null);
Assert.Multiple(() =>
{
Assert.That(output.TotalCost.OriginalValue, Is.EqualTo(0));
Assert.That(output.TotalResourceCount, Is.EqualTo(1));
});
}

[Test]
public void StaticWebApp_WhenStandardTierIsUsed_CorrectCostShouldBeReported()
{
var outputFilename = $"ace_test_{DateTime.Now.Ticks}";
var exitCode = Program.Main([
"templates/reworked/static-web-app/static-web-app-standard.bicep",
"cf70b558-b930-45e4-9048-ebcefb926adf",
"arm-estimator-tests-rg",
"--generateJsonOutput",
"--jsonOutputFilename",
outputFilename,
"--inline",
"parLocation=westeurope",
"--mocked-retail-api-response-path",
"mocked-responses/retail-api/static-web-app/standard.json",
"--debug"
]);

Assert.That(exitCode, Is.EqualTo(0));

var outputFile = File.ReadAllText($"{outputFilename}.json");
var output = JsonSerializer.Deserialize<EstimationOutput>(outputFile, Shared.JsonSerializerOptions);

Assert.That(output, Is.Not.Null);
Assert.Multiple(() =>
{
Assert.That(output.TotalCost.OriginalValue, Is.EqualTo(9d));
Assert.That(output.TotalResourceCount, Is.EqualTo(1));
});
}
}
}
3 changes: 3 additions & 0 deletions ace-tests/azure-cost-estimator-tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,14 @@

<ItemGroup>
<None Remove="mocked-responses/retail-api/key-vault/usage-patterns.json" />
<None Remove="mocked-responses/retail-api/static-web-app/standard.json" />
<None Remove="mocked-responses\retail-api\asr\inferred-1.json" />
<None Remove="mocked-responses\retail-api\asr\inferred-2.json" />
<None Remove="mocked-responses\retail-api\asr\inferred-3.json" />
<None Remove="mocked-responses\retail-api\automation-account\usage-patterns.json" />
<None Remove="templates/reworked/key-vault/usage-patterns-1.bicep" />
<None Remove="templates/reworked/static-web-app/static-web-app-free.bicep" />
<None Remove="templates/reworked/static-web-app/static-web-app-standard.bicep" />
<None Remove="templates/reworked/virtual-network/peering-1-2.bicep" />
<None Remove="templates/reworked/virtual-network/peering-1-3.bicep" />
<None Remove="templates/reworked/virtual-network/peering-2-3.bicep" />
Expand Down
32 changes: 32 additions & 0 deletions ace-tests/mocked-responses/retail-api/static-web-app/standard.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"Url": "https://prices.azure.com/api/retail/prices?currencyCode='USD'&$filter=priceType eq 'Consumption' and serviceId eq 'DZH317GT8G5N' and armRegionName eq 'westeurope' and skuName eq 'Standard' and productName eq 'Static Web Apps' and meterName eq 'Standard App'",
"BillingCurrency": "USD",
"CustomerEntityId": "Default",
"CustomerEntityType": "Retail",
"Items": [
{
"currencyCode": "USD",
"tierMinimumUnits": 0.0,
"retailPrice": 9.0,
"unitPrice": 9.0,
"armRegionName": "westeurope",
"location": "EU West",
"effectiveStartDate": "2021-05-01T00:00:00Z",
"meterId": "56c80fab-f20c-5e41-951d-667dc9503604",
"meterName": "Standard App",
"productId": "DZH318Z08W5K",
"skuId": "DZH318Z08W5K/0002",
"productName": "Static Web Apps",
"skuName": "Standard",
"serviceName": "Azure App Service",
"serviceId": "DZH317GT8G5N",
"serviceFamily": "Compute",
"unitOfMeasure": "1/Month",
"type": "Consumption",
"isPrimaryMeterRegion": true,
"armSkuName": ""
}
],
"NextPageLink": null,
"Count": 1
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
param parSuffix string = utcNow('yyyyMMddhhmmss')
param parLocation string = resourceGroup().location

resource app 'Microsoft.Web/staticSites@2023-01-01' = {
#disable-next-line use-stable-resource-identifiers
name: 'static-site-${parSuffix}'
location: parLocation
sku: {
name: 'Free'
}
properties: {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
param parSuffix string = utcNow('yyyyMMddhhmmss')
param parLocation string = resourceGroup().location

resource app 'Microsoft.Web/staticSites@2023-01-01' = {
#disable-next-line use-stable-resource-identifiers
name: 'static-site-${parSuffix}'
location: parLocation
sku: {
name: 'Standard'
}
properties: {}
}
12 changes: 3 additions & 9 deletions ace/Products/ContainerRegistry/ContainerRegistryQueryFilter.cs
Original file line number Diff line number Diff line change
@@ -1,18 +1,12 @@
using ACE.WhatIf;
using Microsoft.Extensions.Logging;

internal class ContainerRegistryQueryFilter : IQueryFilter
internal class ContainerRegistryQueryFilter(WhatIfAfterBeforeChange afterState, ILogger logger) : IQueryFilter
{
private const string ServiceId = "DZH315F9L8DM";

private readonly WhatIfAfterBeforeChange afterState;
private readonly ILogger logger;

public ContainerRegistryQueryFilter(WhatIfAfterBeforeChange afterState, ILogger logger)
{
this.afterState = afterState;
this.logger = logger;
}
private readonly WhatIfAfterBeforeChange afterState = afterState;
private readonly ILogger logger = logger;

public string? GetFiltersBasedOnDesiredState(string location)
{
Expand Down
13 changes: 5 additions & 8 deletions ace/Products/ContainerRegistry/ContainerRegistryRetailQuery.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
using ACE.WhatIf;
using Azure.Core;
using ACE.Extensions;
using ACE.WhatIf;
using Microsoft.Extensions.Logging;

internal class ContainerRegistryRetailQuery : BaseRetailQuery, IRetailQuery
internal class ContainerRegistryRetailQuery(WhatIfChange change, CommonResourceIdentifier id, ILogger logger, CurrencyCode currency, WhatIfChange[] changes, TemplateSchema template)
: BaseRetailQuery(change, id, logger, currency, changes, template), IRetailQuery
{
public ContainerRegistryRetailQuery(WhatIfChange change, CommonResourceIdentifier id, ILogger logger, CurrencyCode currency, WhatIfChange[] changes, TemplateSchema template) : base(change, id, logger, currency, changes, template)
{
}

public RetailAPIResponse? GetFakeResponse()
{
throw new NotImplementedException();
Expand All @@ -21,7 +18,7 @@ public ContainerRegistryRetailQuery(WhatIfChange change, CommonResourceIdentifie
return null;
}

var change = this.change.after == null ? this.change.before : this.change.after;
var change = this.change.GetChange();
if (change == null)
{
this.logger.LogError("Couldn't determine after / before state.");
Expand Down
41 changes: 41 additions & 0 deletions ace/Products/StaticWebApp/StaticWebAppEstimationCalculation.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using ACE.Calculation;
using ACE.WhatIf;

namespace ACE;

internal class StaticWebAppEstimationCalculation(RetailItem[] items, CommonResourceIdentifier id, WhatIfAfterBeforeChange change, double conversionRate)
: BaseEstimation(items, id, change, conversionRate), IEstimationCalculation
{
public IOrderedEnumerable<RetailItem> GetItems()
{
return this.items.OrderByDescending(_ => _.retailPrice);
}

public TotalCostSummary GetTotalCost(WhatIfChange[] changess, IDictionary<string, string>? usagePatterns)
{
double? estimatedCost = 0;
var items = GetItems();
var summary = new TotalCostSummary();

foreach (var item in items)
{
double? cost = 0;
cost += item.retailPrice;

estimatedCost += cost;

if (summary.DetailedCost.ContainsKey(item.meterName!))
{
summary.DetailedCost[item.meterName!] += cost;
}
else
{
summary.DetailedCost.Add(item.meterName!, cost);
}
}

summary.TotalCost = estimatedCost.GetValueOrDefault();

return summary;
}
}
27 changes: 27 additions & 0 deletions ace/Products/StaticWebApp/StaticWebAppQueryFilter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using ACE.WhatIf;
using Microsoft.Extensions.Logging;

internal class StaticWebAppQueryFilter(WhatIfAfterBeforeChange afterState, ILogger logger) : IQueryFilter
{
private const string ServiceId = "DZH317GT8G5N";

private readonly WhatIfAfterBeforeChange afterState = afterState;
private readonly ILogger logger = logger;

public string? GetFiltersBasedOnDesiredState(string location)
{
var sku = this.afterState.sku?.name;
if (sku == null)
{
this.logger.LogError("Can't create a filter for Static Web App when SKU is unavailable.");
return null;
}

if(sku == "Free")
{
return "FREE";
}

return $"serviceId eq '{ServiceId}' and armRegionName eq '{location}' and skuName eq '{sku}' and productName eq 'Static Web Apps' and meterName eq 'Standard App'";
}
}
36 changes: 36 additions & 0 deletions ace/Products/StaticWebApp/StaticWebAppRetailQuery.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
using ACE.Extensions;
using ACE.WhatIf;
using Microsoft.Extensions.Logging;

internal class StaticWebAppRetailQuery(WhatIfChange change, CommonResourceIdentifier id, ILogger logger, CurrencyCode currency, WhatIfChange[] changes, TemplateSchema template)
: BaseRetailQuery(change, id, logger, currency, changes, template), IRetailQuery
{
public RetailAPIResponse? GetFakeResponse()
{
throw new NotImplementedException();
}

public string? GetQueryUrl(string location)
{
if (this.change.after == null && this.change.before == null)
{
this.logger.LogError("Can't generate Retail API query if desired state is unavailable.");
return null;
}

var change = this.change.GetChange();
if (change == null)
{
this.logger.LogError("Couldn't determine after / before state.");
return null;
}

var filter = new StaticWebAppQueryFilter(change, this.logger).GetFiltersBasedOnDesiredState(location);
if(filter == "FREE")
{
return filter;
}

return $"{baseQuery}{filter}";
}
}
13 changes: 12 additions & 1 deletion ace/WhatIf/WhatIfProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -487,6 +487,9 @@ public async Task<EstimationOutput> Process(CancellationToken token)
resource = new EstimatedResourceData(0, 0, id);
freeResources.Add(id, change.changeType);
break;
case "Microsoft.Web/staticSites":
resource = await Calculate<StaticWebAppRetailQuery, StaticWebAppEstimationCalculation>(change, id, token);
break;
default:
if (id?.GetName() != null)
{
Expand Down Expand Up @@ -653,6 +656,14 @@ await GetRetailAPIResponse<TQuery>(change, id, token) :
return null;
}

if (url == "FREE")
{
return new RetailAPIResponse()
{
Items = Enumerable.Empty<RetailItem>().ToArray()
};
}

this.logger.AddDebugMessage(url, this.debug);
}
catch (KeyNotFoundException)
Expand Down Expand Up @@ -750,7 +761,7 @@ bool ShouldTakeMockedData()

private RetailAPIResponse? GetFakeRetailAPIResponse<T>(WhatIfChange change, CommonResourceIdentifier id) where T : BaseRetailQuery, IRetailQuery
{
if (Activator.CreateInstance(typeof(T), new object[] { change, id, logger, currency, changes, this.template }) is not T query)
if (Activator.CreateInstance(typeof(T), [change, id, logger, currency, changes, this.template]) is not T query)
{
logger.LogError("Couldn't create an instance of {type}.", typeof(T));
return null;
Expand Down

0 comments on commit 99d778c

Please sign in to comment.