Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 28 additions & 6 deletions infra_cost_model/pricing/sources/infracost.py
Original file line number Diff line number Diff line change
Expand Up @@ -270,12 +270,22 @@ def sync_to_cache(self, cache, usage_metric: str, region: str,
return self._upsert_regionless_usagetype(
cache, prices, usage_metric, region, unit_match, descriptor, now)

# Some products are priced by Infracost under a different service than the
# handler/seed model them (e.g. NAT Gateway is priced under AmazonEC2 but
# modeled under AmazonVPC). `store_service` upserts them under the service
# the engine queries. `usagetype_exclude` drops sibling usagetypes that
# share the same unit (e.g. NAT Gateway's $0 "Prvd" provisioned rows).
store_service = descriptor.get("store_service")
excludes = descriptor.get("usagetype_exclude") or []
count = 0
for p in prices:
if unit_match and p.get("unit") != unit_match:
continue
usagetype = (p.get("attributes") or {}).get("usagetype", "")
if any(x in usagetype for x in excludes):
continue
cache.upsert(Price(
vendor=p["vendor"], service=p["service"], region=p["region"],
vendor=p["vendor"], service=store_service or p["service"], region=p["region"],
product_family=p["product_family"], attributes=p["attributes"],
usage_metric=usage_metric, unit=p["unit"], price_usd=p["price_usd"],
start_usage_amount=p["start_usage_amount"],
Expand Down Expand Up @@ -426,11 +436,23 @@ def _upsert_regionless_usagetype(self, cache, prices, usage_metric,
"attribute_filters": [{"key": "group", "value": "ELB:Balancing"}],
"unit": "LCU-Hrs",
},
# NAT Gateway: the Infracost catalog doesn't currently expose NAT GW under a
# standard productFamily, so these entries are present but not yet live-validated.
# The infracost CLI does price this resource; the product grouping is TBD.
#"NAT-Gateway-Hour": { "service": "AmazonVPC" },
#"NAT-Gateway-DataProcessed": { "service": "AmazonVPC" },
# NAT Gateway: Infracost prices this under service "AmazonEC2" / productFamily
# "NAT Gateway" (operation=NatGateway distinguishes it from RegionalNatGateway),
# but the handler and seed model it under "AmazonVPC" — so store_service remaps
# it there. Hourly is a single Hrs row; data-processed shares its GB unit with a
# $0 "Prvd" (provisioned-throughput) row, excluded via usagetype_exclude.
"NAT-Gateway-Hour": {
"service": "AmazonEC2", "store_service": "AmazonVPC",
"product_family": "NAT Gateway",
"attribute_filters": [{"key": "operation", "value": "NatGateway"}],
"unit": "Hrs",
},
"NAT-Gateway-DataProcessed": {
"service": "AmazonEC2", "store_service": "AmazonVPC",
"product_family": "NAT Gateway",
"attribute_filters": [{"key": "operation", "value": "NatGateway"}],
"unit": "GB", "usagetype_exclude": ["Prvd"],
},
# VPC Interface Endpoint (PrivateLink): ENI-hour + per-GB.
"VPC-Endpoint-Hour": {
"service": "AmazonVPC", "product_family": "VpcEndpoint",
Expand Down
55 changes: 55 additions & 0 deletions tests/test_infracost_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -469,3 +469,58 @@ def test_sync_pricing_catalog_defaults_to_us_east_1(monkeypatch):
)
ic.sync_pricing_catalog(services=["KMS-Key-Month"])
assert calls == ["us-east-1"]


# --- NAT Gateway (priced under AmazonEC2, stored under AmazonVPC) ---------------

def _nat_product(usagetype, usd, unit):
return {
"productFamily": "NAT Gateway",
"attributes": [
{"key": "usagetype", "value": usagetype},
{"key": "operation", "value": "NatGateway"},
],
"prices": [{"USD": str(usd), "unit": unit,
"startUsageAmount": "0", "endUsageAmount": None}],
}


def test_nat_gateway_descriptors_present():
for m in ("NAT-Gateway-Hour", "NAT-Gateway-DataProcessed"):
d = ic.METRIC_DESCRIPTORS[m]
assert d["service"] == "AmazonEC2"
assert d["store_service"] == "AmazonVPC"
assert d["product_family"] == "NAT Gateway"


def test_nat_hour_remaps_service_to_vpc(monkeypatch):
_set_creds(monkeypatch)
products = [_nat_product("NatGateway-Hours", 0.045, "Hrs")]
upserted = []
cache = MagicMock()
cache.upsert.side_effect = lambda p: upserted.append(p)
with patch.object(ic.requests, "post", return_value=_graphql_response(products)):
n = ic.InfracostClient().sync_to_cache(cache, "NAT-Gateway-Hour", "us-east-1")
assert n == 1
# Queried under AmazonEC2 but stored under the service the handler models it as.
assert upserted[0].service == "AmazonVPC"
assert upserted[0].usage_metric == "NAT-Gateway-Hour"
assert upserted[0].price_usd == pytest.approx(0.045)


def test_nat_data_processed_excludes_prvd(monkeypatch):
"""The $0 provisioned-throughput 'Prvd' row shares the GB unit and must be
excluded so it doesn't dilute the data-processed rate."""
_set_creds(monkeypatch)
products = [
_nat_product("NatGateway-Bytes", 0.045, "GB"),
_nat_product("NatGateway-Prvd-Bytes", 0.0, "GB"), # excluded
]
upserted = []
cache = MagicMock()
cache.upsert.side_effect = lambda p: upserted.append(p)
with patch.object(ic.requests, "post", return_value=_graphql_response(products)):
n = ic.InfracostClient().sync_to_cache(cache, "NAT-Gateway-DataProcessed", "us-east-1")
assert n == 1
assert upserted[0].service == "AmazonVPC"
assert upserted[0].price_usd == pytest.approx(0.045)
Loading