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
@@ -0,0 +1,235 @@
1
+ import json
2
+ from collections.abc import Mapping
3
+ from unittest.mock import (
4
+ MagicMock,
5
+ mock_open,
6
+ patch,
7
+ )
8
+
9
+ import pytest
10
+
11
+ from reconcile.queries import UserFilter
12
+ from reconcile.utils.secret_reader import SecretReader
13
+ from tools.cli_commands.gpg_encrypt import (
14
+ ArgumentException,
15
+ GPGEncryptCommand,
16
+ GPGEncryptCommandData,
17
+ UserException,
18
+ )
19
+
20
+
21
+ def craft_command(command_data: GPGEncryptCommandData, secret: Mapping[str, str]):
22
+ secret_reader = MagicMock(spec=SecretReader)
23
+ secret_reader.read_all = MagicMock()
24
+ secret_reader.read_all.side_effect = [secret]
25
+ command = GPGEncryptCommand.create(
26
+ command_data=command_data,
27
+ secret_reader=secret_reader,
28
+ )
29
+ return command
30
+
31
+
32
+ @patch("reconcile.utils.gpg.gpg_encrypt")
33
+ @patch("reconcile.queries.get_users_by")
34
+ def test_gpg_encrypt_from_vault(get_users_by_mock, gpg_encrypt_mock):
35
+ vault_secret_path = "app-sre/test"
36
+ target_user = "testuser"
37
+ gpg_key = "xyz"
38
+ secret = {"x": "y"}
39
+ user_query = {
40
+ "org_username": target_user,
41
+ "public_gpg_key": gpg_key,
42
+ }
43
+ command = craft_command(
44
+ command_data=GPGEncryptCommandData(
45
+ vault_secret_path=vault_secret_path,
46
+ target_user=target_user,
47
+ ),
48
+ secret=secret,
49
+ )
50
+ secret_reader_mock = command._secret_reader.read_all
51
+ get_users_by_mock.side_effect = [[user_query]]
52
+ gpg_encrypt_mock.side_effect = ["encrypted_content"]
53
+
54
+ command.execute()
55
+
56
+ secret_reader_mock.assert_called_once_with({"path": vault_secret_path})
57
+ get_users_by_mock.assert_called_once_with(
58
+ refs=False,
59
+ filter=UserFilter(
60
+ org_username=target_user,
61
+ ),
62
+ )
63
+ gpg_encrypt_mock.assert_called_once_with(
64
+ content=json.dumps(secret, sort_keys=True, indent=4),
65
+ public_gpg_key=gpg_key,
66
+ )
67
+
68
+
69
+ @patch("reconcile.utils.gpg.gpg_encrypt")
70
+ @patch("reconcile.queries.get_users_by")
71
+ def test_gpg_encrypt_from_vault_with_version(get_users_by_mock, gpg_encrypt_mock):
72
+ vault_secret_path = "app-sre/test"
73
+ target_user = "testuser"
74
+ gpg_key = "xyz"
75
+ version = 4
76
+ secret = {"x": "y"}
77
+ user_query = {
78
+ "org_username": target_user,
79
+ "public_gpg_key": gpg_key,
80
+ }
81
+ command = craft_command(
82
+ command_data=GPGEncryptCommandData(
83
+ vault_secret_path=vault_secret_path,
84
+ vault_secret_version=version,
85
+ target_user=target_user,
86
+ ),
87
+ secret=secret,
88
+ )
89
+ secret_reader_mock = command._secret_reader.read_all
90
+ get_users_by_mock.side_effect = [[user_query]]
91
+ gpg_encrypt_mock.side_effect = ["encrypted_content"]
92
+
93
+ command.execute()
94
+
95
+ secret_reader_mock.assert_called_once_with({
96
+ "path": vault_secret_path,
97
+ "version": str(version),
98
+ })
99
+ get_users_by_mock.assert_called_once_with(
100
+ refs=False,
101
+ filter=UserFilter(
102
+ org_username=target_user,
103
+ ),
104
+ )
105
+ gpg_encrypt_mock.assert_called_once_with(
106
+ content=json.dumps(secret, sort_keys=True, indent=4),
107
+ public_gpg_key=gpg_key,
108
+ )
109
+
110
+
111
+ @patch("reconcile.queries.get_users_by")
112
+ @patch("reconcile.queries.get_clusters")
113
+ def test_gpg_encrypt_oc_bad_path(get_clusters_mock, get_users_by_mock):
114
+ target_user = "testuser"
115
+ user_query = {
116
+ "org_username": target_user,
117
+ "public_gpg_key": "xyz",
118
+ }
119
+ command = craft_command(
120
+ command_data=GPGEncryptCommandData(
121
+ openshift_path="cluster/secret",
122
+ target_user=target_user,
123
+ ),
124
+ secret={},
125
+ )
126
+
127
+ get_users_by_mock.side_effect = [[user_query]]
128
+ get_clusters_mock.side_effect = [[{"name": "cluster"}]]
129
+
130
+ with pytest.raises(ArgumentException) as exc:
131
+ command.execute()
132
+ assert "Wrong format!" in str(exc.value)
133
+
134
+
135
+ @patch("reconcile.queries.get_users_by")
136
+ @patch("reconcile.queries.get_clusters_by")
137
+ def test_gpg_encrypt_oc_cluster_not_exists(get_clusters_mock, get_users_by_mock):
138
+ target_user = "testuser"
139
+ user_query = {
140
+ "org_username": target_user,
141
+ "public_gpg_key": "xyz",
142
+ }
143
+ command = craft_command(
144
+ command_data=GPGEncryptCommandData(
145
+ openshift_path="cluster/namespace/secret",
146
+ target_user=target_user,
147
+ ),
148
+ secret={},
149
+ )
150
+
151
+ get_users_by_mock.side_effect = [[user_query]]
152
+ get_clusters_mock.side_effect = [[]]
153
+
154
+ with pytest.raises(ArgumentException) as exc:
155
+ command.execute()
156
+ assert "No cluster found" in str(exc.value)
157
+
158
+
159
+ @patch("builtins.open", new_callable=mock_open, read_data="test-data")
160
+ @patch("reconcile.utils.gpg.gpg_encrypt")
161
+ @patch("reconcile.queries.get_users_by")
162
+ def test_gpg_encrypt_from_local_file(
163
+ get_users_by_mock, gpg_encrypt_mock, mock_file, capsys
164
+ ):
165
+ target_user = "testuser"
166
+ file_path = "/tmp/tmp"
167
+ encrypted_content = "encrypted_content"
168
+ user_query = {
169
+ "org_username": target_user,
170
+ "public_gpg_key": "xyz",
171
+ }
172
+ command = craft_command(
173
+ command_data=GPGEncryptCommandData(
174
+ secret_file_path=file_path,
175
+ target_user=target_user,
176
+ ),
177
+ secret={},
178
+ )
179
+ secret_reader_mock = command._secret_reader.read_all
180
+ get_users_by_mock.side_effect = [[user_query]]
181
+ gpg_encrypt_mock.side_effect = [encrypted_content]
182
+
183
+ command.execute()
184
+
185
+ captured = capsys.readouterr()
186
+ assert captured.out == f"{encrypted_content}\n"
187
+ mock_file.assert_called_once_with(file_path, encoding="locale")
188
+ secret_reader_mock.read_all.assert_not_called()
189
+
190
+
191
+ @patch("reconcile.queries.get_users_by")
192
+ def test_gpg_encrypt_user_not_found(get_users_by_mock):
193
+ target_user = "testuser"
194
+ command = craft_command(
195
+ command_data=GPGEncryptCommandData(
196
+ vault_secret_path="/tmp/tmp",
197
+ target_user=target_user,
198
+ ),
199
+ secret={},
200
+ )
201
+ get_users_by_mock.side_effect = [[]]
202
+
203
+ with pytest.raises(UserException) as exc:
204
+ command.execute()
205
+ assert "Expected to find exactly one user" in str(exc.value)
206
+
207
+
208
+ @patch("reconcile.queries.get_users_by")
209
+ def test_gpg_encrypt_user_no_gpg_key(get_users_by_mock):
210
+ target_user = "testuser"
211
+ command = craft_command(
212
+ command_data=GPGEncryptCommandData(
213
+ vault_secret_path="/tmp/tmp",
214
+ target_user=target_user,
215
+ ),
216
+ secret={},
217
+ )
218
+ get_users_by_mock.side_effect = [[{"org_username": target_user}]]
219
+
220
+ with pytest.raises(UserException) as exc:
221
+ command.execute()
222
+ assert "associated GPG key" in str(exc.value)
223
+
224
+
225
+ def test_gpg_encrypt_no_secret_specified():
226
+ command = craft_command(
227
+ command_data=GPGEncryptCommandData(
228
+ target_user="one_user",
229
+ ),
230
+ secret={},
231
+ )
232
+
233
+ with pytest.raises(ArgumentException) as exc:
234
+ command.execute()
235
+ assert "No argument given" in str(exc.value)
@@ -0,0 +1,255 @@
1
+ from collections.abc import Callable
2
+ from typing import Any
3
+
4
+ import pytest
5
+ from pytest_mock import MockerFixture
6
+
7
+ from reconcile.typed_queries.cost_report.app_names import App
8
+ from reconcile.typed_queries.cost_report.cost_namespaces import (
9
+ CostNamespace,
10
+ CostNamespaceLabels,
11
+ )
12
+ from tools.cli_commands.cost_report.model import (
13
+ OptimizationReport,
14
+ OptimizationReportItem,
15
+ )
16
+ from tools.cli_commands.cost_report.openshift_cost_optimization import (
17
+ OpenShiftCostOptimizationReportCommand,
18
+ )
19
+ from tools.cli_commands.test.conftest import (
20
+ COST_REPORT_SECRET,
21
+ OPENSHIFT_COST_OPTIMIZATION_RESPONSE,
22
+ OPENSHIFT_COST_OPTIMIZATION_WITH_FUZZY_MATCH_RESPONSE,
23
+ )
24
+
25
+
26
+ @pytest.fixture
27
+ def mock_gql(mocker: MockerFixture) -> Any:
28
+ return mocker.patch(
29
+ "tools.cli_commands.cost_report.openshift_cost_optimization.gql"
30
+ )
31
+
32
+
33
+ @pytest.fixture
34
+ def mock_cost_management_api(mocker: MockerFixture) -> Any:
35
+ return mocker.patch(
36
+ "tools.cli_commands.cost_report.openshift_cost_optimization.CostManagementApi",
37
+ autospec=True,
38
+ )
39
+
40
+
41
+ @pytest.fixture
42
+ def mock_fetch_cost_report_secret(mocker: MockerFixture) -> Any:
43
+ return mocker.patch(
44
+ "tools.cli_commands.cost_report.openshift_cost_optimization.fetch_cost_report_secret",
45
+ return_value=COST_REPORT_SECRET,
46
+ )
47
+
48
+
49
+ def test_openshift_cost_optimization_report_create(
50
+ mock_gql: Any,
51
+ mock_cost_management_api: Any,
52
+ mock_fetch_cost_report_secret: Any,
53
+ ) -> None:
54
+ openshift_cost_optimization_report_command = (
55
+ OpenShiftCostOptimizationReportCommand.create()
56
+ )
57
+
58
+ assert isinstance(
59
+ openshift_cost_optimization_report_command,
60
+ OpenShiftCostOptimizationReportCommand,
61
+ )
62
+ assert (
63
+ openshift_cost_optimization_report_command.gql_api
64
+ == mock_gql.get_api.return_value
65
+ )
66
+ assert (
67
+ openshift_cost_optimization_report_command.cost_management_api
68
+ == mock_cost_management_api.create_from_secret.return_value
69
+ )
70
+ assert openshift_cost_optimization_report_command.thread_pool_size == 10
71
+ mock_cost_management_api.create_from_secret.assert_called_once_with(
72
+ COST_REPORT_SECRET
73
+ )
74
+ mock_fetch_cost_report_secret.assert_called_once_with(mock_gql.get_api.return_value)
75
+
76
+
77
+ @pytest.fixture
78
+ def openshift_cost_optimization_report_command(
79
+ mock_gql: Any,
80
+ mock_cost_management_api: Any,
81
+ mock_fetch_cost_report_secret: Any,
82
+ ) -> OpenShiftCostOptimizationReportCommand:
83
+ return OpenShiftCostOptimizationReportCommand.create()
84
+
85
+
86
+ @pytest.fixture
87
+ def mock_get_app_names(mocker: MockerFixture) -> Any:
88
+ return mocker.patch(
89
+ "tools.cli_commands.cost_report.openshift_cost_optimization.get_app_names"
90
+ )
91
+
92
+
93
+ @pytest.fixture
94
+ def mock_get_cost_namespaces(mocker: MockerFixture) -> Any:
95
+ return mocker.patch(
96
+ "tools.cli_commands.cost_report.openshift_cost_optimization.get_cost_namespaces"
97
+ )
98
+
99
+
100
+ APP = App(name="app", parent_app_name=None)
101
+
102
+ APP_NAMESPACE = CostNamespace(
103
+ name="some-project",
104
+ labels=CostNamespaceLabels(insights_cost_management_optimizations="true"),
105
+ app_name=APP.name,
106
+ cluster_name="some-cluster",
107
+ cluster_external_id="some-cluster-uuid",
108
+ )
109
+
110
+ APP_NAMESPACE_OPTIMIZATION_DISABLED = CostNamespace(
111
+ name="some-project-disabled",
112
+ labels=CostNamespaceLabels(),
113
+ app_name=APP.name,
114
+ cluster_name="some-cluster",
115
+ cluster_external_id="some-cluster-uuid",
116
+ )
117
+
118
+
119
+ def test_openshift_cost_optimization_report_execute(
120
+ openshift_cost_optimization_report_command: OpenShiftCostOptimizationReportCommand,
121
+ mock_get_app_names: Any,
122
+ mock_get_cost_namespaces: Any,
123
+ fx: Callable,
124
+ ) -> None:
125
+ mock_get_app_names.return_value = []
126
+ mock_get_cost_namespaces.return_value = []
127
+ expected_output = fx("empty_openshift_cost_optimization_report.md")
128
+
129
+ output = openshift_cost_optimization_report_command.execute()
130
+
131
+ assert output.rstrip() == expected_output.rstrip()
132
+
133
+
134
+ def test_openshift_cost_optimization_report_get_apps(
135
+ openshift_cost_optimization_report_command: OpenShiftCostOptimizationReportCommand,
136
+ mock_get_app_names: Any,
137
+ ) -> None:
138
+ expected_apps = [APP]
139
+ mock_get_app_names.return_value = expected_apps
140
+
141
+ apps = openshift_cost_optimization_report_command.get_apps()
142
+
143
+ assert apps == expected_apps
144
+
145
+
146
+ def test_openshift_cost_optimization_report_get_cost_namespaces(
147
+ openshift_cost_optimization_report_command: OpenShiftCostOptimizationReportCommand,
148
+ mock_get_cost_namespaces: Any,
149
+ ) -> None:
150
+ expected_namespaces = [APP_NAMESPACE]
151
+ mock_get_cost_namespaces.return_value = [
152
+ APP_NAMESPACE,
153
+ APP_NAMESPACE_OPTIMIZATION_DISABLED,
154
+ ]
155
+
156
+ apps = openshift_cost_optimization_report_command.get_cost_namespaces()
157
+
158
+ assert apps == expected_namespaces
159
+
160
+
161
+ def test_openshift_cost_optimization_report_get_cost_namespaces_filter(
162
+ openshift_cost_optimization_report_command: OpenShiftCostOptimizationReportCommand,
163
+ mock_get_cost_namespaces: Any,
164
+ ) -> None:
165
+ expected_namespaces = [APP_NAMESPACE]
166
+ mock_get_cost_namespaces.return_value = expected_namespaces
167
+
168
+ apps = openshift_cost_optimization_report_command.get_cost_namespaces()
169
+
170
+ assert apps == expected_namespaces
171
+
172
+
173
+ def test_openshift_cost_optimization_report_get_reports(
174
+ openshift_cost_optimization_report_command: OpenShiftCostOptimizationReportCommand,
175
+ mock_cost_management_api: Any,
176
+ ) -> None:
177
+ mocked_api = mock_cost_management_api.create_from_secret.return_value
178
+ mocked_api.get_openshift_cost_optimization_report.return_value = (
179
+ OPENSHIFT_COST_OPTIMIZATION_RESPONSE
180
+ )
181
+
182
+ reports = openshift_cost_optimization_report_command.get_reports([APP_NAMESPACE])
183
+
184
+ assert reports == {APP_NAMESPACE: OPENSHIFT_COST_OPTIMIZATION_RESPONSE}
185
+ mocked_api.get_openshift_cost_optimization_report.assert_called_once_with(
186
+ project=APP_NAMESPACE.name,
187
+ cluster=APP_NAMESPACE.cluster_external_id,
188
+ )
189
+
190
+
191
+ def test_openshift_cost_optimization_report_get_reports_with_fuzzy_results(
192
+ openshift_cost_optimization_report_command: OpenShiftCostOptimizationReportCommand,
193
+ mock_cost_management_api: Any,
194
+ ) -> None:
195
+ mocked_api = mock_cost_management_api.create_from_secret.return_value
196
+ mocked_api.get_openshift_cost_optimization_report.return_value = (
197
+ OPENSHIFT_COST_OPTIMIZATION_WITH_FUZZY_MATCH_RESPONSE
198
+ )
199
+
200
+ reports = openshift_cost_optimization_report_command.get_reports([APP_NAMESPACE])
201
+
202
+ assert reports == {APP_NAMESPACE: OPENSHIFT_COST_OPTIMIZATION_RESPONSE}
203
+ mocked_api.get_openshift_cost_optimization_report.assert_called_once_with(
204
+ project=APP_NAMESPACE.name,
205
+ cluster=APP_NAMESPACE.cluster_external_id,
206
+ )
207
+
208
+
209
+ APP_REPORT = OptimizationReport(
210
+ app_name=APP.name,
211
+ items=[
212
+ OptimizationReportItem(
213
+ cluster=APP_NAMESPACE.cluster_name,
214
+ project=APP_NAMESPACE.name,
215
+ workload="test-deployment",
216
+ workload_type="deployment",
217
+ container="test",
218
+ current_cpu_limit="4",
219
+ current_cpu_request="1",
220
+ current_memory_limit="5Gi",
221
+ current_memory_request="400Mi",
222
+ recommend_cpu_request="3",
223
+ recommend_cpu_limit="5",
224
+ recommend_memory_request="700Mi",
225
+ recommend_memory_limit="6Gi",
226
+ )
227
+ ],
228
+ )
229
+
230
+
231
+ def test_openshift_cost_report_process_reports(
232
+ openshift_cost_optimization_report_command: OpenShiftCostOptimizationReportCommand,
233
+ ) -> None:
234
+ expected_reports = [APP_REPORT]
235
+
236
+ reports = openshift_cost_optimization_report_command.process_reports(
237
+ apps=[APP],
238
+ responses={
239
+ APP_NAMESPACE: OPENSHIFT_COST_OPTIMIZATION_RESPONSE,
240
+ },
241
+ )
242
+
243
+ assert reports == expected_reports
244
+
245
+
246
+ def test_openshift_cost_report_render(
247
+ openshift_cost_optimization_report_command: OpenShiftCostOptimizationReportCommand,
248
+ fx: Callable,
249
+ ) -> None:
250
+ expected_output = fx("openshift_cost_optimization_report.md")
251
+ reports = [APP_REPORT]
252
+
253
+ output = openshift_cost_optimization_report_command.render(reports)
254
+
255
+ assert output == expected_output