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.
- {qontract_reconcile-0.10.1rc1149.dist-info → qontract_reconcile-0.10.1rc1151.dist-info}/METADATA +1 -1
- {qontract_reconcile-0.10.1rc1149.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 +67 -24
- tools/test/test_qontract_cli.py +24 -0
- {qontract_reconcile-0.10.1rc1149.dist-info → qontract_reconcile-0.10.1rc1151.dist-info}/WHEEL +0 -0
- {qontract_reconcile-0.10.1rc1149.dist-info → qontract_reconcile-0.10.1rc1151.dist-info}/entry_points.txt +0 -0
- {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
|