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.
Files changed (25) hide show
  1. {qontract_reconcile-0.10.1rc801.dist-info → qontract_reconcile-0.10.1rc803.dist-info}/METADATA +1 -1
  2. {qontract_reconcile-0.10.1rc801.dist-info → qontract_reconcile-0.10.1rc803.dist-info}/RECORD +25 -12
  3. reconcile/gql_definitions/cost_report/cost_namespaces.py +84 -0
  4. reconcile/statuspage/__init__.py +0 -0
  5. reconcile/statuspage/atlassian.py +425 -0
  6. reconcile/statuspage/integration.py +22 -0
  7. reconcile/statuspage/integrations/__init__.py +0 -0
  8. reconcile/statuspage/integrations/components.py +77 -0
  9. reconcile/statuspage/integrations/maintenances.py +73 -0
  10. reconcile/statuspage/page.py +114 -0
  11. reconcile/statuspage/state.py +47 -0
  12. reconcile/statuspage/status.py +97 -0
  13. reconcile/typed_queries/cost_report/cost_namespaces.py +40 -0
  14. tools/cli_commands/cost_report/{command.py → aws.py} +53 -75
  15. tools/cli_commands/cost_report/cost_management_api.py +28 -4
  16. tools/cli_commands/cost_report/model.py +7 -8
  17. tools/cli_commands/cost_report/openshift.py +179 -0
  18. tools/cli_commands/cost_report/response.py +25 -4
  19. tools/cli_commands/cost_report/util.py +59 -0
  20. tools/cli_commands/cost_report/view.py +121 -56
  21. tools/qontract_cli.py +10 -2
  22. tools/test/test_qontract_cli.py +32 -6
  23. {qontract_reconcile-0.10.1rc801.dist-info → qontract_reconcile-0.10.1rc803.dist-info}/WHEEL +0 -0
  24. {qontract_reconcile-0.10.1rc801.dist-info → qontract_reconcile-0.10.1rc803.dist-info}/entry_points.txt +0 -0
  25. {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 ReportCostResponse
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: List[str] | None = None,
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 ServiceReport(BaseModel):
8
- service: str
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: List[ChildAppReport]
20
+ child_apps: list[ChildAppReport]
22
21
  child_apps_total: Decimal
23
22
  date: str
24
23
  parent_app_name: str | None
25
- services: List[ServiceReport]
26
- services_delta_percent: float | None
27
- services_delta_value: Decimal
28
- services_total: Decimal
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: List[ServiceCostValueResponse]
37
+ values: list[ServiceCostValueResponse]
39
38
 
40
39
 
41
40
  class CostResponse(BaseModel):
42
41
  date: str
43
- services: List[ServiceCostResponse]
42
+ services: list[ServiceCostResponse]
44
43
 
45
44
 
46
45
  class ReportCostResponse(BaseModel):
47
46
  meta: ReportMetaResponse
48
- data: List[CostResponse]
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
- HEADER = """\
18
+ AWS_HEADER = """\
19
19
  # AWS Cost Report
20
20
  """
21
21
 
22
- SUMMARY = """\
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: {services_total}, {services_delta_value}{services_delta_percent} \
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
- services_total: Decimal
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(reports: Mapping[str, Report]) -> str:
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
- services_total=round(report.services_total, 2),
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="services_total", label="Self App ($)", sortable=True),
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 SUMMARY.format(
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.services_delta_value, 2),
210
+ delta_value=round(report.items_delta_value, 2),
173
211
  delta_percent=(
174
- round(report.services_delta_percent, 2)
175
- if report.services_delta_percent is not None
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.services_total, 2),
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
- services = [
216
- s.copy(
217
- update={
218
- "delta_value": round(s.delta_value, 2),
219
- "delta_percent": round(s.delta_percent, 2)
220
- if s.delta_percent is not None
221
- else None,
222
- "total": round(s.total, 2),
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.services
288
+ for s in report.items
226
289
  ]
227
- json_table = JsonTable(
290
+ return JsonTable(
228
291
  filter=True,
229
- items=sorted(services, key=lambda service: service.total, reverse=True),
292
+ items=sorted(items, key=lambda item: item.total, reverse=True),
230
293
  fields=[
231
- TableField(key="service", label="Service", sortable=True),
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
- app.copy(
252
- update={
253
- "total": round(app.total, 2),
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
- cost_management_console_base_url: str,
333
+ item_cost_renderer: Callable[..., str],
334
+ **kwargs: Any,
282
335
  ) -> str:
283
336
  cost_details_sections = []
284
- if report.services:
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
- cost_management_console_base_url: str,
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
- cost_management_console_base_url,
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 render_report(
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=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
- cost_management_console_base_url,
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
  )