qontract-reconcile 0.10.1rc801__py3-none-any.whl → 0.10.1rc803__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {qontract_reconcile-0.10.1rc801.dist-info → qontract_reconcile-0.10.1rc803.dist-info}/METADATA +1 -1
- {qontract_reconcile-0.10.1rc801.dist-info → qontract_reconcile-0.10.1rc803.dist-info}/RECORD +25 -12
- reconcile/gql_definitions/cost_report/cost_namespaces.py +84 -0
- reconcile/statuspage/__init__.py +0 -0
- reconcile/statuspage/atlassian.py +425 -0
- reconcile/statuspage/integration.py +22 -0
- reconcile/statuspage/integrations/__init__.py +0 -0
- reconcile/statuspage/integrations/components.py +77 -0
- reconcile/statuspage/integrations/maintenances.py +73 -0
- reconcile/statuspage/page.py +114 -0
- reconcile/statuspage/state.py +47 -0
- reconcile/statuspage/status.py +97 -0
- reconcile/typed_queries/cost_report/cost_namespaces.py +40 -0
- tools/cli_commands/cost_report/{command.py → aws.py} +53 -75
- tools/cli_commands/cost_report/cost_management_api.py +28 -4
- tools/cli_commands/cost_report/model.py +7 -8
- tools/cli_commands/cost_report/openshift.py +179 -0
- tools/cli_commands/cost_report/response.py +25 -4
- tools/cli_commands/cost_report/util.py +59 -0
- tools/cli_commands/cost_report/view.py +121 -56
- tools/qontract_cli.py +10 -2
- tools/test/test_qontract_cli.py +32 -6
- {qontract_reconcile-0.10.1rc801.dist-info → qontract_reconcile-0.10.1rc803.dist-info}/WHEEL +0 -0
- {qontract_reconcile-0.10.1rc801.dist-info → qontract_reconcile-0.10.1rc803.dist-info}/entry_points.txt +0 -0
- {qontract_reconcile-0.10.1rc801.dist-info → qontract_reconcile-0.10.1rc803.dist-info}/top_level.txt +0 -0
@@ -1,12 +1,13 @@
|
|
1
|
-
from typing import List
|
2
|
-
|
3
1
|
from urllib3.util import Retry
|
4
2
|
|
5
3
|
from reconcile.utils.oauth2_backend_application_session import (
|
6
4
|
OAuth2BackendApplicationSession,
|
7
5
|
)
|
8
6
|
from reconcile.utils.rest_api_base import ApiBase
|
9
|
-
from tools.cli_commands.cost_report.response import
|
7
|
+
from tools.cli_commands.cost_report.response import (
|
8
|
+
OpenShiftReportCostResponse,
|
9
|
+
ReportCostResponse,
|
10
|
+
)
|
10
11
|
|
11
12
|
REQUEST_TIMEOUT = 60
|
12
13
|
|
@@ -18,7 +19,7 @@ class CostManagementApi(ApiBase):
|
|
18
19
|
token_url: str,
|
19
20
|
client_id: str,
|
20
21
|
client_secret: str,
|
21
|
-
scope:
|
22
|
+
scope: list[str] | None = None,
|
22
23
|
) -> None:
|
23
24
|
session = OAuth2BackendApplicationSession(
|
24
25
|
client_id=client_id,
|
@@ -56,3 +57,26 @@ class CostManagementApi(ApiBase):
|
|
56
57
|
)
|
57
58
|
response.raise_for_status()
|
58
59
|
return ReportCostResponse.parse_obj(response.json())
|
60
|
+
|
61
|
+
def get_openshift_costs_report(
|
62
|
+
self,
|
63
|
+
cluster: str,
|
64
|
+
project: str,
|
65
|
+
) -> OpenShiftReportCostResponse:
|
66
|
+
params = {
|
67
|
+
"delta": "cost",
|
68
|
+
"filter[resolution]": "monthly",
|
69
|
+
"filter[cluster]": cluster,
|
70
|
+
"filter[exact:project]": project,
|
71
|
+
"filter[time_scope_units]": "month",
|
72
|
+
"filter[time_scope_value]": "-2",
|
73
|
+
"group_by[project]": "*",
|
74
|
+
}
|
75
|
+
response = self.session.request(
|
76
|
+
method="GET",
|
77
|
+
url=f"{self.host}/reports/openshift/costs/",
|
78
|
+
params=params,
|
79
|
+
timeout=self.read_timeout,
|
80
|
+
)
|
81
|
+
response.raise_for_status()
|
82
|
+
return OpenShiftReportCostResponse.parse_obj(response.json())
|
@@ -1,11 +1,10 @@
|
|
1
1
|
from decimal import Decimal
|
2
|
-
from typing import List
|
3
2
|
|
4
3
|
from pydantic import BaseModel
|
5
4
|
|
6
5
|
|
7
|
-
class
|
8
|
-
|
6
|
+
class ReportItem(BaseModel):
|
7
|
+
name: str
|
9
8
|
delta_value: Decimal
|
10
9
|
delta_percent: float | None
|
11
10
|
total: Decimal
|
@@ -18,12 +17,12 @@ class ChildAppReport(BaseModel):
|
|
18
17
|
|
19
18
|
class Report(BaseModel):
|
20
19
|
app_name: str
|
21
|
-
child_apps:
|
20
|
+
child_apps: list[ChildAppReport]
|
22
21
|
child_apps_total: Decimal
|
23
22
|
date: str
|
24
23
|
parent_app_name: str | None
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
24
|
+
items: list[ReportItem]
|
25
|
+
items_delta_percent: float | None
|
26
|
+
items_delta_value: Decimal
|
27
|
+
items_total: Decimal
|
29
28
|
total: Decimal
|
@@ -0,0 +1,179 @@
|
|
1
|
+
from collections import defaultdict
|
2
|
+
from collections.abc import Iterable, Mapping
|
3
|
+
from decimal import Decimal
|
4
|
+
from typing import Self
|
5
|
+
|
6
|
+
from sretoolbox.utils import threaded
|
7
|
+
|
8
|
+
from reconcile.typed_queries.app_interface_vault_settings import (
|
9
|
+
get_app_interface_vault_settings,
|
10
|
+
)
|
11
|
+
from reconcile.typed_queries.cost_report.app_names import App, get_app_names
|
12
|
+
from reconcile.typed_queries.cost_report.cost_namespaces import (
|
13
|
+
CostNamespace,
|
14
|
+
get_cost_namespaces,
|
15
|
+
)
|
16
|
+
from reconcile.typed_queries.cost_report.settings import get_cost_report_settings
|
17
|
+
from reconcile.utils import gql
|
18
|
+
from reconcile.utils.secret_reader import create_secret_reader
|
19
|
+
from tools.cli_commands.cost_report.cost_management_api import CostManagementApi
|
20
|
+
from tools.cli_commands.cost_report.model import ChildAppReport, Report, ReportItem
|
21
|
+
from tools.cli_commands.cost_report.response import OpenShiftReportCostResponse
|
22
|
+
from tools.cli_commands.cost_report.util import process_reports
|
23
|
+
from tools.cli_commands.cost_report.view import render_openshift_cost_report
|
24
|
+
|
25
|
+
THREAD_POOL_SIZE = 10
|
26
|
+
|
27
|
+
|
28
|
+
class OpenShiftCostReportCommand:
|
29
|
+
def __init__(
|
30
|
+
self,
|
31
|
+
gql_api: gql.GqlApi,
|
32
|
+
cost_management_api: CostManagementApi,
|
33
|
+
thread_pool_size: int = THREAD_POOL_SIZE,
|
34
|
+
) -> None:
|
35
|
+
self.gql_api = gql_api
|
36
|
+
self.cost_management_api = cost_management_api
|
37
|
+
self.thread_pool_size = thread_pool_size
|
38
|
+
|
39
|
+
def execute(self) -> str:
|
40
|
+
apps = self.get_apps()
|
41
|
+
cost_namespaces = self.get_cost_namespaces()
|
42
|
+
responses = self.get_reports(cost_namespaces)
|
43
|
+
reports = self.process_reports(apps, responses)
|
44
|
+
return self.render(reports)
|
45
|
+
|
46
|
+
def get_apps(self) -> list[App]:
|
47
|
+
return get_app_names(self.gql_api)
|
48
|
+
|
49
|
+
def get_cost_namespaces(self) -> list[CostNamespace]:
|
50
|
+
return get_cost_namespaces(self.gql_api)
|
51
|
+
|
52
|
+
def _get_report(
|
53
|
+
self,
|
54
|
+
cost_namespace: CostNamespace,
|
55
|
+
) -> tuple[CostNamespace, OpenShiftReportCostResponse]:
|
56
|
+
cluster = (
|
57
|
+
cost_namespace.cluster_external_id
|
58
|
+
if cost_namespace.cluster_external_id is not None
|
59
|
+
else cost_namespace.cluster_name
|
60
|
+
)
|
61
|
+
response = self.cost_management_api.get_openshift_costs_report(
|
62
|
+
project=cost_namespace.name,
|
63
|
+
cluster=cluster,
|
64
|
+
)
|
65
|
+
return cost_namespace, response
|
66
|
+
|
67
|
+
def get_reports(
|
68
|
+
self,
|
69
|
+
cost_namespaces: Iterable[CostNamespace],
|
70
|
+
) -> dict[CostNamespace, OpenShiftReportCostResponse]:
|
71
|
+
"""
|
72
|
+
Fetch reports from cost management API
|
73
|
+
"""
|
74
|
+
results = threaded.run(self._get_report, cost_namespaces, self.thread_pool_size)
|
75
|
+
return dict(results)
|
76
|
+
|
77
|
+
def process_reports(
|
78
|
+
self,
|
79
|
+
apps: Iterable[App],
|
80
|
+
responses: Mapping[CostNamespace, OpenShiftReportCostResponse],
|
81
|
+
) -> dict[str, Report]:
|
82
|
+
"""
|
83
|
+
Build reports with parent-child app tree.
|
84
|
+
"""
|
85
|
+
app_responses = defaultdict(list)
|
86
|
+
for cost_namespace, response in responses.items():
|
87
|
+
app_responses[cost_namespace.app_name].append(response)
|
88
|
+
return process_reports(
|
89
|
+
apps,
|
90
|
+
app_responses,
|
91
|
+
report_builder=self._build_report,
|
92
|
+
)
|
93
|
+
|
94
|
+
@staticmethod
|
95
|
+
def render(
|
96
|
+
reports: Mapping[str, Report],
|
97
|
+
) -> str:
|
98
|
+
return render_openshift_cost_report(reports=reports)
|
99
|
+
|
100
|
+
@staticmethod
|
101
|
+
def _build_report(
|
102
|
+
app_name: str,
|
103
|
+
parent_app_name: str,
|
104
|
+
child_apps: list[str],
|
105
|
+
reports: Mapping[str, Report],
|
106
|
+
response: list[OpenShiftReportCostResponse],
|
107
|
+
) -> Report:
|
108
|
+
child_app_reports = [
|
109
|
+
ChildAppReport(
|
110
|
+
name=child_app,
|
111
|
+
total=reports[child_app].total,
|
112
|
+
)
|
113
|
+
for child_app in child_apps
|
114
|
+
]
|
115
|
+
child_apps_total = Decimal(
|
116
|
+
sum(child_app.total for child_app in child_app_reports)
|
117
|
+
)
|
118
|
+
|
119
|
+
items = [
|
120
|
+
ReportItem(
|
121
|
+
name=f"{value.clusters[0]}/{project.project}",
|
122
|
+
delta_value=value.delta_value,
|
123
|
+
delta_percent=value.delta_percent,
|
124
|
+
total=value.cost.total.value,
|
125
|
+
)
|
126
|
+
for r in response
|
127
|
+
for data in r.data
|
128
|
+
for project in data.projects
|
129
|
+
if len(project.values) == 1 and (value := project.values[0]) is not None
|
130
|
+
]
|
131
|
+
|
132
|
+
items_total = Decimal(sum(item.total for item in items))
|
133
|
+
items_delta_value = Decimal(sum(item.delta_value for item in items))
|
134
|
+
previous_items_total = items_total - items_delta_value
|
135
|
+
items_delta_percent = (
|
136
|
+
items_delta_value / previous_items_total * 100
|
137
|
+
if previous_items_total != 0
|
138
|
+
else None
|
139
|
+
)
|
140
|
+
total = items_total + child_apps_total
|
141
|
+
date = next(
|
142
|
+
(d for r in response for data in r.data if (d := data.date)),
|
143
|
+
"",
|
144
|
+
)
|
145
|
+
|
146
|
+
return Report(
|
147
|
+
app_name=app_name,
|
148
|
+
child_apps=child_app_reports,
|
149
|
+
child_apps_total=child_apps_total,
|
150
|
+
date=date,
|
151
|
+
parent_app_name=parent_app_name,
|
152
|
+
items_delta_value=items_delta_value,
|
153
|
+
items_delta_percent=items_delta_percent,
|
154
|
+
items_total=items_total,
|
155
|
+
total=total,
|
156
|
+
items=items,
|
157
|
+
)
|
158
|
+
|
159
|
+
@classmethod
|
160
|
+
def create(
|
161
|
+
cls,
|
162
|
+
) -> Self:
|
163
|
+
gql_api = gql.get_api()
|
164
|
+
vault_settings = get_app_interface_vault_settings(gql_api.query)
|
165
|
+
secret_reader = create_secret_reader(use_vault=vault_settings.vault)
|
166
|
+
cost_report_settings = get_cost_report_settings(gql_api)
|
167
|
+
secret = secret_reader.read_all_secret(cost_report_settings.credentials)
|
168
|
+
cost_management_api = CostManagementApi(
|
169
|
+
base_url=secret["api_base_url"],
|
170
|
+
token_url=secret["token_url"],
|
171
|
+
client_id=secret["client_id"],
|
172
|
+
client_secret=secret["client_secret"],
|
173
|
+
scope=secret["scope"].split(" "),
|
174
|
+
)
|
175
|
+
return cls(
|
176
|
+
gql_api=gql_api,
|
177
|
+
cost_management_api=cost_management_api,
|
178
|
+
thread_pool_size=THREAD_POOL_SIZE,
|
179
|
+
)
|
@@ -1,5 +1,4 @@
|
|
1
1
|
from decimal import Decimal
|
2
|
-
from typing import List
|
3
2
|
|
4
3
|
from pydantic import BaseModel
|
5
4
|
|
@@ -35,14 +34,36 @@ class ServiceCostValueResponse(BaseModel):
|
|
35
34
|
|
36
35
|
class ServiceCostResponse(BaseModel):
|
37
36
|
service: str
|
38
|
-
values:
|
37
|
+
values: list[ServiceCostValueResponse]
|
39
38
|
|
40
39
|
|
41
40
|
class CostResponse(BaseModel):
|
42
41
|
date: str
|
43
|
-
services:
|
42
|
+
services: list[ServiceCostResponse]
|
44
43
|
|
45
44
|
|
46
45
|
class ReportCostResponse(BaseModel):
|
47
46
|
meta: ReportMetaResponse
|
48
|
-
data:
|
47
|
+
data: list[CostResponse]
|
48
|
+
|
49
|
+
|
50
|
+
class ProjectCostValueResponse(BaseModel):
|
51
|
+
delta_value: Decimal
|
52
|
+
delta_percent: float | None
|
53
|
+
cost: CostTotalResponse
|
54
|
+
clusters: list[str]
|
55
|
+
|
56
|
+
|
57
|
+
class ProjectCostResponse(BaseModel):
|
58
|
+
project: str
|
59
|
+
values: list[ProjectCostValueResponse]
|
60
|
+
|
61
|
+
|
62
|
+
class OpenShiftCostResponse(BaseModel):
|
63
|
+
date: str
|
64
|
+
projects: list[ProjectCostResponse]
|
65
|
+
|
66
|
+
|
67
|
+
class OpenShiftReportCostResponse(BaseModel):
|
68
|
+
meta: ReportMetaResponse
|
69
|
+
data: list[OpenShiftCostResponse]
|
@@ -0,0 +1,59 @@
|
|
1
|
+
from collections import defaultdict
|
2
|
+
from collections.abc import Callable, Iterable, Mapping, MutableMapping
|
3
|
+
from typing import Any
|
4
|
+
|
5
|
+
from reconcile.typed_queries.cost_report.app_names import App
|
6
|
+
from tools.cli_commands.cost_report.model import Report
|
7
|
+
|
8
|
+
|
9
|
+
def dfs_build_reports(
|
10
|
+
app_name: str,
|
11
|
+
parent_app_name: str | None,
|
12
|
+
child_apps_by_parent: Mapping[str | None, list[str]],
|
13
|
+
responses: Mapping[str, Any],
|
14
|
+
reports: MutableMapping[str, Report],
|
15
|
+
report_builder: Callable[..., Report],
|
16
|
+
) -> None:
|
17
|
+
"""
|
18
|
+
Depth-first search to build the reports. Build leaf nodes first to ensure total is calculated correctly.
|
19
|
+
"""
|
20
|
+
child_apps = child_apps_by_parent.get(app_name, [])
|
21
|
+
for child_app in child_apps:
|
22
|
+
dfs_build_reports(
|
23
|
+
app_name=child_app,
|
24
|
+
parent_app_name=app_name,
|
25
|
+
child_apps_by_parent=child_apps_by_parent,
|
26
|
+
responses=responses,
|
27
|
+
reports=reports,
|
28
|
+
report_builder=report_builder,
|
29
|
+
)
|
30
|
+
reports[app_name] = report_builder(
|
31
|
+
app_name=app_name,
|
32
|
+
parent_app_name=parent_app_name,
|
33
|
+
child_apps=child_apps,
|
34
|
+
reports=reports,
|
35
|
+
response=responses[app_name],
|
36
|
+
)
|
37
|
+
|
38
|
+
|
39
|
+
def process_reports(
|
40
|
+
apps: Iterable[App],
|
41
|
+
responses: Mapping[str, Any],
|
42
|
+
report_builder: Callable[..., Report],
|
43
|
+
) -> dict[str, Report]:
|
44
|
+
child_apps_by_parent = defaultdict(list)
|
45
|
+
for app in apps:
|
46
|
+
child_apps_by_parent[app.parent_app_name].append(app.name)
|
47
|
+
|
48
|
+
reports: dict[str, Report] = {}
|
49
|
+
root_apps = child_apps_by_parent.get(None, [])
|
50
|
+
for app_name in root_apps:
|
51
|
+
dfs_build_reports(
|
52
|
+
app_name,
|
53
|
+
None,
|
54
|
+
child_apps_by_parent=child_apps_by_parent,
|
55
|
+
responses=responses,
|
56
|
+
reports=reports,
|
57
|
+
report_builder=report_builder,
|
58
|
+
)
|
59
|
+
return reports
|
@@ -1,4 +1,4 @@
|
|
1
|
-
from collections.abc import Mapping
|
1
|
+
from collections.abc import Callable, Mapping
|
2
2
|
from decimal import Decimal
|
3
3
|
from typing import Any
|
4
4
|
|
@@ -15,11 +15,15 @@ LAYOUT = """\
|
|
15
15
|
{cost_breakdown}\
|
16
16
|
"""
|
17
17
|
|
18
|
-
|
18
|
+
AWS_HEADER = """\
|
19
19
|
# AWS Cost Report
|
20
20
|
"""
|
21
21
|
|
22
|
-
|
22
|
+
OPENSHIFT_HEADER = """\
|
23
|
+
# OpenShift Cost Report
|
24
|
+
"""
|
25
|
+
|
26
|
+
AWS_SUMMARY = """\
|
23
27
|
## Summary
|
24
28
|
|
25
29
|
Total AWS Cost for {date}: {total_cost}
|
@@ -29,6 +33,16 @@ Total AWS Cost for {date}: {total_cost}
|
|
29
33
|
```
|
30
34
|
"""
|
31
35
|
|
36
|
+
OPENSHIFT_SUMMARY = """\
|
37
|
+
## Summary
|
38
|
+
|
39
|
+
Total OpenShift Cost for {date}: {total_cost}
|
40
|
+
|
41
|
+
```json:table
|
42
|
+
{json_table}
|
43
|
+
```
|
44
|
+
"""
|
45
|
+
|
32
46
|
MONTH_OVER_MONTH_CHANGE = """\
|
33
47
|
## Month Over Month Change
|
34
48
|
|
@@ -52,7 +66,7 @@ APP = """\
|
|
52
66
|
"""
|
53
67
|
|
54
68
|
AWS_SERVICES_COST = """\
|
55
|
-
AWS Services Cost: {
|
69
|
+
AWS Services Cost: {items_total}, {items_delta_value}{items_delta_percent} \
|
56
70
|
compared to previous month.
|
57
71
|
View in [Cost Management Console]({cost_management_console_url}).
|
58
72
|
|
@@ -61,6 +75,15 @@ View in [Cost Management Console]({cost_management_console_url}).
|
|
61
75
|
```
|
62
76
|
"""
|
63
77
|
|
78
|
+
OPENSHIFT_WORKLOADS_COST = """\
|
79
|
+
OpenShift Workloads Cost: {items_total}, {items_delta_value}{items_delta_percent} \
|
80
|
+
compared to previous month.
|
81
|
+
|
82
|
+
```json:table
|
83
|
+
{json_table}
|
84
|
+
```
|
85
|
+
"""
|
86
|
+
|
64
87
|
COST_MANAGEMENT_CONSOLE_EXPLORE_URL = (
|
65
88
|
"{base_url}/explorer?"
|
66
89
|
"dateRangeType=previous_month&"
|
@@ -100,7 +123,7 @@ class JsonTable(BaseModel):
|
|
100
123
|
class SummaryItem(BaseModel):
|
101
124
|
name: str
|
102
125
|
child_apps_total: Decimal
|
103
|
-
|
126
|
+
items_total: Decimal
|
104
127
|
total: Decimal
|
105
128
|
|
106
129
|
|
@@ -111,6 +134,18 @@ class MonthOverMonthChangeItem(BaseModel):
|
|
111
134
|
total: Decimal
|
112
135
|
|
113
136
|
|
137
|
+
class ViewReportItem(BaseModel):
|
138
|
+
name: str
|
139
|
+
delta_value: Decimal
|
140
|
+
delta_percent: float | None
|
141
|
+
total: Decimal
|
142
|
+
|
143
|
+
|
144
|
+
class ViewChildAppReport(BaseModel):
|
145
|
+
name: str
|
146
|
+
total: Decimal
|
147
|
+
|
148
|
+
|
114
149
|
def format_cost_value(value: Decimal) -> str:
|
115
150
|
return f"${value:,.2f}"
|
116
151
|
|
@@ -132,7 +167,10 @@ def get_date(reports: Mapping[str, Report]) -> str:
|
|
132
167
|
return next((d for report in reports.values() if (d := report.date)), "")
|
133
168
|
|
134
169
|
|
135
|
-
def render_summary(
|
170
|
+
def render_summary(
|
171
|
+
template: str,
|
172
|
+
reports: Mapping[str, Report],
|
173
|
+
) -> str:
|
136
174
|
root_apps = {
|
137
175
|
name: report
|
138
176
|
for name, report in reports.items()
|
@@ -143,7 +181,7 @@ def render_summary(reports: Mapping[str, Report]) -> str:
|
|
143
181
|
SummaryItem(
|
144
182
|
name=name,
|
145
183
|
child_apps_total=round(report.child_apps_total, 2),
|
146
|
-
|
184
|
+
items_total=round(report.items_total, 2),
|
147
185
|
total=round(report.total, 2),
|
148
186
|
)
|
149
187
|
for name, report in root_apps.items()
|
@@ -153,12 +191,12 @@ def render_summary(reports: Mapping[str, Report]) -> str:
|
|
153
191
|
items=sorted(summary_items, key=lambda item: item.total, reverse=True),
|
154
192
|
fields=[
|
155
193
|
TableField(key="name", label="Name", sortable=True),
|
156
|
-
TableField(key="
|
194
|
+
TableField(key="items_total", label="Self App ($)", sortable=True),
|
157
195
|
TableField(key="child_apps_total", label="Child Apps ($)", sortable=True),
|
158
196
|
TableField(key="total", label="Total ($)", sortable=True),
|
159
197
|
],
|
160
198
|
)
|
161
|
-
return
|
199
|
+
return template.format(
|
162
200
|
date=get_date(reports),
|
163
201
|
total_cost=format_cost_value(total_cost),
|
164
202
|
json_table=json_table.json(indent=2),
|
@@ -169,13 +207,13 @@ def render_month_over_month_change(reports: Mapping[str, Report]) -> str:
|
|
169
207
|
items = [
|
170
208
|
MonthOverMonthChangeItem(
|
171
209
|
name=name,
|
172
|
-
delta_value=round(report.
|
210
|
+
delta_value=round(report.items_delta_value, 2),
|
173
211
|
delta_percent=(
|
174
|
-
round(report.
|
175
|
-
if report.
|
212
|
+
round(report.items_delta_percent, 2)
|
213
|
+
if report.items_delta_percent is not None
|
176
214
|
else None
|
177
215
|
),
|
178
|
-
total=round(report.
|
216
|
+
total=round(report.items_total, 2),
|
179
217
|
)
|
180
218
|
for name, report in reports.items()
|
181
219
|
]
|
@@ -212,46 +250,60 @@ def render_aws_services_cost(
|
|
212
250
|
report: Report,
|
213
251
|
cost_management_console_base_url: str,
|
214
252
|
) -> str:
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
253
|
+
json_table = _build_items_cost_json_table(report, name_label="Service")
|
254
|
+
return AWS_SERVICES_COST.format(
|
255
|
+
cost_management_console_url=build_cost_management_console_url(
|
256
|
+
cost_management_console_base_url,
|
257
|
+
report.app_name,
|
258
|
+
),
|
259
|
+
items_total=format_cost_value(report.items_total),
|
260
|
+
items_delta_value=format_delta_value(report.items_delta_value),
|
261
|
+
items_delta_percent=format_delta_percent(report.items_delta_percent),
|
262
|
+
json_table=json_table.json(indent=2),
|
263
|
+
)
|
264
|
+
|
265
|
+
|
266
|
+
def render_openshift_workloads_cost(
|
267
|
+
report: Report,
|
268
|
+
) -> str:
|
269
|
+
json_table = _build_items_cost_json_table(report, name_label="Cluster/Namespace")
|
270
|
+
return OPENSHIFT_WORKLOADS_COST.format(
|
271
|
+
items_total=format_cost_value(report.items_total),
|
272
|
+
items_delta_value=format_delta_value(report.items_delta_value),
|
273
|
+
items_delta_percent=format_delta_percent(report.items_delta_percent),
|
274
|
+
json_table=json_table.json(indent=2),
|
275
|
+
)
|
276
|
+
|
277
|
+
|
278
|
+
def _build_items_cost_json_table(report: Report, name_label: str) -> JsonTable:
|
279
|
+
items = [
|
280
|
+
ViewReportItem(
|
281
|
+
name=s.name,
|
282
|
+
delta_value=round(s.delta_value, 2),
|
283
|
+
delta_percent=round(s.delta_percent, 2)
|
284
|
+
if s.delta_percent is not None
|
285
|
+
else None,
|
286
|
+
total=round(s.total, 2),
|
224
287
|
)
|
225
|
-
for s in report.
|
288
|
+
for s in report.items
|
226
289
|
]
|
227
|
-
|
290
|
+
return JsonTable(
|
228
291
|
filter=True,
|
229
|
-
items=sorted(
|
292
|
+
items=sorted(items, key=lambda item: item.total, reverse=True),
|
230
293
|
fields=[
|
231
|
-
TableField(key="
|
294
|
+
TableField(key="name", label=name_label, sortable=True),
|
232
295
|
TableField(key="delta_value", label="Change ($)", sortable=True),
|
233
296
|
TableField(key="delta_percent", label="Change (%)", sortable=True),
|
234
297
|
TableField(key="total", label="Total ($)", sortable=True),
|
235
298
|
],
|
236
299
|
)
|
237
|
-
return AWS_SERVICES_COST.format(
|
238
|
-
cost_management_console_url=build_cost_management_console_url(
|
239
|
-
cost_management_console_base_url,
|
240
|
-
report.app_name,
|
241
|
-
),
|
242
|
-
services_total=format_cost_value(report.services_total),
|
243
|
-
services_delta_value=format_delta_value(report.services_delta_value),
|
244
|
-
services_delta_percent=format_delta_percent(report.services_delta_percent),
|
245
|
-
json_table=json_table.json(indent=2),
|
246
|
-
)
|
247
300
|
|
248
301
|
|
249
302
|
def render_child_apps_cost(report: Report) -> str:
|
250
303
|
child_apps = [
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
}
|
304
|
+
ViewChildAppReport(
|
305
|
+
name=app.name,
|
306
|
+
total=round(app.total, 2),
|
255
307
|
)
|
256
308
|
for app in report.child_apps
|
257
309
|
]
|
@@ -278,16 +330,12 @@ def render_total_cost(report: Report) -> str:
|
|
278
330
|
def render_app_cost(
|
279
331
|
name: str,
|
280
332
|
report: Report,
|
281
|
-
|
333
|
+
item_cost_renderer: Callable[..., str],
|
334
|
+
**kwargs: Any,
|
282
335
|
) -> str:
|
283
336
|
cost_details_sections = []
|
284
|
-
if report.
|
285
|
-
cost_details_sections.append(
|
286
|
-
render_aws_services_cost(
|
287
|
-
report,
|
288
|
-
cost_management_console_base_url,
|
289
|
-
)
|
290
|
-
)
|
337
|
+
if report.items:
|
338
|
+
cost_details_sections.append(item_cost_renderer(report=report, **kwargs))
|
291
339
|
if report.child_apps:
|
292
340
|
cost_details_sections.append(render_child_apps_cost(report))
|
293
341
|
cost_details_sections.append(render_total_cost(report))
|
@@ -302,13 +350,15 @@ def render_app_cost(
|
|
302
350
|
|
303
351
|
def render_cost_breakdown(
|
304
352
|
reports: Mapping[str, Report],
|
305
|
-
|
353
|
+
item_cost_renderer: Callable[..., str],
|
354
|
+
**kwargs: Any,
|
306
355
|
) -> str:
|
307
356
|
apps = "\n".join(
|
308
357
|
render_app_cost(
|
309
|
-
name,
|
310
|
-
report,
|
311
|
-
|
358
|
+
name=name,
|
359
|
+
report=report,
|
360
|
+
item_cost_renderer=item_cost_renderer,
|
361
|
+
**kwargs,
|
312
362
|
)
|
313
363
|
for name, report in sorted(
|
314
364
|
reports.items(),
|
@@ -318,16 +368,31 @@ def render_cost_breakdown(
|
|
318
368
|
return COST_BREAKDOWN.format(apps=apps)
|
319
369
|
|
320
370
|
|
321
|
-
def
|
371
|
+
def render_aws_cost_report(
|
322
372
|
reports: Mapping[str, Report],
|
323
373
|
cost_management_console_base_url: str,
|
324
374
|
) -> str:
|
325
375
|
return LAYOUT.format(
|
326
|
-
header=
|
327
|
-
summary=render_summary(reports),
|
376
|
+
header=AWS_HEADER,
|
377
|
+
summary=render_summary(AWS_SUMMARY, reports),
|
328
378
|
month_over_month_change=render_month_over_month_change(reports),
|
329
379
|
cost_breakdown=render_cost_breakdown(
|
330
380
|
reports,
|
331
|
-
|
381
|
+
item_cost_renderer=render_aws_services_cost,
|
382
|
+
cost_management_console_base_url=cost_management_console_base_url,
|
383
|
+
),
|
384
|
+
)
|
385
|
+
|
386
|
+
|
387
|
+
def render_openshift_cost_report(
|
388
|
+
reports: Mapping[str, Report],
|
389
|
+
) -> str:
|
390
|
+
return LAYOUT.format(
|
391
|
+
header=OPENSHIFT_HEADER,
|
392
|
+
summary=render_summary(OPENSHIFT_SUMMARY, reports),
|
393
|
+
month_over_month_change=render_month_over_month_change(reports),
|
394
|
+
cost_breakdown=render_cost_breakdown(
|
395
|
+
reports,
|
396
|
+
item_cost_renderer=render_openshift_workloads_cost,
|
332
397
|
),
|
333
398
|
)
|