diff --git a/documentation/docs/tirith-providers/infracost.md b/documentation/docs/tirith-providers/infracost.md index a3b694fc..08c5beca 100644 --- a/documentation/docs/tirith-providers/infracost.md +++ b/documentation/docs/tirith-providers/infracost.md @@ -26,7 +26,7 @@ tirith -policy-path policy.json -input-path infracost.json The provider reads `projects[].breakdown.resources[]` and understands both the older per-resource keys (`totalMonthlyCost` / `totalHourlyCost`) and the newer ones (`monthlyCost` / `hourlyCost`). Resources whose cost field is missing or `null` contribute nothing to the sum. -Note: only the **first** project in the `projects` array is summed. +Every project in the `projects` array is summed. ## Operation types diff --git a/documentation/static/docs/tirith-providers/infracost-provider.md b/documentation/static/docs/tirith-providers/infracost-provider.md index d8450105..37c25c9b 100644 --- a/documentation/static/docs/tirith-providers/infracost-provider.md +++ b/documentation/static/docs/tirith-providers/infracost-provider.md @@ -20,7 +20,7 @@ tirith -policy-path policy.json -input-path infracost.json The provider reads `projects[].breakdown.resources[]` and understands both the older per-resource keys (`totalMonthlyCost` / `totalHourlyCost`) and the newer ones (`monthlyCost` / `hourlyCost`). Resources whose cost field is missing or `null` contribute nothing to the sum. -Note: only the **first** project in the `projects` array is summed. +Every project in the `projects` array is summed. ## Operation types diff --git a/documentation/static/llms-full.txt b/documentation/static/llms-full.txt index 6be335af..1200baa9 100644 --- a/documentation/static/llms-full.txt +++ b/documentation/static/llms-full.txt @@ -2049,7 +2049,7 @@ tirith -policy-path policy.json -input-path infracost.json The provider reads `projects[].breakdown.resources[]` and understands both the older per-resource keys (`totalMonthlyCost` / `totalHourlyCost`) and the newer ones (`monthlyCost` / `hourlyCost`). Resources whose cost field is missing or `null` contribute nothing to the sum. -Note: only the **first** project in the `projects` array is summed. +Every project in the `projects` array is summed. ## Operation types diff --git a/src/tirith/providers/infracost/handler.py b/src/tirith/providers/infracost/handler.py index 786b86d0..ad7f9023 100644 --- a/src/tirith/providers/infracost/handler.py +++ b/src/tirith/providers/infracost/handler.py @@ -33,10 +33,10 @@ def __get_all_costs(operation_type, input_data): else: pass # raise KeyError(f'{costType} not found in one of the resource') - logger.debug(f"Total sum of {operation_type} of all resources : {total_sum}") - return total_sum, matched_count else: raise KeyError("breakdown/resources not found in one of the project") + logger.debug(f"Total sum of {operation_type} of all resources : {total_sum}") + return total_sum, matched_count else: raise KeyError("projects not found in input_data") @@ -75,10 +75,10 @@ def __get_resources_costs(resource_type, operation_type, input_data): else: pass # raise KeyError(f'{costType} not found in one of the resource') - logger.debug(f"Total sum of {operation_type} of specific resources : {total_sum}") - return total_sum, matched_count else: raise KeyError("breakdown/resources not found in one of the project") + logger.debug(f"Total sum of {operation_type} of specific resources : {total_sum}") + return total_sum, matched_count else: raise KeyError("projects not found in input_data") diff --git a/tests/providers/infracost/test_infracost_multiple_projects.py b/tests/providers/infracost/test_infracost_multiple_projects.py new file mode 100644 index 00000000..3e71ebe6 --- /dev/null +++ b/tests/providers/infracost/test_infracost_multiple_projects.py @@ -0,0 +1,61 @@ +""" +Tests for infracost input with more than one project. + +`infracost breakdown` writes one entry in `projects` per Terraform directory it finds. Only the +first one used to be summed, so a cost limit passed whenever the expensive project was not first. +""" + +from pytest import mark + +from tirith.core.core import start_policy_evaluation_from_dict +from tirith.providers.infracost import handler + + +INPUT = { + "currency": "USD", + "projects": [ + { + "name": "networking", + "breakdown": {"resources": [{"name": "aws_nat_gateway.main", "monthlyCost": "10"}]}, + }, + { + "name": "data", + "breakdown": {"resources": [{"name": "aws_redshift_cluster.warehouse", "monthlyCost": "5000"}]}, + }, + ], +} + + +def evaluate(provider_args, condition): + policy = { + "meta": {"version": "v1", "required_provider": "stackguardian/infracost"}, + "evaluators": [{"id": "cost", "provider_args": provider_args, "condition": condition}], + "eval_expression": "cost", + } + return start_policy_evaluation_from_dict(policy, INPUT) + + +@mark.passing +def test_total_cost_sums_every_project(): + result = handler.provide({"operation_type": "total_monthly_cost", "resource_type": ["*"]}, INPUT) + + assert result[0]["value"] == 5010 + assert result[0]["context"]["matched_resources"] == 2 + + +@mark.passing +def test_resource_type_is_found_outside_the_first_project(): + result = handler.provide({"operation_type": "total_monthly_cost", "resource_type": ["aws_redshift_cluster"]}, INPUT) + + assert result[0]["value"] == 5000 + assert result[0]["context"]["matched_resources"] == 1 + + +@mark.passing +def test_cost_limit_fails_when_a_later_project_exceeds_it(): + result = evaluate( + {"operation_type": "total_monthly_cost", "resource_type": ["*"]}, + {"type": "LessThanEqualTo", "value": 1000}, + ) + + assert result["final_result"] is False