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
27 changes: 27 additions & 0 deletions infra_cost_model/pricing/sources/infracost.py
Original file line number Diff line number Diff line change
Expand Up @@ -510,6 +510,33 @@ def _upsert_regionless_usagetype(self, cache, prices, usage_metric,
"attribute_filters": [{"key": "group", "value": "awskms-APIRequest-All"}],
"unit": "Requests",
},
# WAFv2 (#234): web ACL + per-rule monthly + per-request inspection. Infracost
# prices these under the lowercase service code "awswaf", product family
# "Web Application Firewall"; `store_service` upserts them under "AWSWAF" so
# the handler/seed (which use the uppercase convention) can query them — same
# remap the NAT Gateway descriptor does (EC2 → AmazonVPC).
# The usagetype encodes the region as a short prefix (USE1- / CAN1- / …),
# resolved at query time; the "V2" suffix distinguishes WAFv2 from classic
# WAF, and the exact-match value naturally excludes the "ShieldProtected-"
# siblings (Infracost matches these with a `(?!ShieldProtected-)` regex).
# RequestV2-Tier1 is the standard per-request inspection tier. No `unit`
# filter: each usagetype resolves to a single price row, so filtering by it
# would only risk a spurious miss on the (region-independent) unit string.
"WAF-WebACL-Month": {
"service": "awswaf", "store_service": "AWSWAF",
"product_family": "Web Application Firewall",
"attribute_filters": [{"key": "usagetype", "value": "REGION_PREFIX-WebACLV2"}],
},
"WAF-Rule-Month": {
"service": "awswaf", "store_service": "AWSWAF",
"product_family": "Web Application Firewall",
"attribute_filters": [{"key": "usagetype", "value": "REGION_PREFIX-RuleV2"}],
},
"WAF-Request": {
"service": "awswaf", "store_service": "AWSWAF",
"product_family": "Web Application Firewall",
"attribute_filters": [{"key": "usagetype", "value": "REGION_PREFIX-RequestV2-Tier1"}],
},
# Public IPv4 address (#210): $0.005/hr in-use or idle. The usagetype encodes
# the region as a short prefix (USE1- / …); REGION_PREFIX is resolved at query
# time. Product family is unset on these rows, so the usagetype filter alone
Expand Down
56 changes: 56 additions & 0 deletions tests/test_infracost_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,62 @@ def test_sync_to_cache_kms_key_month_upserts(monkeypatch):
assert upserted[0].source == "infracost"


# --- WAFv2 descriptors (#234) --------------------------------------------------

@pytest.mark.parametrize("metric,usagetype_base", [
("WAF-WebACL-Month", "WebACLV2"),
("WAF-Rule-Month", "RuleV2"),
("WAF-Request", "RequestV2-Tier1"),
])
def test_waf_descriptors_present(metric, usagetype_base):
d = ic.METRIC_DESCRIPTORS[metric]
assert d["service"] == "awswaf"
assert d["product_family"] == "Web Application Firewall"
assert d["attribute_filters"] == [
{"key": "usagetype", "value": f"REGION_PREFIX-{usagetype_base}"}
]


@pytest.mark.parametrize("region,prefix", [("us-east-1", "USE1"), ("ca-central-1", "CAN1")])
def test_waf_descriptor_resolves_region_prefix(monkeypatch, region, prefix):
"""The web-ACL usagetype filter must have REGION_PREFIX resolved to the
region's short code before the query is sent (USE1-WebACLV2 / CAN1-WebACLV2)."""
_set_creds(monkeypatch)
d = ic.METRIC_DESCRIPTORS["WAF-WebACL-Month"]
with patch.object(ic.requests, "post", return_value=_graphql_response([])) as post:
ic.InfracostClient().query_prices(
service=d["service"], region=region,
attribute_filters=d["attribute_filters"],
)
sent = post.call_args.kwargs["json"]["variables"]["attributeFilters"]
assert sent == [{"key": "usagetype", "value": f"{prefix}-WebACLV2"}]


def test_sync_to_cache_waf_webacl_upserts(monkeypatch):
"""End-to-end (mocked HTTP): the WAF-WebACL-Month descriptor stores the
fetched price under the catalog usage_metric name with source=infracost, and
remaps the service to the uppercase "AWSWAF" the handler/seed query."""
_set_creds(monkeypatch)
products = [{
"productFamily": "Web Application Firewall",
"attributes": [{"key": "usagetype", "value": "USE1-WebACLV2"}],
"prices": [
{"USD": "5.0", "unit": "Month", "startUsageAmount": "0", "endUsageAmount": None},
],
}]
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, "WAF-WebACL-Month", "us-east-1")
assert n == 1
assert upserted[0].usage_metric == "WAF-WebACL-Month"
# store_service remaps Infracost's lowercase "awswaf" to the queried "AWSWAF".
assert upserted[0].service == "AWSWAF"
assert upserted[0].price_usd == pytest.approx(5.0)
assert upserted[0].source == "infracost"


# --- Regionless / region-pair data-transfer sync -------------------------------

def _dt_product(usagetype, usd, transfer_type="InterRegion Outbound", unit="GB"):
Expand Down
Loading