qontract-reconcile 0.10.1rc1150__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.
- {qontract_reconcile-0.10.1rc1150.dist-info → qontract_reconcile-0.10.1rc1151.dist-info}/METADATA +1 -1
- {qontract_reconcile-0.10.1rc1150.dist-info → qontract_reconcile-0.10.1rc1151.dist-info}/RECORD +25 -16
- reconcile/gql_definitions/cost_report/cost_namespaces.py +2 -0
- reconcile/typed_queries/cost_report/cost_namespaces.py +7 -4
- tools/cli_commands/cost_report/aws.py +12 -25
- tools/cli_commands/cost_report/cost_management_api.py +79 -6
- tools/cli_commands/cost_report/model.py +21 -0
- tools/cli_commands/cost_report/openshift.py +7 -20
- tools/cli_commands/cost_report/openshift_cost_optimization.py +187 -0
- tools/cli_commands/cost_report/response.py +56 -1
- tools/cli_commands/cost_report/util.py +13 -0
- tools/cli_commands/cost_report/view.py +128 -2
- tools/cli_commands/test/__init__.py +0 -0
- tools/cli_commands/test/conftest.py +332 -0
- tools/cli_commands/test/test_aws_cost_report.py +258 -0
- tools/cli_commands/test/test_cost_management_api.py +326 -0
- tools/cli_commands/test/test_gpg_encrypt.py +235 -0
- tools/cli_commands/test/test_openshift_cost_optimization_report.py +255 -0
- tools/cli_commands/test/test_openshift_cost_report.py +295 -0
- tools/cli_commands/test/test_util.py +70 -0
- tools/qontract_cli.py +10 -0
- tools/test/test_qontract_cli.py +24 -0
- {qontract_reconcile-0.10.1rc1150.dist-info → qontract_reconcile-0.10.1rc1151.dist-info}/WHEEL +0 -0
- {qontract_reconcile-0.10.1rc1150.dist-info → qontract_reconcile-0.10.1rc1151.dist-info}/entry_points.txt +0 -0
- {qontract_reconcile-0.10.1rc1150.dist-info → qontract_reconcile-0.10.1rc1151.dist-info}/top_level.txt +0 -0
{qontract_reconcile-0.10.1rc1150.dist-info → qontract_reconcile-0.10.1rc1151.dist-info}/METADATA
RENAMED
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: qontract-reconcile
|
3
|
-
Version: 0.10.
|
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
|
{qontract_reconcile-0.10.1rc1150.dist-info → qontract_reconcile-0.10.1rc1151.dist-info}/RECORD
RENAMED
@@ -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=
|
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=
|
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=
|
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=
|
850
|
-
tools/cli_commands/cost_report/cost_management_api.py,sha256=
|
851
|
-
tools/cli_commands/cost_report/model.py,sha256=
|
852
|
-
tools/cli_commands/cost_report/openshift.py,sha256=
|
853
|
-
tools/cli_commands/cost_report/
|
854
|
-
tools/cli_commands/cost_report/
|
855
|
-
tools/cli_commands/cost_report/
|
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=
|
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.
|
875
|
-
qontract_reconcile-0.10.
|
876
|
-
qontract_reconcile-0.10.
|
877
|
-
qontract_reconcile-0.10.
|
878
|
-
qontract_reconcile-0.10.
|
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
|
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
|
17
|
-
from tools.cli_commands.cost_report.util import
|
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,
|
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,
|
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,
|
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:
|
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
|
-
|
135
|
-
|
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=
|
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) ->
|
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.
|
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
|
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.
|
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
|
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
|
-
|
165
|
-
|
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
|
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]
|