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
57 changes: 57 additions & 0 deletions infra_cost_model/pricing/sources/infracost.py
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,63 @@ def sync_to_cache(self, cache, usage_metric: str, region: str,
"attribute_filters": [{"key": "group", "value": "S3-API-PutObject"}],
"unit": "Requests",
},
# KMS (#208): $1/customer-managed key-month + per-symmetric-request.
# Note: the Infracost service code for KMS is lowercase "awskms".
"KMS-Key-Month": {
"service": "awskms", "product_family": "Encryption Key",
"unit": "Keys",
},
"KMS-API-Request": {
"service": "awskms", "product_family": "API Request",
"attribute_filters": [{"key": "group", "value": "awskms-APIRequest-All"}],
"unit": "Requests",
},
# 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
# selects the address (and distinguishes in-use from idle).
"IPv4-InUse-Hours": {
"service": "AmazonVPC",
"attribute_filters": [{"key": "usagetype", "value": "REGION_PREFIX-PublicIPv4:InUseAddress"}],
"unit": "Hrs",
},
"IPv4-Idle-Hours": {
"service": "AmazonVPC",
"attribute_filters": [{"key": "usagetype", "value": "REGION_PREFIX-PublicIPv4:IdleAddress"}],
"unit": "Hrs",
},
# CloudWatch Metrics/Alarms (#209). Custom-metric pricing is tiered
# ($0.30 / $0.10 / $0.05 / $0.02) and comes back as multiple tiers under the
# one usagetype. GetMetricData is a per-metric API request (excludes the
# GetMetricWidgetImage rows that share the family) with a 1M free tier.
"CloudWatch-Metric-Month": {
"service": "AmazonCloudWatch", "product_family": "Metric",
"attribute_filters": [{"key": "usagetype", "value": "CW:MetricMonitorUsage"}],
"unit": "Metrics",
},
"CloudWatch-Alarm-Month": {
"service": "AmazonCloudWatch", "product_family": "Alarm",
"attribute_filters": [{"key": "usagetype", "value": "CW:AlarmMonitorUsage"}],
"unit": "Alarms",
},
"CloudWatch-GetMetricData": {
"service": "AmazonCloudWatch", "product_family": "API Request",
"attribute_filters": [{"key": "usagetype", "value": "CW:GMD-Metrics"}],
"unit": "Metrics",
},
# Inter-region data transfer (#211): priced under service "AWSDataTransfer",
# but those products are catalogued globally (empty region) with a distinct
# usagetype PER source/destination region pair (e.g. USE1-APS4-AWS-Out-Bytes
# at $0.02/GB, transferType "InterRegion Outbound"). That doesn't fit the
# region-scoped sync (which queries region="us-east-1"), and collapsing every
# region pair into a single DataTransfer-InterRegion-GB rate would be
# arbitrary. Left seed-only until the sync grows regionless / region-pair
# support; documented here so the grouping is known.
#"DataTransfer-InterRegion-GB": {
# "service": "AWSDataTransfer", # region="" (global); usagetype per region pair
# "attribute_filters": [{"key": "transferType", "value": "InterRegion Outbound"}],
# "unit": "GB",
#},
}


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 @@ -151,3 +151,58 @@ def test_no_warning_when_unauthenticated(monkeypatch, tmp_path):
warnings.simplefilter("error")
count, source = ic.sync_pricing_catalog()
assert source == "seed-pricelist"


# --- Descriptors added for #208-#211 handlers (issue #217) ---------------------

@pytest.mark.parametrize("metric,service,unit", [
("KMS-Key-Month", "awskms", "Keys"),
("KMS-API-Request", "awskms", "Requests"),
("IPv4-InUse-Hours", "AmazonVPC", "Hrs"),
("IPv4-Idle-Hours", "AmazonVPC", "Hrs"),
("CloudWatch-Metric-Month", "AmazonCloudWatch", "Metrics"),
("CloudWatch-Alarm-Month", "AmazonCloudWatch", "Alarms"),
("CloudWatch-GetMetricData", "AmazonCloudWatch", "Metrics"),
])
def test_new_handler_descriptors_present(metric, service, unit):
d = ic.METRIC_DESCRIPTORS[metric]
assert d["service"] == service
assert d["unit"] == unit


def test_ipv4_descriptor_resolves_region_prefix(monkeypatch):
"""The EIP usagetype filter must have REGION_PREFIX resolved to the region's
short code (us-east-1 -> USE1) before the query is sent."""
_set_creds(monkeypatch)
d = ic.METRIC_DESCRIPTORS["IPv4-InUse-Hours"]
with patch.object(ic.requests, "post", return_value=_graphql_response([])) as post:
ic.InfracostClient().query_prices(
service=d["service"], region="us-east-1",
attribute_filters=d["attribute_filters"],
)
sent = post.call_args.kwargs["json"]["variables"]["attributeFilters"]
assert sent == [{"key": "usagetype", "value": "USE1-PublicIPv4:InUseAddress"}]


def test_sync_to_cache_kms_key_month_upserts(monkeypatch):
"""End-to-end (mocked HTTP): the KMS-Key-Month descriptor stores the fetched
price under the catalog usage_metric name with source=infracost."""
_set_creds(monkeypatch)
products = [{
"productFamily": "Encryption Key",
"attributes": [{"key": "usagetype", "value": "us-east-1-KMS-Keys"}],
"prices": [
{"USD": "1.0", "unit": "Keys", "startUsageAmount": "0", "endUsageAmount": None},
{"USD": "9.99", "unit": "WRONG-UNIT", "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, "KMS-Key-Month", "us-east-1")
assert n == 1 # WRONG-UNIT row filtered out by the descriptor's unit
assert upserted[0].usage_metric == "KMS-Key-Month"
assert upserted[0].service == "awskms"
assert upserted[0].price_usd == pytest.approx(1.0)
assert upserted[0].source == "infracost"
Loading