CostExplorer - get_cost_and_usage() (#7385)

This commit is contained in:
Bert Blommers 2024-02-23 21:40:15 +00:00 committed by GitHub
parent 6505971ecb
commit 3a8d3a3a08
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 191 additions and 3 deletions

View File

@ -735,7 +735,7 @@
## ce
<details>
<summary>18% implemented</summary>
<summary>21% implemented</summary>
- [ ] create_anomaly_monitor
- [ ] create_anomaly_subscription
@ -747,7 +747,7 @@
- [ ] get_anomalies
- [ ] get_anomaly_monitors
- [ ] get_anomaly_subscriptions
- [ ] get_cost_and_usage
- [X] get_cost_and_usage
- [ ] get_cost_and_usage_with_resources
- [ ] get_cost_categories
- [ ] get_cost_forecast

View File

@ -38,7 +38,50 @@ ce
- [ ] get_anomalies
- [ ] get_anomaly_monitors
- [ ] get_anomaly_subscriptions
- [ ] get_cost_and_usage
- [X] get_cost_and_usage
There is no validation yet on any of the input parameters.
Cost or usage is not tracked by Moto, so this call will return nothing by default.
You can use a dedicated API to override this, by configuring a queue of expected results.
A request to `get_cost_and_usage` will take the first result from that queue, and assign it to the provided parameters. Subsequent requests using the same parameters will return the same result. Other requests using different parameters will take the next result from the queue, or return an empty result if the queue is empty.
Configure this queue by making an HTTP request to `/moto-api/static/ce/cost-and-usage-results`. An example invocation looks like this:
.. sourcecode:: python
result = {
"results": [
{
"ResultsByTime": [
{
"TimePeriod": {"Start": "2024-01-01", "End": "2024-01-02"},
"Total": {
"BlendedCost": {"Amount": "0.0101516483", "Unit": "USD"}
},
"Groups": [],
"Estimated": False
}
],
"DimensionValueAttributes": [{"Value": "v", "Attributes": {"a": "b"}}]
},
{
...
},
]
}
resp = requests.post(
"http://motoapi.amazonaws.com:5000/moto-api/static/ce/cost-and-usage-results",
json=expected_results,
)
assert resp.status_code == 201
ce = boto3.client("ce", region_name="us-east-1")
resp = ce.get_cost_and_usage(...)
- [ ] get_cost_and_usage_with_resources
- [ ] get_cost_categories
- [ ] get_cost_forecast

View File

@ -74,6 +74,8 @@ class CostExplorerBackend(BaseBackend):
def __init__(self, region_name: str, account_id: str):
super().__init__(region_name, account_id)
self.cost_categories: Dict[str, CostCategoryDefinition] = dict()
self.cost_usage_results_queue: List[Dict[str, Any]] = []
self.cost_usage_results: Dict[str, Dict[str, Any]] = {}
self.tagger = TaggingService()
def create_cost_category_definition(
@ -154,6 +156,57 @@ class CostExplorerBackend(BaseBackend):
def untag_resource(self, resource_arn: str, tag_keys: List[str]) -> None:
self.tagger.untag_resource_using_names(resource_arn, tag_keys)
def get_cost_and_usage(self, body: str) -> Dict[str, Any]:
"""
There is no validation yet on any of the input parameters.
Cost or usage is not tracked by Moto, so this call will return nothing by default.
You can use a dedicated API to override this, by configuring a queue of expected results.
A request to `get_cost_and_usage` will take the first result from that queue, and assign it to the provided parameters. Subsequent requests using the same parameters will return the same result. Other requests using different parameters will take the next result from the queue, or return an empty result if the queue is empty.
Configure this queue by making an HTTP request to `/moto-api/static/ce/cost-and-usage-results`. An example invocation looks like this:
.. sourcecode:: python
result = {
"results": [
{
"ResultsByTime": [
{
"TimePeriod": {"Start": "2024-01-01", "End": "2024-01-02"},
"Total": {
"BlendedCost": {"Amount": "0.0101516483", "Unit": "USD"}
},
"Groups": [],
"Estimated": False
}
],
"DimensionValueAttributes": [{"Value": "v", "Attributes": {"a": "b"}}]
},
{
...
},
]
}
resp = requests.post(
"http://motoapi.amazonaws.com:5000/moto-api/static/ce/cost-and-usage-results",
json=expected_results,
)
assert resp.status_code == 201
ce = boto3.client("ce", region_name="us-east-1")
resp = ce.get_cost_and_usage(...)
"""
default_result: Dict[str, Any] = {
"ResultsByTime": [],
"DimensionValueAttributes": [],
}
if body not in self.cost_usage_results and self.cost_usage_results_queue:
self.cost_usage_results[body] = self.cost_usage_results_queue.pop(0)
return self.cost_usage_results.get(body, default_result)
ce_backends = BackendDict(
CostExplorerBackend, "ce", use_boto3_regions=False, additional_regions=["global"]

View File

@ -102,3 +102,7 @@ class CostExplorerResponse(BaseResponse):
tag_names = params.get("ResourceTagKeys")
self.ce_backend.untag_resource(resource_arn, tag_names)
return json.dumps({})
def get_cost_and_usage(self) -> str:
resp = self.ce_backend.get_cost_and_usage(self.body)
return json.dumps(resp)

View File

@ -42,6 +42,12 @@ class MotoAPIBackend(BaseBackend):
results = QueryResults(rows=rows, column_info=column_info)
backend.query_results_queue.append(results)
def set_ce_cost_usage(self, result: Dict[str, Any], account_id: str) -> None:
from moto.ce.models import ce_backends
backend = ce_backends[account_id]["global"]
backend.cost_usage_results_queue.append(result)
def set_sagemaker_result(
self,
body: str,

View File

@ -168,6 +168,23 @@ class MotoAPIResponse(BaseResponse):
)
return 201, {}, ""
def set_ce_cost_usage_result(
self,
request: Any,
full_url: str, # pylint: disable=unused-argument
headers: Any,
) -> TYPE_RESPONSE:
from .models import moto_api_backend
request_body_size = int(headers["Content-Length"])
body = request.environ["wsgi.input"].read(request_body_size).decode("utf-8")
body = json.loads(body)
account_id = body.get("account_id", DEFAULT_ACCOUNT_ID)
for result in body.get("results", []):
moto_api_backend.set_ce_cost_usage(result=result, account_id=account_id)
return 201, {}, ""
def set_sagemaker_result(
self,
request: Any,

View File

@ -13,6 +13,7 @@ url_paths = {
"{0}/moto-api/reset-auth": response_instance.reset_auth_response,
"{0}/moto-api/seed": response_instance.seed,
"{0}/moto-api/static/athena/query-results": response_instance.set_athena_result,
"{0}/moto-api/static/ce/cost-and-usage-results": response_instance.set_ce_cost_usage_result,
"{0}/moto-api/static/inspector2/findings-results": response_instance.set_inspector2_findings_result,
"{0}/moto-api/static/sagemaker/endpoint-results": response_instance.set_sagemaker_result,
"{0}/moto-api/static/rds-data/statement-results": response_instance.set_rds_data_result,

View File

@ -0,0 +1,64 @@
import boto3
import requests
from moto import mock_aws, settings
# See our Development Tips on writing tests for hints on how to write good tests:
# http://docs.getmoto.org/en/latest/docs/contributing/development_tips/tests.html
@mock_aws
def test_get_cost_and_usage():
ce = boto3.client("ce", region_name="eu-west-1")
resp = ce.get_cost_and_usage(
TimePeriod={"Start": "2024-01-01", "End": "2024-02-01"},
Granularity="DAILY",
Metrics=["UNBLENDEDCOST"],
)
assert resp["DimensionValueAttributes"] == []
assert resp["ResultsByTime"] == []
@mock_aws
def test_set_query_results():
base_url = (
settings.test_server_mode_endpoint()
if settings.TEST_SERVER_MODE
else "http://motoapi.amazonaws.com"
)
cost_and_usage_result = {
"results": [
{
"ResultsByTime": [
{
"TimePeriod": {"Start": "2024-01-01", "End": "2024-01-02"},
"Total": {
"BlendedCost": {"Amount": "0.0101516483", "Unit": "USD"}
},
"Groups": [],
"Estimated": False,
}
],
"DimensionValueAttributes": [{"Value": "v", "Attributes": {"a": "b"}}],
}
],
}
resp = requests.post(
f"{base_url}/moto-api/static/ce/cost-and-usage-results",
json=cost_and_usage_result,
)
assert resp.status_code == 201
ce = boto3.client("ce", region_name="us-west-1")
resp = ce.get_cost_and_usage(
TimePeriod={"Start": "2024-01-01", "End": "2024-02-01"},
Granularity="DAILY",
Metrics=["BLENDEDCOST", "AMORTIZEDCOST"],
)
resp.pop("ResponseMetadata")
assert resp == cost_and_usage_result["results"][0]