qontract-reconcile 0.10.1rc1149__py3-none-any.whl → 0.10.1rc1151__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.1rc1149.dist-info → qontract_reconcile-0.10.1rc1151.dist-info}/METADATA +1 -1
  2. {qontract_reconcile-0.10.1rc1149.dist-info → qontract_reconcile-0.10.1rc1151.dist-info}/RECORD +25 -16
  3. reconcile/gql_definitions/cost_report/cost_namespaces.py +2 -0
  4. reconcile/typed_queries/cost_report/cost_namespaces.py +7 -4
  5. tools/cli_commands/cost_report/aws.py +12 -25
  6. tools/cli_commands/cost_report/cost_management_api.py +79 -6
  7. tools/cli_commands/cost_report/model.py +21 -0
  8. tools/cli_commands/cost_report/openshift.py +7 -20
  9. tools/cli_commands/cost_report/openshift_cost_optimization.py +187 -0
  10. tools/cli_commands/cost_report/response.py +56 -1
  11. tools/cli_commands/cost_report/util.py +13 -0
  12. tools/cli_commands/cost_report/view.py +128 -2
  13. tools/cli_commands/test/__init__.py +0 -0
  14. tools/cli_commands/test/conftest.py +332 -0
  15. tools/cli_commands/test/test_aws_cost_report.py +258 -0
  16. tools/cli_commands/test/test_cost_management_api.py +326 -0
  17. tools/cli_commands/test/test_gpg_encrypt.py +235 -0
  18. tools/cli_commands/test/test_openshift_cost_optimization_report.py +255 -0
  19. tools/cli_commands/test/test_openshift_cost_report.py +295 -0
  20. tools/cli_commands/test/test_util.py +70 -0
  21. tools/qontract_cli.py +67 -24
  22. tools/test/test_qontract_cli.py +24 -0
  23. {qontract_reconcile-0.10.1rc1149.dist-info → qontract_reconcile-0.10.1rc1151.dist-info}/WHEEL +0 -0
  24. {qontract_reconcile-0.10.1rc1149.dist-info → qontract_reconcile-0.10.1rc1151.dist-info}/entry_points.txt +0 -0
  25. {qontract_reconcile-0.10.1rc1149.dist-info → qontract_reconcile-0.10.1rc1151.dist-info}/top_level.txt +0 -0
@@ -2,7 +2,13 @@ from collections import defaultdict
2
2
  from collections.abc import Callable, Iterable, Mapping, MutableMapping
3
3
  from typing import Any
4
4
 
5
+ from reconcile.typed_queries.app_interface_vault_settings import (
6
+ get_app_interface_vault_settings,
7
+ )
5
8
  from reconcile.typed_queries.cost_report.app_names import App
9
+ from reconcile.typed_queries.cost_report.settings import get_cost_report_settings
10
+ from reconcile.utils.gql import GqlApi
11
+ from reconcile.utils.secret_reader import create_secret_reader
6
12
  from tools.cli_commands.cost_report.model import Report
7
13
 
8
14
 
@@ -57,3 +63,10 @@ def process_reports(
57
63
  report_builder=report_builder,
58
64
  )
59
65
  return reports
66
+
67
+
68
+ def fetch_cost_report_secret(gql_api: GqlApi) -> dict[str, str]:
69
+ vault_settings = get_app_interface_vault_settings(gql_api.query)
70
+ secret_reader = create_secret_reader(use_vault=vault_settings.vault)
71
+ cost_report_settings = get_cost_report_settings(gql_api)
72
+ return secret_reader.read_all_secret(cost_report_settings.credentials)
@@ -1,10 +1,10 @@
1
- from collections.abc import Callable, Mapping
1
+ from collections.abc import Callable, Iterable, Mapping
2
2
  from decimal import Decimal
3
3
  from typing import Any
4
4
 
5
5
  from pydantic import BaseModel
6
6
 
7
- from tools.cli_commands.cost_report.model import Report
7
+ from tools.cli_commands.cost_report.model import OptimizationReport, Report
8
8
 
9
9
  LAYOUT = """\
10
10
  [TOC]
@@ -15,6 +15,13 @@ LAYOUT = """\
15
15
  {cost_breakdown}\
16
16
  """
17
17
 
18
+ OPTIMIZATION_LAYOUT = """\
19
+ [TOC]
20
+
21
+ {header}
22
+ {optimizations}\
23
+ """
24
+
18
25
  AWS_HEADER = """\
19
26
  # AWS Cost Report
20
27
  """
@@ -23,6 +30,19 @@ OPENSHIFT_HEADER = """\
23
30
  # OpenShift Cost Report
24
31
  """
25
32
 
33
+ OPTIMIZATION_HEADER = """\
34
+ # OpenShift Cost Optimization Report
35
+
36
+ Report is generated for optimization enabled namespaces
37
+ (`insights_cost_management_optimizations='true'` in namespace `labels`).
38
+
39
+ In Cost optimizations, recommendations get generated when
40
+ CPU usage is at or above the 60th percentile and
41
+ memory usage is at the 100th percentile.
42
+
43
+ View details in [Cost Management Optimizations](https://console.redhat.com/openshift/cost-management/optimizations).
44
+ """
45
+
26
46
  AWS_SUMMARY = """\
27
47
  ## Summary
28
48
 
@@ -107,6 +127,14 @@ TOTAL_COST = """\
107
127
  Total Cost: {total}
108
128
  """
109
129
 
130
+ OPTIMIZATION = """\
131
+ ## {app_name}
132
+
133
+ ```json:table
134
+ {json_table}
135
+ ```
136
+ """
137
+
110
138
 
111
139
  class TableField(BaseModel):
112
140
  key: str
@@ -146,6 +174,19 @@ class ViewChildAppReport(BaseModel):
146
174
  total: Decimal
147
175
 
148
176
 
177
+ class ViewOptimizationReportItem(BaseModel):
178
+ namespace: str
179
+ workload: str
180
+ current_cpu_limit: str | None
181
+ current_cpu_request: str | None
182
+ current_memory_limit: str | None
183
+ current_memory_request: str | None
184
+ recommend_cpu_limit: str | None
185
+ recommend_cpu_request: str | None
186
+ recommend_memory_limit: str | None
187
+ recommend_memory_request: str | None
188
+
189
+
149
190
  def format_cost_value(value: Decimal) -> str:
150
191
  return f"${value:,.2f}"
151
192
 
@@ -396,3 +437,88 @@ def render_openshift_cost_report(
396
437
  item_cost_renderer=render_openshift_workloads_cost,
397
438
  ),
398
439
  )
440
+
441
+
442
+ def render_optimization(
443
+ report: OptimizationReport,
444
+ ) -> str:
445
+ items = [
446
+ ViewOptimizationReportItem(
447
+ namespace=f"{i.cluster}/{i.project}",
448
+ workload=f"{i.workload_type}/{i.workload}/{i.container}",
449
+ current_cpu_limit=i.current_cpu_limit,
450
+ current_cpu_request=i.current_cpu_request,
451
+ current_memory_limit=i.current_memory_limit,
452
+ current_memory_request=i.current_memory_request,
453
+ recommend_cpu_limit=i.recommend_cpu_limit,
454
+ recommend_cpu_request=i.recommend_cpu_request,
455
+ recommend_memory_limit=i.recommend_memory_limit,
456
+ recommend_memory_request=i.recommend_memory_request,
457
+ )
458
+ for i in report.items
459
+ ]
460
+ json_table = JsonTable(
461
+ filter=True,
462
+ items=sorted(items, key=lambda item: (item.namespace, item.workload)),
463
+ fields=[
464
+ TableField(key="namespace", label="Namespace", sortable=True),
465
+ TableField(key="workload", label="Workload", sortable=True),
466
+ TableField(
467
+ key="current_cpu_request", label="Current CPU Request", sortable=True
468
+ ),
469
+ TableField(
470
+ key="recommend_cpu_request",
471
+ label="Recommend CPU Request",
472
+ sortable=True,
473
+ ),
474
+ TableField(
475
+ key="current_cpu_limit", label="Current CPU Limit", sortable=True
476
+ ),
477
+ TableField(
478
+ key="recommend_cpu_limit", label="Recommend CPU Limit", sortable=True
479
+ ),
480
+ TableField(
481
+ key="current_memory_request",
482
+ label="Current Memory Request",
483
+ sortable=True,
484
+ ),
485
+ TableField(
486
+ key="recommend_memory_request",
487
+ label="Recommend Memory Request",
488
+ sortable=True,
489
+ ),
490
+ TableField(
491
+ key="current_memory_limit", label="Current Memory Limit", sortable=True
492
+ ),
493
+ TableField(
494
+ key="recommend_memory_limit",
495
+ label="Recommend Memory Limit",
496
+ sortable=True,
497
+ ),
498
+ ],
499
+ )
500
+ return OPTIMIZATION.format(
501
+ app_name=report.app_name,
502
+ json_table=json_table.json(indent=2),
503
+ )
504
+
505
+
506
+ def render_optimizations(
507
+ reports: Iterable[OptimizationReport],
508
+ ) -> str:
509
+ return "\n".join(
510
+ render_optimization(report=report)
511
+ for report in sorted(
512
+ reports,
513
+ key=lambda item: item.app_name.lower(),
514
+ )
515
+ )
516
+
517
+
518
+ def render_openshift_cost_optimization_report(
519
+ reports: Iterable[OptimizationReport],
520
+ ) -> str:
521
+ return OPTIMIZATION_LAYOUT.format(
522
+ header=OPTIMIZATION_HEADER,
523
+ optimizations=render_optimizations(reports),
524
+ )
File without changes
@@ -0,0 +1,332 @@
1
+ from collections.abc import Callable
2
+ from pathlib import Path
3
+
4
+ import pytest
5
+
6
+ from tools.cli_commands.cost_report.response import (
7
+ OpenShiftCostOptimizationReportResponse,
8
+ OpenShiftCostOptimizationResponse,
9
+ RecommendationEngineResponse,
10
+ RecommendationEnginesResponse,
11
+ RecommendationResourcesResponse,
12
+ RecommendationsResponse,
13
+ RecommendationTermResponse,
14
+ RecommendationTermsResponse,
15
+ ResourceConfigResponse,
16
+ ResourceResponse,
17
+ )
18
+
19
+
20
+ @pytest.fixture
21
+ def fx() -> Callable:
22
+ def _fx(name: str) -> str:
23
+ return (Path(__file__).parent / "fixtures" / name).read_text()
24
+
25
+ return _fx
26
+
27
+
28
+ COST_MANAGEMENT_CONSOLE_BASE_URL = (
29
+ "https://console.redhat.com/openshift/cost-management"
30
+ )
31
+
32
+ COST_MANAGEMENT_API_HOST = "https://host/"
33
+
34
+ COST_REPORT_SECRET = {
35
+ "api_base_url": f"{COST_MANAGEMENT_API_HOST}/v1",
36
+ "token_url": "token_url",
37
+ "client_id": "client_id",
38
+ "client_secret": "client_secret",
39
+ "scope": "scope",
40
+ "console_base_url": COST_MANAGEMENT_CONSOLE_BASE_URL,
41
+ }
42
+
43
+ OPENSHIFT_COST_OPTIMIZATION_RESPONSE = OpenShiftCostOptimizationReportResponse(
44
+ data=[
45
+ OpenShiftCostOptimizationResponse(
46
+ cluster_alias="some-cluster",
47
+ cluster_uuid="some-cluster-uuid",
48
+ container="test",
49
+ id="id-uuid",
50
+ project="some-project",
51
+ workload="test-deployment",
52
+ workload_type="deployment",
53
+ recommendations=RecommendationsResponse(
54
+ current=RecommendationResourcesResponse(
55
+ limits=ResourceResponse(
56
+ cpu=ResourceConfigResponse(amount=4),
57
+ memory=ResourceConfigResponse(amount=5, format="Gi"),
58
+ ),
59
+ requests=ResourceResponse(
60
+ cpu=ResourceConfigResponse(amount=1),
61
+ memory=ResourceConfigResponse(amount=400, format="Mi"),
62
+ ),
63
+ ),
64
+ recommendation_terms=RecommendationTermsResponse(
65
+ long_term=RecommendationTermResponse(),
66
+ medium_term=RecommendationTermResponse(),
67
+ short_term=RecommendationTermResponse(
68
+ recommendation_engines=RecommendationEnginesResponse(
69
+ cost=RecommendationEngineResponse(
70
+ config=RecommendationResourcesResponse(
71
+ limits=ResourceResponse(
72
+ cpu=ResourceConfigResponse(amount=5),
73
+ memory=ResourceConfigResponse(
74
+ amount=6, format="Gi"
75
+ ),
76
+ ),
77
+ requests=ResourceResponse(
78
+ cpu=ResourceConfigResponse(amount=3),
79
+ memory=ResourceConfigResponse(
80
+ amount=700, format="Mi"
81
+ ),
82
+ ),
83
+ ),
84
+ variation=RecommendationResourcesResponse(
85
+ limits=ResourceResponse(
86
+ cpu=ResourceConfigResponse(amount=1),
87
+ memory=ResourceConfigResponse(
88
+ amount=1, format="Gi"
89
+ ),
90
+ ),
91
+ requests=ResourceResponse(
92
+ cpu=ResourceConfigResponse(amount=2),
93
+ memory=ResourceConfigResponse(
94
+ amount=300, format="Mi"
95
+ ),
96
+ ),
97
+ ),
98
+ ),
99
+ performance=RecommendationEngineResponse(
100
+ config=RecommendationResourcesResponse(
101
+ limits=ResourceResponse(
102
+ cpu=ResourceConfigResponse(amount=3),
103
+ memory=ResourceConfigResponse(
104
+ amount=6, format="Gi"
105
+ ),
106
+ ),
107
+ requests=ResourceResponse(
108
+ cpu=ResourceConfigResponse(
109
+ amount=600, format="millicores"
110
+ ),
111
+ memory=ResourceConfigResponse(
112
+ amount=700, format="Mi"
113
+ ),
114
+ ),
115
+ ),
116
+ variation=RecommendationResourcesResponse(
117
+ limits=ResourceResponse(
118
+ cpu=ResourceConfigResponse(amount=-1),
119
+ memory=ResourceConfigResponse(
120
+ amount=1, format="Gi"
121
+ ),
122
+ ),
123
+ requests=ResourceResponse(
124
+ cpu=ResourceConfigResponse(
125
+ amount=-400, format="millicores"
126
+ ),
127
+ memory=ResourceConfigResponse(
128
+ amount=300, format="Mi"
129
+ ),
130
+ ),
131
+ ),
132
+ ),
133
+ )
134
+ ),
135
+ ),
136
+ ),
137
+ )
138
+ ]
139
+ )
140
+
141
+ OPENSHIFT_COST_OPTIMIZATION_WITH_FUZZY_MATCH_RESPONSE = (
142
+ OpenShiftCostOptimizationReportResponse(
143
+ data=[
144
+ OpenShiftCostOptimizationResponse(
145
+ cluster_alias="some-cluster",
146
+ cluster_uuid="some-cluster-uuid",
147
+ container="test",
148
+ id="id-uuid",
149
+ project="some-project",
150
+ workload="test-deployment",
151
+ workload_type="deployment",
152
+ recommendations=RecommendationsResponse(
153
+ current=RecommendationResourcesResponse(
154
+ limits=ResourceResponse(
155
+ cpu=ResourceConfigResponse(amount=4),
156
+ memory=ResourceConfigResponse(amount=5, format="Gi"),
157
+ ),
158
+ requests=ResourceResponse(
159
+ cpu=ResourceConfigResponse(amount=1),
160
+ memory=ResourceConfigResponse(amount=400, format="Mi"),
161
+ ),
162
+ ),
163
+ recommendation_terms=RecommendationTermsResponse(
164
+ long_term=RecommendationTermResponse(),
165
+ medium_term=RecommendationTermResponse(),
166
+ short_term=RecommendationTermResponse(
167
+ recommendation_engines=RecommendationEnginesResponse(
168
+ cost=RecommendationEngineResponse(
169
+ config=RecommendationResourcesResponse(
170
+ limits=ResourceResponse(
171
+ cpu=ResourceConfigResponse(amount=5),
172
+ memory=ResourceConfigResponse(
173
+ amount=6, format="Gi"
174
+ ),
175
+ ),
176
+ requests=ResourceResponse(
177
+ cpu=ResourceConfigResponse(amount=3),
178
+ memory=ResourceConfigResponse(
179
+ amount=700, format="Mi"
180
+ ),
181
+ ),
182
+ ),
183
+ variation=RecommendationResourcesResponse(
184
+ limits=ResourceResponse(
185
+ cpu=ResourceConfigResponse(amount=1),
186
+ memory=ResourceConfigResponse(
187
+ amount=1, format="Gi"
188
+ ),
189
+ ),
190
+ requests=ResourceResponse(
191
+ cpu=ResourceConfigResponse(amount=2),
192
+ memory=ResourceConfigResponse(
193
+ amount=300, format="Mi"
194
+ ),
195
+ ),
196
+ ),
197
+ ),
198
+ performance=RecommendationEngineResponse(
199
+ config=RecommendationResourcesResponse(
200
+ limits=ResourceResponse(
201
+ cpu=ResourceConfigResponse(amount=3),
202
+ memory=ResourceConfigResponse(
203
+ amount=6, format="Gi"
204
+ ),
205
+ ),
206
+ requests=ResourceResponse(
207
+ cpu=ResourceConfigResponse(
208
+ amount=600, format="millicores"
209
+ ),
210
+ memory=ResourceConfigResponse(
211
+ amount=700, format="Mi"
212
+ ),
213
+ ),
214
+ ),
215
+ variation=RecommendationResourcesResponse(
216
+ limits=ResourceResponse(
217
+ cpu=ResourceConfigResponse(amount=-1),
218
+ memory=ResourceConfigResponse(
219
+ amount=1, format="Gi"
220
+ ),
221
+ ),
222
+ requests=ResourceResponse(
223
+ cpu=ResourceConfigResponse(
224
+ amount=-400, format="millicores"
225
+ ),
226
+ memory=ResourceConfigResponse(
227
+ amount=300, format="Mi"
228
+ ),
229
+ ),
230
+ ),
231
+ ),
232
+ )
233
+ ),
234
+ ),
235
+ ),
236
+ ),
237
+ OpenShiftCostOptimizationResponse(
238
+ cluster_alias="some-cluster2",
239
+ cluster_uuid="some-cluster-uuid2",
240
+ container="test",
241
+ id="id-uuid",
242
+ project="some-project2",
243
+ workload="test-deployment",
244
+ workload_type="deployment",
245
+ recommendations=RecommendationsResponse(
246
+ current=RecommendationResourcesResponse(
247
+ limits=ResourceResponse(
248
+ cpu=ResourceConfigResponse(amount=4),
249
+ memory=ResourceConfigResponse(amount=5, format="Gi"),
250
+ ),
251
+ requests=ResourceResponse(
252
+ cpu=ResourceConfigResponse(amount=1),
253
+ memory=ResourceConfigResponse(amount=400, format="Mi"),
254
+ ),
255
+ ),
256
+ recommendation_terms=RecommendationTermsResponse(
257
+ long_term=RecommendationTermResponse(),
258
+ medium_term=RecommendationTermResponse(),
259
+ short_term=RecommendationTermResponse(
260
+ recommendation_engines=RecommendationEnginesResponse(
261
+ cost=RecommendationEngineResponse(
262
+ config=RecommendationResourcesResponse(
263
+ limits=ResourceResponse(
264
+ cpu=ResourceConfigResponse(amount=5),
265
+ memory=ResourceConfigResponse(
266
+ amount=6, format="Gi"
267
+ ),
268
+ ),
269
+ requests=ResourceResponse(
270
+ cpu=ResourceConfigResponse(amount=3),
271
+ memory=ResourceConfigResponse(
272
+ amount=700, format="Mi"
273
+ ),
274
+ ),
275
+ ),
276
+ variation=RecommendationResourcesResponse(
277
+ limits=ResourceResponse(
278
+ cpu=ResourceConfigResponse(amount=1),
279
+ memory=ResourceConfigResponse(
280
+ amount=1, format="Gi"
281
+ ),
282
+ ),
283
+ requests=ResourceResponse(
284
+ cpu=ResourceConfigResponse(amount=2),
285
+ memory=ResourceConfigResponse(
286
+ amount=300, format="Mi"
287
+ ),
288
+ ),
289
+ ),
290
+ ),
291
+ performance=RecommendationEngineResponse(
292
+ config=RecommendationResourcesResponse(
293
+ limits=ResourceResponse(
294
+ cpu=ResourceConfigResponse(amount=3),
295
+ memory=ResourceConfigResponse(
296
+ amount=6, format="Gi"
297
+ ),
298
+ ),
299
+ requests=ResourceResponse(
300
+ cpu=ResourceConfigResponse(
301
+ amount=600, format="millicores"
302
+ ),
303
+ memory=ResourceConfigResponse(
304
+ amount=700, format="Mi"
305
+ ),
306
+ ),
307
+ ),
308
+ variation=RecommendationResourcesResponse(
309
+ limits=ResourceResponse(
310
+ cpu=ResourceConfigResponse(amount=-1),
311
+ memory=ResourceConfigResponse(
312
+ amount=1, format="Gi"
313
+ ),
314
+ ),
315
+ requests=ResourceResponse(
316
+ cpu=ResourceConfigResponse(
317
+ amount=-400, format="millicores"
318
+ ),
319
+ memory=ResourceConfigResponse(
320
+ amount=300, format="Mi"
321
+ ),
322
+ ),
323
+ ),
324
+ ),
325
+ )
326
+ ),
327
+ ),
328
+ ),
329
+ ),
330
+ ]
331
+ )
332
+ )