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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: qontract-reconcile
3
- Version: 0.10.1rc1149
3
+ Version: 0.10.1rc1151
4
4
  Summary: Collection of tools to reconcile services with their desired state as defined in the app-interface DB.
5
5
  Home-page: https://github.com/app-sre/qontract-reconcile
6
6
  Author: Red Hat App-SRE Team
@@ -280,7 +280,7 @@ reconcile/gql_definitions/common/state_aws_account.py,sha256=LAdpCG2-ykVpWBPO0Zu
280
280
  reconcile/gql_definitions/common/users.py,sha256=ahY3d185LbTekCGYBLJwZJljn54RJI_P5CVefdqyoZA,1705
281
281
  reconcile/gql_definitions/cost_report/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
282
282
  reconcile/gql_definitions/cost_report/app_names.py,sha256=fzqYXyiTSll359J1F1o7qapco0MSxgs3sr_Ssb2Kbns,1786
283
- reconcile/gql_definitions/cost_report/cost_namespaces.py,sha256=wV76J8Ynq0ObSfjSZQRLmCPUZ96frGc82Z5HPM_e4F4,2219
283
+ reconcile/gql_definitions/cost_report/cost_namespaces.py,sha256=URRozAgSa9OnkqOCZf3MGH21_wcnsqYl0n-olXdjQH0,2286
284
284
  reconcile/gql_definitions/cost_report/settings.py,sha256=0nhBDJ5MZ1m7XkNDGrRLmsnUbzqZ4WRh_DDEEzKhcxU,2153
285
285
  reconcile/gql_definitions/dashdotdb_slo/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
286
286
  reconcile/gql_definitions/dashdotdb_slo/slo_documents_query.py,sha256=MYYpVOc8Ze9w7k6-tlUkp5OaPG_5bqHPS5FhfWTw00U,4335
@@ -646,7 +646,7 @@ reconcile/typed_queries/app_interface_metrics_exporter/__init__.py,sha256=47DEQp
646
646
  reconcile/typed_queries/app_interface_metrics_exporter/onboarding_status.py,sha256=X-N1WJGOL6OR9940P0_K4-YJzkL5Vg4favhYrBxXD9A,327
647
647
  reconcile/typed_queries/cost_report/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
648
648
  reconcile/typed_queries/cost_report/app_names.py,sha256=HMEMIqAbMyVQfoQ5YXTXE4xDt7FaXBRz0QIHnsIZC1c,478
649
- reconcile/typed_queries/cost_report/cost_namespaces.py,sha256=VXIqdGE5lwa5z4UTROEsuc3QdxgodRgXglei0LJI2Co,985
649
+ reconcile/typed_queries/cost_report/cost_namespaces.py,sha256=1GUjWXQj7U2djVHBPYcd8Cy2-enKXf0-GaplLi8JZw4,1178
650
650
  reconcile/typed_queries/cost_report/settings.py,sha256=xbTMMUQnbub2pav4B-ctzzRe7ijjTv2bqfqdtb9OnO0,589
651
651
  reconcile/typed_queries/terraform_tgw_attachments/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
652
652
  reconcile/typed_queries/terraform_tgw_attachments/aws_accounts.py,sha256=2idUmpavfwDu0RmH3xTVlvJAhj2ImhREoO8WL8EX920,378
@@ -838,7 +838,7 @@ tools/app_interface_metrics_exporter.py,sha256=zkwkxdAUAxjdc-pzx2_oJXG25fo0Fnyd5
838
838
  tools/app_interface_reporter.py,sha256=oZPib4HPq0aZ2Zui1QGJGk6qQdfpeihujGDBnSdKyGE,17627
839
839
  tools/glitchtip_access_reporter.py,sha256=oPBnk_YoDuljU3v0FaChzOwwnk4vap1xEE67QEjzdqs,2948
840
840
  tools/glitchtip_access_revalidation.py,sha256=8kbBJk04mkq28kWoRDDkfCGIF3GRg3pJrFAh1sW0dbk,2821
841
- tools/qontract_cli.py,sha256=OPfTSTyzhWrE1-s0Dl2L57aJ3soYpvAKKQmRlQnc6l0,139114
841
+ tools/qontract_cli.py,sha256=ndRUc8mjkubajPkZKaoE2IFVT2e4YXYRVtUaeLt3zzE,140496
842
842
  tools/sd_app_sre_alert_report.py,sha256=e9vAdyenUz2f5c8-z-5WY0wv-SJ9aePKDH2r4IwB6pc,5063
843
843
  tools/template_validation.py,sha256=qpKYaTgk0GOPGa2Ct5_5sKdwIHtCAKIBGzsMPuJU5fw,3371
844
844
  tools/cli_commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -846,13 +846,22 @@ tools/cli_commands/erv2.py,sha256=vHYBYkTaS3h2qEStuAE6iThCt54LD2o3-0bJLcYODKY,16
846
846
  tools/cli_commands/gpg_encrypt.py,sha256=x02JOMn834z89YSNvr5B-oJky7rR1C0begCkPh45eHk,4958
847
847
  tools/cli_commands/systems_and_tools.py,sha256=EMHOF1AtUDaoSk0bbjl6oUKYAz4rTZjIBaF-6E6GspM,16816
848
848
  tools/cli_commands/cost_report/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
849
- tools/cli_commands/cost_report/aws.py,sha256=bdvbT3bDymBOMx4Ve4kt154MeGiGusTSpOpMMFrQ4SM,5191
850
- tools/cli_commands/cost_report/cost_management_api.py,sha256=tFzjaZ3RSVJeAUsfSWXHiQlMVbV8dFcXibK158ODQcA,2549
851
- tools/cli_commands/cost_report/model.py,sha256=KGkro82zoFXLmfQ4nv7dkv4QoNtOF-k6PT6VURCLZKE,548
852
- tools/cli_commands/cost_report/openshift.py,sha256=XNEJpgtIQAi3Eoej1Btnl74IWg3g-gLh6W36WOwQ9bk,6188
853
- tools/cli_commands/cost_report/response.py,sha256=_kbpBSjMjbPXGkjDgidTOLvFpVqfBo3VMkSOheUdmMA,1308
854
- tools/cli_commands/cost_report/util.py,sha256=r4K8nC1S0YZNSUNro141cYG1nuG8HwkkYEqWV9GCvu8,1861
855
- tools/cli_commands/cost_report/view.py,sha256=pKM6LDeWZJcTw2a7sWBwKSuR9p3SAk3lEb37uwMhlLw,10183
849
+ tools/cli_commands/cost_report/aws.py,sha256=QKpzQ12Ysyo8CaKq-RNc4K434qDXqFAjzb0gXWnrT34,4521
850
+ tools/cli_commands/cost_report/cost_management_api.py,sha256=UZVD1P755gRqKSln5gn1vBiwi9BAhpx5mQuMKEDfI3o,4671
851
+ tools/cli_commands/cost_report/model.py,sha256=7W6mi8p_hRn8z5YyRCEoDionGHM3RXL7ItDWqQXaCMg,1082
852
+ tools/cli_commands/cost_report/openshift.py,sha256=5qad9bG8EmNNrw5kTmFBuRk1pApn9xiYboQ6aK6LYq0,5503
853
+ tools/cli_commands/cost_report/openshift_cost_optimization.py,sha256=lpdqnhQGXaPuL19IxchvFvM8DPz0xB6vg_fXJAopkGQ,6711
854
+ tools/cli_commands/cost_report/response.py,sha256=l5fVNMg8zzoNWm8LquYJsWM3VqxABBG8KT_qN0L3P6c,2689
855
+ tools/cli_commands/cost_report/util.py,sha256=lKSWjjUfDzMfSSTbFqc4Ln8JfzlWiW8kR2i51VJbEnc,2497
856
+ tools/cli_commands/cost_report/view.py,sha256=2uNO_fWIUydEL2kQXV959crRkHqzjp9c1OrefXNiJ5s,14033
857
+ tools/cli_commands/test/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
858
+ tools/cli_commands/test/conftest.py,sha256=ta2eRWhajOdMFMt3AMmh6fvLuM3lBwU9kmkXAaD2_vs,17011
859
+ tools/cli_commands/test/test_aws_cost_report.py,sha256=YNH_6NXLkWMkrgti4v8HDT2uwveh_ss3GdppjBOXq6I,6993
860
+ tools/cli_commands/test/test_cost_management_api.py,sha256=g4LCf3KE-KL3DjZLGY7sOcT5Ds_cgDRMqDKgitjvVhI,9607
861
+ tools/cli_commands/test/test_gpg_encrypt.py,sha256=BIOPMsvytRb_j-Ae7KylcOCUrbNNVw8-WDpvV1FlmDs,6867
862
+ tools/cli_commands/test/test_openshift_cost_optimization_report.py,sha256=GmfsqzfBkU1yE3o4tBVW7Od09M19EOK4gfX5ExjRiGM,8132
863
+ tools/cli_commands/test/test_openshift_cost_report.py,sha256=bK9vI5bCBsHyyTNxpVRZ6AxV1JP_5uHqZXITqOJ5GTM,8724
864
+ tools/cli_commands/test/test_util.py,sha256=51AeUTzS4D_gZqdlq5WDbqWzP5Eet6vAL-DW1Ktcti4,2213
856
865
  tools/saas_metrics_exporter/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
857
866
  tools/saas_metrics_exporter/main.py,sha256=piocx6meMdJxoxeNz52gQGUjt5n7Fma4kgqYamszPrM,3180
858
867
  tools/saas_metrics_exporter/commit_distance/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -867,12 +876,12 @@ tools/test/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
867
876
  tools/test/conftest.py,sha256=CsDbu4otrxb7X7kXKKGyV3ZEzu3pCkgjCoCGiHNx6zc,2401
868
877
  tools/test/test_app_interface_metrics_exporter.py,sha256=SX7qL3D1SIRKFo95FoQztvftCWEEf-g1mfXOtgCog-g,1271
869
878
  tools/test/test_erv2.py,sha256=EAS7QuJkHisRVO9bMGxm662L5B6i66wF_mT9PAjVzrU,3128
870
- tools/test/test_qontract_cli.py,sha256=_D61RFGAN5x44CY1tYbouhlGXXABwYfxKSWSQx3Jrss,4941
879
+ tools/test/test_qontract_cli.py,sha256=iuzKbQ6ahinvjoQmQLBrG4shey0z-1rB6qCgS8T6dgU,5789
871
880
  tools/test/test_saas_promotion_state.py,sha256=dy4kkSSAQ7bC0Xp2CociETGN-2aABEfL6FU5D9Jl00Y,6056
872
881
  tools/test/test_sd_app_sre_alert_report.py,sha256=v363r9zM7__0kR5K6mvJoGFcM9BvE33fWAayrqkpojA,2116
873
882
  tools/test/test_sre_checkpoints.py,sha256=SKqPPTl9ua0RFdSSofnoQX-JZE6dFLO3LRhfQzqtfh8,2607
874
- qontract_reconcile-0.10.1rc1149.dist-info/METADATA,sha256=Nm_UnTQqjHtOQmjpgR8izLobnzDXO1UJxvKd0sLsy5s,2213
875
- qontract_reconcile-0.10.1rc1149.dist-info/WHEEL,sha256=eOLhNAGa2EW3wWl_TU484h7q1UNgy0JXjjoqKoxAAQc,92
876
- qontract_reconcile-0.10.1rc1149.dist-info/entry_points.txt,sha256=GKQqCl2j2X1BJQ69een6rHcR26PmnxnONLNOQB-nRjY,491
877
- qontract_reconcile-0.10.1rc1149.dist-info/top_level.txt,sha256=l5ISPoXzt0SdR4jVdkfa7RPSKNc8zAHYWAnR-Dw8Ey8,24
878
- qontract_reconcile-0.10.1rc1149.dist-info/RECORD,,
883
+ qontract_reconcile-0.10.1rc1151.dist-info/METADATA,sha256=kIbyRbeEHOgr_be8cLvc2kZ_5psU6e_Hltp8zQvbkMA,2213
884
+ qontract_reconcile-0.10.1rc1151.dist-info/WHEEL,sha256=eOLhNAGa2EW3wWl_TU484h7q1UNgy0JXjjoqKoxAAQc,92
885
+ qontract_reconcile-0.10.1rc1151.dist-info/entry_points.txt,sha256=GKQqCl2j2X1BJQ69een6rHcR26PmnxnONLNOQB-nRjY,491
886
+ qontract_reconcile-0.10.1rc1151.dist-info/top_level.txt,sha256=l5ISPoXzt0SdR4jVdkfa7RPSKNc8zAHYWAnR-Dw8Ey8,24
887
+ qontract_reconcile-0.10.1rc1151.dist-info/RECORD,,
@@ -22,6 +22,7 @@ DEFINITION = """
22
22
  query CostNamespaces($filter: JSON) {
23
23
  namespaces: namespaces_v1(filter: $filter) {
24
24
  name
25
+ labels
25
26
  app {
26
27
  name
27
28
  }
@@ -57,6 +58,7 @@ class ClusterV1(ConfiguredBaseModel):
57
58
 
58
59
  class NamespaceV1(ConfiguredBaseModel):
59
60
  name: str = Field(..., alias="name")
61
+ labels: Optional[Json] = Field(..., alias="labels")
60
62
  app: AppV1 = Field(..., alias="app")
61
63
  cluster: ClusterV1 = Field(..., alias="cluster")
62
64
 
@@ -4,15 +4,17 @@ from reconcile.gql_definitions.cost_report.cost_namespaces import query
4
4
  from reconcile.utils.gql import GqlApi
5
5
 
6
6
 
7
- class CostNamespace(BaseModel):
7
+ class CostNamespaceLabels(BaseModel, frozen=True):
8
+ insights_cost_management_optimizations: str | None = None
9
+
10
+
11
+ class CostNamespace(BaseModel, frozen=True):
8
12
  name: str
13
+ labels: CostNamespaceLabels
9
14
  app_name: str
10
15
  cluster_name: str
11
16
  cluster_external_id: str | None
12
17
 
13
- class Config:
14
- frozen = True
15
-
16
18
 
17
19
  def get_cost_namespaces(
18
20
  gql_api: GqlApi,
@@ -30,6 +32,7 @@ def get_cost_namespaces(
30
32
  return [
31
33
  CostNamespace(
32
34
  name=namespace.name,
35
+ labels=CostNamespaceLabels.parse_obj(namespace.labels or {}),
33
36
  app_name=namespace.app.name,
34
37
  cluster_name=namespace.cluster.name,
35
38
  cluster_external_id=namespace.cluster.spec.external_id
@@ -4,17 +4,15 @@ from typing import Self
4
4
 
5
5
  from sretoolbox.utils import threaded
6
6
 
7
- from reconcile.typed_queries.app_interface_vault_settings import (
8
- get_app_interface_vault_settings,
9
- )
10
7
  from reconcile.typed_queries.cost_report.app_names import App, get_app_names
11
- from reconcile.typed_queries.cost_report.settings import get_cost_report_settings
12
8
  from reconcile.utils import gql
13
- from reconcile.utils.secret_reader import create_secret_reader
14
9
  from tools.cli_commands.cost_report.cost_management_api import CostManagementApi
15
10
  from tools.cli_commands.cost_report.model import ChildAppReport, Report, ReportItem
16
- from tools.cli_commands.cost_report.response import ReportCostResponse
17
- from tools.cli_commands.cost_report.util import process_reports
11
+ from tools.cli_commands.cost_report.response import AwsReportCostResponse
12
+ from tools.cli_commands.cost_report.util import (
13
+ fetch_cost_report_secret,
14
+ process_reports,
15
+ )
18
16
  from tools.cli_commands.cost_report.view import render_aws_cost_report
19
17
 
20
18
  THREAD_POOL_SIZE = 10
@@ -45,13 +43,13 @@ class AwsCostReportCommand:
45
43
  """
46
44
  return get_app_names(self.gql_api)
47
45
 
48
- def _get_report(self, app: App) -> tuple[str, ReportCostResponse]:
46
+ def _get_report(self, app: App) -> tuple[str, AwsReportCostResponse]:
49
47
  return app.name, self.cost_management_api.get_aws_costs_report(app.name)
50
48
 
51
49
  def get_reports(
52
50
  self,
53
51
  apps: Iterable[App],
54
- ) -> Mapping[str, ReportCostResponse]:
52
+ ) -> Mapping[str, AwsReportCostResponse]:
55
53
  """
56
54
  Fetch reports from cost management API
57
55
  """
@@ -61,7 +59,7 @@ class AwsCostReportCommand:
61
59
  def process_reports(
62
60
  self,
63
61
  apps: Iterable[App],
64
- responses: Mapping[str, ReportCostResponse],
62
+ responses: Mapping[str, AwsReportCostResponse],
65
63
  ) -> dict[str, Report]:
66
64
  """
67
65
  Build reports with parent-child app tree.
@@ -84,7 +82,7 @@ class AwsCostReportCommand:
84
82
  parent_app_name: str | None,
85
83
  child_apps: list[str],
86
84
  reports: Mapping[str, Report],
87
- response: ReportCostResponse,
85
+ response: AwsReportCostResponse,
88
86
  ) -> Report:
89
87
  child_app_reports = [
90
88
  ChildAppReport(
@@ -127,21 +125,10 @@ class AwsCostReportCommand:
127
125
  )
128
126
 
129
127
  @classmethod
130
- def create(
131
- cls,
132
- ) -> Self:
128
+ def create(cls) -> Self:
133
129
  gql_api = gql.get_api()
134
- vault_settings = get_app_interface_vault_settings(gql_api.query)
135
- secret_reader = create_secret_reader(use_vault=vault_settings.vault)
136
- cost_report_settings = get_cost_report_settings(gql_api)
137
- secret = secret_reader.read_all_secret(cost_report_settings.credentials)
138
- cost_management_api = CostManagementApi(
139
- base_url=secret["api_base_url"],
140
- token_url=secret["token_url"],
141
- client_id=secret["client_id"],
142
- client_secret=secret["client_secret"],
143
- scope=secret["scope"].split(" "),
144
- )
130
+ secret = fetch_cost_report_secret(gql_api)
131
+ cost_management_api = CostManagementApi.create_from_secret(secret)
145
132
  return cls(
146
133
  gql_api=gql_api,
147
134
  cost_management_api=cost_management_api,
@@ -1,3 +1,8 @@
1
+ from collections.abc import Mapping
2
+ from typing import Self
3
+ from urllib.parse import urljoin, urlparse
4
+
5
+ from requests import Response
1
6
  from urllib3.util import Retry
2
7
 
3
8
  from reconcile.utils.oauth2_backend_application_session import (
@@ -5,14 +10,24 @@ from reconcile.utils.oauth2_backend_application_session import (
5
10
  )
6
11
  from reconcile.utils.rest_api_base import ApiBase
7
12
  from tools.cli_commands.cost_report.response import (
13
+ AwsReportCostResponse,
14
+ OpenShiftCostOptimizationReportResponse,
8
15
  OpenShiftReportCostResponse,
9
- ReportCostResponse,
10
16
  )
11
17
 
12
18
  REQUEST_TIMEOUT = 60
19
+ PAGE_LIMIT = 100
20
+ MEMORY_UNIT = "MiB"
21
+ CPU_UNIT = "millicores"
13
22
 
14
23
 
15
24
  class CostManagementApi(ApiBase):
25
+ """
26
+ Cost Management API client.
27
+
28
+ Doc at https://console.redhat.com/docs/api/cost-management
29
+ """
30
+
16
31
  def __init__(
17
32
  self,
18
33
  base_url: str,
@@ -32,14 +47,17 @@ class CostManagementApi(ApiBase):
32
47
  backoff_factor=15, # large backoff required for server-side processing
33
48
  status_forcelist=[500, 502, 503, 504],
34
49
  )
50
+ self.base_url = base_url
51
+ parsed_url = urlparse(base_url)
52
+ host = f"{parsed_url.scheme}://{parsed_url.netloc}/"
35
53
  super().__init__(
36
- host=base_url,
54
+ host=host,
37
55
  session=session,
38
56
  read_timeout=REQUEST_TIMEOUT,
39
57
  max_retries=max_retries,
40
58
  )
41
59
 
42
- def get_aws_costs_report(self, app: str) -> ReportCostResponse:
60
+ def get_aws_costs_report(self, app: str) -> AwsReportCostResponse:
43
61
  params = {
44
62
  "cost_type": "calculated_amortized_cost",
45
63
  "delta": "cost",
@@ -51,12 +69,12 @@ class CostManagementApi(ApiBase):
51
69
  }
52
70
  response = self.session.request(
53
71
  method="GET",
54
- url=f"{self.host}/reports/aws/costs/",
72
+ url=f"{self.base_url}/reports/aws/costs/",
55
73
  params=params,
56
74
  timeout=self.read_timeout,
57
75
  )
58
76
  response.raise_for_status()
59
- return ReportCostResponse.parse_obj(response.json())
77
+ return AwsReportCostResponse.parse_obj(response.json())
60
78
 
61
79
  def get_openshift_costs_report(
62
80
  self,
@@ -74,9 +92,64 @@ class CostManagementApi(ApiBase):
74
92
  }
75
93
  response = self.session.request(
76
94
  method="GET",
77
- url=f"{self.host}/reports/openshift/costs/",
95
+ url=f"{self.base_url}/reports/openshift/costs/",
78
96
  params=params,
79
97
  timeout=self.read_timeout,
80
98
  )
81
99
  response.raise_for_status()
82
100
  return OpenShiftReportCostResponse.parse_obj(response.json())
101
+
102
+ def get_openshift_cost_optimization_report(
103
+ self,
104
+ cluster: str,
105
+ project: str,
106
+ ) -> OpenShiftCostOptimizationReportResponse:
107
+ params = {
108
+ "cluster": cluster,
109
+ "project": project,
110
+ "limit": str(PAGE_LIMIT),
111
+ "memory-unit": MEMORY_UNIT,
112
+ "cpu-unit": CPU_UNIT,
113
+ }
114
+ response = self.session.request(
115
+ method="GET",
116
+ url=f"{self.base_url}/recommendations/openshift",
117
+ params=params,
118
+ timeout=self.read_timeout,
119
+ )
120
+ response.raise_for_status()
121
+
122
+ data = self._get_paginated(response)
123
+ return OpenShiftCostOptimizationReportResponse.parse_obj(data)
124
+
125
+ def _get_paginated(
126
+ self,
127
+ response: Response,
128
+ ) -> dict[str, list]:
129
+ body = response.json()
130
+ data = body.get("data", [])
131
+
132
+ while next_url := body.get("links", {}).get("next"):
133
+ r = self.session.request(
134
+ method="GET",
135
+ url=urljoin(self.host, next_url),
136
+ timeout=self.read_timeout,
137
+ )
138
+ r.raise_for_status()
139
+ body = r.json()
140
+ data.extend(body.get("data", []))
141
+
142
+ return {"data": data}
143
+
144
+ @classmethod
145
+ def create_from_secret(
146
+ cls,
147
+ secret: Mapping[str, str],
148
+ ) -> Self:
149
+ return cls(
150
+ base_url=secret["api_base_url"],
151
+ token_url=secret["token_url"],
152
+ client_id=secret["client_id"],
153
+ client_secret=secret["client_secret"],
154
+ scope=secret["scope"].split(" "),
155
+ )
@@ -26,3 +26,24 @@ class Report(BaseModel):
26
26
  items_delta_value: Decimal
27
27
  items_total: Decimal
28
28
  total: Decimal
29
+
30
+
31
+ class OptimizationReportItem(BaseModel):
32
+ cluster: str
33
+ project: str
34
+ workload: str
35
+ workload_type: str
36
+ container: str
37
+ current_cpu_limit: str | None
38
+ current_cpu_request: str | None
39
+ current_memory_limit: str | None
40
+ current_memory_request: str | None
41
+ recommend_cpu_limit: str | None
42
+ recommend_cpu_request: str | None
43
+ recommend_memory_limit: str | None
44
+ recommend_memory_request: str | None
45
+
46
+
47
+ class OptimizationReport(BaseModel):
48
+ app_name: str
49
+ items: list[OptimizationReportItem]
@@ -5,21 +5,19 @@ from typing import Self
5
5
 
6
6
  from sretoolbox.utils import threaded
7
7
 
8
- from reconcile.typed_queries.app_interface_vault_settings import (
9
- get_app_interface_vault_settings,
10
- )
11
8
  from reconcile.typed_queries.cost_report.app_names import App, get_app_names
12
9
  from reconcile.typed_queries.cost_report.cost_namespaces import (
13
10
  CostNamespace,
14
11
  get_cost_namespaces,
15
12
  )
16
- from reconcile.typed_queries.cost_report.settings import get_cost_report_settings
17
13
  from reconcile.utils import gql
18
- from reconcile.utils.secret_reader import create_secret_reader
19
14
  from tools.cli_commands.cost_report.cost_management_api import CostManagementApi
20
15
  from tools.cli_commands.cost_report.model import ChildAppReport, Report, ReportItem
21
16
  from tools.cli_commands.cost_report.response import OpenShiftReportCostResponse
22
- from tools.cli_commands.cost_report.util import process_reports
17
+ from tools.cli_commands.cost_report.util import (
18
+ fetch_cost_report_secret,
19
+ process_reports,
20
+ )
23
21
  from tools.cli_commands.cost_report.view import render_openshift_cost_report
24
22
 
25
23
  THREAD_POOL_SIZE = 10
@@ -157,21 +155,10 @@ class OpenShiftCostReportCommand:
157
155
  )
158
156
 
159
157
  @classmethod
160
- def create(
161
- cls,
162
- ) -> Self:
158
+ def create(cls) -> Self:
163
159
  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
- )
160
+ secret = fetch_cost_report_secret(gql_api)
161
+ cost_management_api = CostManagementApi.create_from_secret(secret)
175
162
  return cls(
176
163
  gql_api=gql_api,
177
164
  cost_management_api=cost_management_api,
@@ -0,0 +1,187 @@
1
+ from collections import defaultdict
2
+ from collections.abc import Iterable, Mapping
3
+ from typing import Self
4
+
5
+ from sretoolbox.utils import threaded
6
+
7
+ from reconcile.typed_queries.cost_report.app_names import App, get_app_names
8
+ from reconcile.typed_queries.cost_report.cost_namespaces import (
9
+ CostNamespace,
10
+ get_cost_namespaces,
11
+ )
12
+ from reconcile.utils import gql
13
+ from tools.cli_commands.cost_report.cost_management_api import CostManagementApi
14
+ from tools.cli_commands.cost_report.model import (
15
+ OptimizationReport,
16
+ OptimizationReportItem,
17
+ )
18
+ from tools.cli_commands.cost_report.response import (
19
+ OpenShiftCostOptimizationReportResponse,
20
+ OpenShiftCostOptimizationResponse,
21
+ ResourceConfigResponse,
22
+ )
23
+ from tools.cli_commands.cost_report.util import fetch_cost_report_secret
24
+ from tools.cli_commands.cost_report.view import (
25
+ render_openshift_cost_optimization_report,
26
+ )
27
+
28
+ THREAD_POOL_SIZE = 10
29
+
30
+
31
+ class OpenShiftCostOptimizationReportCommand:
32
+ def __init__(
33
+ self,
34
+ gql_api: gql.GqlApi,
35
+ cost_management_api: CostManagementApi,
36
+ thread_pool_size: int = THREAD_POOL_SIZE,
37
+ ) -> None:
38
+ self.gql_api = gql_api
39
+ self.cost_management_api = cost_management_api
40
+ self.thread_pool_size = thread_pool_size
41
+
42
+ def execute(self) -> str:
43
+ apps = self.get_apps()
44
+ cost_namespaces = self.get_cost_namespaces()
45
+ responses = self.get_reports(cost_namespaces)
46
+ reports = self.process_reports(apps, responses)
47
+ return self.render(reports)
48
+
49
+ def get_apps(self) -> list[App]:
50
+ return get_app_names(self.gql_api)
51
+
52
+ def get_cost_namespaces(self) -> list[CostNamespace]:
53
+ cost_namespaces = get_cost_namespaces(self.gql_api)
54
+ return [
55
+ n
56
+ for n in cost_namespaces
57
+ if n.labels.insights_cost_management_optimizations == "true"
58
+ ]
59
+
60
+ def get_reports(
61
+ self,
62
+ cost_namespaces: Iterable[CostNamespace],
63
+ ) -> dict[CostNamespace, OpenShiftCostOptimizationReportResponse]:
64
+ results = threaded.run(self._get_report, cost_namespaces, self.thread_pool_size)
65
+ return dict(results)
66
+
67
+ def process_reports(
68
+ self,
69
+ apps: Iterable[App],
70
+ responses: Mapping[CostNamespace, OpenShiftCostOptimizationReportResponse],
71
+ ) -> list[OptimizationReport]:
72
+ app_responses = defaultdict(list)
73
+ for cost_namespace, response in responses.items():
74
+ app_responses[cost_namespace.app_name].append(response)
75
+ return [
76
+ self._build_report(
77
+ app.name,
78
+ app_responses.get(app.name, []),
79
+ )
80
+ for app in apps
81
+ ]
82
+
83
+ def _get_report(
84
+ self,
85
+ cost_namespace: CostNamespace,
86
+ ) -> tuple[CostNamespace, OpenShiftCostOptimizationReportResponse]:
87
+ cluster = (
88
+ cost_namespace.cluster_external_id
89
+ if cost_namespace.cluster_external_id is not None
90
+ else cost_namespace.cluster_name
91
+ )
92
+ response = self.cost_management_api.get_openshift_cost_optimization_report(
93
+ project=cost_namespace.name,
94
+ cluster=cluster,
95
+ )
96
+ response.data = [
97
+ data
98
+ for data in response.data
99
+ if self._match_cost_namespace(data, cost_namespace)
100
+ ]
101
+ return cost_namespace, response
102
+
103
+ @staticmethod
104
+ def _match_cost_namespace(
105
+ response: OpenShiftCostOptimizationResponse,
106
+ cost_namespace: CostNamespace,
107
+ ) -> bool:
108
+ """
109
+ Exactly match the cost namespace from the response data.
110
+ Cost Management API returns fuzzy match on fields.
111
+ Client side filter is needed.
112
+
113
+ :param response: OpenShiftCostOptimizationResponse
114
+ :param cost_namespace: CostNamespace
115
+ :return: exactly match or not
116
+ """
117
+ if cluster_uuid := cost_namespace.cluster_external_id:
118
+ if response.cluster_uuid != cluster_uuid:
119
+ return False
120
+ elif response.cluster_alias != cost_namespace.cluster_name:
121
+ return False
122
+ return response.project == cost_namespace.name
123
+
124
+ @staticmethod
125
+ def render(
126
+ reports: Iterable[OptimizationReport],
127
+ ) -> str:
128
+ return render_openshift_cost_optimization_report(reports)
129
+
130
+ def _build_report(
131
+ self,
132
+ app_name: str,
133
+ responses: list[OpenShiftCostOptimizationReportResponse],
134
+ ) -> OptimizationReport:
135
+ return OptimizationReport(
136
+ app_name=app_name,
137
+ items=[self._build_report_item(data) for r in responses for data in r.data],
138
+ )
139
+
140
+ def _build_report_item(
141
+ self,
142
+ data: OpenShiftCostOptimizationResponse,
143
+ ) -> OptimizationReportItem:
144
+ current = data.recommendations.current
145
+ terms = data.recommendations.recommendation_terms
146
+ recommend = next(
147
+ engine.cost.config
148
+ for t in [terms.long_term, terms.medium_term, terms.short_term]
149
+ if (engine := t.recommendation_engines) is not None
150
+ )
151
+
152
+ return OptimizationReportItem(
153
+ cluster=data.cluster_alias,
154
+ project=data.project,
155
+ workload=data.workload,
156
+ workload_type=data.workload_type,
157
+ container=data.container,
158
+ current_cpu_limit=self._build_resource_config(current.limits.cpu),
159
+ current_cpu_request=self._build_resource_config(current.requests.cpu),
160
+ current_memory_limit=self._build_resource_config(current.limits.memory),
161
+ current_memory_request=self._build_resource_config(current.requests.memory),
162
+ recommend_cpu_limit=self._build_resource_config(recommend.limits.cpu),
163
+ recommend_cpu_request=self._build_resource_config(recommend.requests.cpu),
164
+ recommend_memory_limit=self._build_resource_config(recommend.limits.memory),
165
+ recommend_memory_request=self._build_resource_config(
166
+ recommend.requests.memory
167
+ ),
168
+ )
169
+
170
+ @staticmethod
171
+ def _build_resource_config(response: ResourceConfigResponse) -> str | None:
172
+ if response.amount is None:
173
+ return None
174
+ if response.format is None:
175
+ return str(round(response.amount))
176
+ return f"{round(response.amount)}{response.format}"
177
+
178
+ @classmethod
179
+ def create(cls) -> Self:
180
+ gql_api = gql.get_api()
181
+ secret = fetch_cost_report_secret(gql_api)
182
+ cost_management_api = CostManagementApi.create_from_secret(secret)
183
+ return cls(
184
+ gql_api=gql_api,
185
+ cost_management_api=cost_management_api,
186
+ thread_pool_size=THREAD_POOL_SIZE,
187
+ )
@@ -42,7 +42,7 @@ class CostResponse(BaseModel):
42
42
  services: list[ServiceCostResponse]
43
43
 
44
44
 
45
- class ReportCostResponse(BaseModel):
45
+ class AwsReportCostResponse(BaseModel):
46
46
  meta: ReportMetaResponse
47
47
  data: list[CostResponse]
48
48
 
@@ -67,3 +67,58 @@ class OpenShiftCostResponse(BaseModel):
67
67
  class OpenShiftReportCostResponse(BaseModel):
68
68
  meta: ReportMetaResponse
69
69
  data: list[OpenShiftCostResponse]
70
+
71
+
72
+ class ResourceConfigResponse(BaseModel):
73
+ amount: float | None = None
74
+ format: str | None = None
75
+
76
+
77
+ class ResourceResponse(BaseModel):
78
+ cpu: ResourceConfigResponse
79
+ memory: ResourceConfigResponse
80
+
81
+
82
+ class RecommendationResourcesResponse(BaseModel):
83
+ limits: ResourceResponse
84
+ requests: ResourceResponse
85
+
86
+
87
+ class RecommendationEngineResponse(BaseModel):
88
+ config: RecommendationResourcesResponse
89
+ variation: RecommendationResourcesResponse
90
+
91
+
92
+ class RecommendationEnginesResponse(BaseModel):
93
+ cost: RecommendationEngineResponse
94
+ performance: RecommendationEngineResponse
95
+
96
+
97
+ class RecommendationTermResponse(BaseModel):
98
+ recommendation_engines: RecommendationEnginesResponse | None = None
99
+
100
+
101
+ class RecommendationTermsResponse(BaseModel):
102
+ long_term: RecommendationTermResponse
103
+ medium_term: RecommendationTermResponse
104
+ short_term: RecommendationTermResponse
105
+
106
+
107
+ class RecommendationsResponse(BaseModel):
108
+ current: RecommendationResourcesResponse
109
+ recommendation_terms: RecommendationTermsResponse
110
+
111
+
112
+ class OpenShiftCostOptimizationResponse(BaseModel):
113
+ cluster_alias: str
114
+ cluster_uuid: str
115
+ container: str
116
+ id: str
117
+ project: str
118
+ recommendations: RecommendationsResponse
119
+ workload: str
120
+ workload_type: str
121
+
122
+
123
+ class OpenShiftCostOptimizationReportResponse(BaseModel):
124
+ data: list[OpenShiftCostOptimizationResponse]