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,295 @@
1
+ from collections.abc import Callable
2
+ from decimal import Decimal
3
+ from typing import Any
4
+
5
+ import pytest
6
+ from pytest_mock import MockerFixture
7
+
8
+ from reconcile.typed_queries.cost_report.app_names import App
9
+ from reconcile.typed_queries.cost_report.cost_namespaces import (
10
+ CostNamespace,
11
+ CostNamespaceLabels,
12
+ )
13
+ from tools.cli_commands.cost_report.model import ChildAppReport, Report, ReportItem
14
+ from tools.cli_commands.cost_report.openshift import OpenShiftCostReportCommand
15
+ from tools.cli_commands.cost_report.response import OpenShiftReportCostResponse
16
+ from tools.cli_commands.test.conftest import (
17
+ COST_REPORT_SECRET,
18
+ )
19
+
20
+
21
+ @pytest.fixture
22
+ def mock_gql(mocker: MockerFixture) -> Any:
23
+ return mocker.patch("tools.cli_commands.cost_report.openshift.gql")
24
+
25
+
26
+ @pytest.fixture
27
+ def mock_cost_management_api(mocker: MockerFixture) -> Any:
28
+ return mocker.patch(
29
+ "tools.cli_commands.cost_report.openshift.CostManagementApi",
30
+ autospec=True,
31
+ )
32
+
33
+
34
+ @pytest.fixture
35
+ def mock_fetch_cost_report_secret(mocker: MockerFixture) -> Any:
36
+ return mocker.patch(
37
+ "tools.cli_commands.cost_report.openshift.fetch_cost_report_secret",
38
+ return_value=COST_REPORT_SECRET,
39
+ )
40
+
41
+
42
+ def test_openshift_cost_report_create(
43
+ mock_gql: Any,
44
+ mock_cost_management_api: Any,
45
+ mock_fetch_cost_report_secret: Any,
46
+ ) -> None:
47
+ openshift_cost_report_command = OpenShiftCostReportCommand.create()
48
+
49
+ assert isinstance(openshift_cost_report_command, OpenShiftCostReportCommand)
50
+ assert openshift_cost_report_command.gql_api == mock_gql.get_api.return_value
51
+ assert (
52
+ openshift_cost_report_command.cost_management_api
53
+ == mock_cost_management_api.create_from_secret.return_value
54
+ )
55
+ assert openshift_cost_report_command.thread_pool_size == 10
56
+ mock_cost_management_api.create_from_secret.assert_called_once_with(
57
+ COST_REPORT_SECRET
58
+ )
59
+ mock_fetch_cost_report_secret.assert_called_once_with(mock_gql.get_api.return_value)
60
+
61
+
62
+ @pytest.fixture
63
+ def openshift_cost_report_command(
64
+ mock_gql: Any,
65
+ mock_cost_management_api: Any,
66
+ mock_fetch_cost_report_secret: Any,
67
+ ) -> OpenShiftCostReportCommand:
68
+ return OpenShiftCostReportCommand.create()
69
+
70
+
71
+ @pytest.fixture
72
+ def mock_get_app_names(mocker: MockerFixture) -> Any:
73
+ return mocker.patch("tools.cli_commands.cost_report.openshift.get_app_names")
74
+
75
+
76
+ @pytest.fixture
77
+ def mock_get_cost_namespaces(mocker: MockerFixture) -> Any:
78
+ return mocker.patch("tools.cli_commands.cost_report.openshift.get_cost_namespaces")
79
+
80
+
81
+ PARENT_APP = App(name="parent", parent_app_name=None)
82
+ CHILD_APP = App(name="child", parent_app_name="parent")
83
+
84
+ PARENT_APP_NAMESPACE = CostNamespace(
85
+ name="parent_namespace",
86
+ labels=CostNamespaceLabels(),
87
+ app_name=PARENT_APP.name,
88
+ cluster_name="parent_cluster",
89
+ cluster_external_id="parent_cluster_external_id",
90
+ )
91
+ CHILD_APP_NAMESPACE = CostNamespace(
92
+ name="child_namespace",
93
+ labels=CostNamespaceLabels(),
94
+ app_name=CHILD_APP.name,
95
+ cluster_name="child_cluster",
96
+ cluster_external_id="child_cluster_external_id",
97
+ )
98
+
99
+
100
+ def test_openshift_cost_report_execute(
101
+ openshift_cost_report_command: OpenShiftCostReportCommand,
102
+ mock_get_app_names: Any,
103
+ mock_get_cost_namespaces: Any,
104
+ fx: Callable,
105
+ ) -> None:
106
+ expected_output = fx("empty_openshift_cost_report.md")
107
+
108
+ mock_get_app_names.return_value = []
109
+ mock_get_cost_namespaces.return_value = []
110
+
111
+ output = openshift_cost_report_command.execute()
112
+
113
+ assert output.rstrip() == expected_output.rstrip()
114
+
115
+
116
+ def test_openshift_cost_report_get_apps(
117
+ openshift_cost_report_command: OpenShiftCostReportCommand,
118
+ mock_get_app_names: Any,
119
+ ) -> None:
120
+ expected_apps = [PARENT_APP, CHILD_APP]
121
+ mock_get_app_names.return_value = expected_apps
122
+
123
+ apps = openshift_cost_report_command.get_apps()
124
+
125
+ assert apps == expected_apps
126
+
127
+
128
+ def test_openshift_cost_report_get_cost_namespaces(
129
+ openshift_cost_report_command: OpenShiftCostReportCommand,
130
+ mock_get_cost_namespaces: Any,
131
+ ) -> None:
132
+ expected_namespaces = [PARENT_APP_NAMESPACE, CHILD_APP_NAMESPACE]
133
+ mock_get_cost_namespaces.return_value = expected_namespaces
134
+
135
+ apps = openshift_cost_report_command.get_cost_namespaces()
136
+
137
+ assert apps == expected_namespaces
138
+
139
+
140
+ def openshift_report_cost_response_builder(
141
+ delta_value: int,
142
+ delta_percent: int,
143
+ total: int,
144
+ project: str,
145
+ cluster: str,
146
+ ) -> OpenShiftReportCostResponse:
147
+ return OpenShiftReportCostResponse.parse_obj({
148
+ "meta": {
149
+ "delta": {
150
+ "value": delta_value,
151
+ "percent": delta_percent,
152
+ },
153
+ "total": {
154
+ "cost": {
155
+ "total": {
156
+ "value": total,
157
+ "units": "USD",
158
+ }
159
+ }
160
+ },
161
+ },
162
+ "data": [
163
+ {
164
+ "date": "2024-02",
165
+ "projects": [
166
+ {
167
+ "project": project,
168
+ "values": [
169
+ {
170
+ "delta_value": delta_value,
171
+ "delta_percent": delta_percent,
172
+ "clusters": [cluster],
173
+ "cost": {
174
+ "total": {
175
+ "value": total,
176
+ "units": "USD",
177
+ }
178
+ },
179
+ }
180
+ ],
181
+ },
182
+ ],
183
+ }
184
+ ],
185
+ })
186
+
187
+
188
+ PARENT_APP_COST_RESPONSE = openshift_report_cost_response_builder(
189
+ delta_value=100,
190
+ delta_percent=10,
191
+ total=1100,
192
+ project=PARENT_APP_NAMESPACE.name,
193
+ cluster=PARENT_APP_NAMESPACE.cluster_name,
194
+ )
195
+
196
+ CHILD_APP_COST_RESPONSE = openshift_report_cost_response_builder(
197
+ delta_value=200,
198
+ delta_percent=10,
199
+ total=2200,
200
+ project=CHILD_APP_NAMESPACE.name,
201
+ cluster=CHILD_APP_NAMESPACE.cluster_name,
202
+ )
203
+
204
+
205
+ def test_openshift_cost_report_get_reports(
206
+ openshift_cost_report_command: OpenShiftCostReportCommand,
207
+ mock_cost_management_api: Any,
208
+ ) -> None:
209
+ mocked_api = mock_cost_management_api.create_from_secret.return_value
210
+ mocked_api.get_openshift_costs_report.return_value = PARENT_APP_COST_RESPONSE
211
+
212
+ reports = openshift_cost_report_command.get_reports([PARENT_APP_NAMESPACE])
213
+
214
+ assert reports == {PARENT_APP_NAMESPACE: PARENT_APP_COST_RESPONSE}
215
+ mocked_api.get_openshift_costs_report.assert_called_once_with(
216
+ project=PARENT_APP_NAMESPACE.name,
217
+ cluster=PARENT_APP_NAMESPACE.cluster_external_id,
218
+ )
219
+
220
+
221
+ PARENT_APP_REPORT = Report(
222
+ app_name="parent",
223
+ parent_app_name=None,
224
+ child_apps=[
225
+ ChildAppReport(name="child", total=Decimal(2200)),
226
+ ],
227
+ child_apps_total=Decimal(2200),
228
+ date="2024-02",
229
+ items=[
230
+ ReportItem(
231
+ name=f"{PARENT_APP_NAMESPACE.cluster_name}/{PARENT_APP_NAMESPACE.name}",
232
+ delta_value=Decimal(100),
233
+ delta_percent=10,
234
+ total=Decimal(1100),
235
+ )
236
+ ],
237
+ items_total=Decimal(1100),
238
+ items_delta_value=Decimal(100),
239
+ items_delta_percent=10,
240
+ total=Decimal(3300),
241
+ )
242
+
243
+ CHILD_APP_REPORT = Report(
244
+ app_name="child",
245
+ parent_app_name="parent",
246
+ child_apps=[],
247
+ child_apps_total=Decimal(0),
248
+ date="2024-02",
249
+ items=[
250
+ ReportItem(
251
+ name=f"{CHILD_APP_NAMESPACE.cluster_name}/{CHILD_APP_NAMESPACE.name}",
252
+ delta_value=Decimal(200),
253
+ delta_percent=10,
254
+ total=Decimal(2200),
255
+ )
256
+ ],
257
+ items_total=Decimal(2200),
258
+ items_delta_value=Decimal(200),
259
+ items_delta_percent=10,
260
+ total=Decimal(2200),
261
+ )
262
+
263
+
264
+ def test_openshift_cost_report_process_reports(
265
+ openshift_cost_report_command: OpenShiftCostReportCommand,
266
+ ) -> None:
267
+ expected_reports = {
268
+ "parent": PARENT_APP_REPORT,
269
+ "child": CHILD_APP_REPORT,
270
+ }
271
+
272
+ reports = openshift_cost_report_command.process_reports(
273
+ apps=[PARENT_APP, CHILD_APP],
274
+ responses={
275
+ PARENT_APP_NAMESPACE: PARENT_APP_COST_RESPONSE,
276
+ CHILD_APP_NAMESPACE: CHILD_APP_COST_RESPONSE,
277
+ },
278
+ )
279
+
280
+ assert reports == expected_reports
281
+
282
+
283
+ def test_openshift_cost_report_render(
284
+ openshift_cost_report_command: OpenShiftCostReportCommand,
285
+ fx: Callable,
286
+ ) -> None:
287
+ expected_output = fx("openshift_cost_report.md")
288
+ reports = {
289
+ "parent": PARENT_APP_REPORT,
290
+ "child": CHILD_APP_REPORT,
291
+ }
292
+
293
+ output = openshift_cost_report_command.render(reports)
294
+
295
+ assert output == expected_output
@@ -0,0 +1,70 @@
1
+ from typing import Any
2
+ from unittest.mock import create_autospec
3
+
4
+ import pytest
5
+ from pytest_mock import MockerFixture
6
+
7
+ from reconcile.gql_definitions.common.app_interface_vault_settings import (
8
+ AppInterfaceSettingsV1,
9
+ )
10
+ from reconcile.gql_definitions.cost_report.settings import CostReportSettingsV1
11
+ from reconcile.gql_definitions.fragments.vault_secret import VaultSecret
12
+ from reconcile.utils.gql import GqlApi
13
+ from tools.cli_commands.cost_report.util import fetch_cost_report_secret
14
+ from tools.cli_commands.test.conftest import (
15
+ COST_REPORT_SECRET,
16
+ )
17
+
18
+ VAULT_SETTINGS = AppInterfaceSettingsV1(vault=True)
19
+ COST_REPORT_SETTINGS = CostReportSettingsV1(
20
+ credentials=VaultSecret(
21
+ path="some-path",
22
+ field="all",
23
+ version=None,
24
+ format=None,
25
+ )
26
+ )
27
+
28
+
29
+ @pytest.fixture
30
+ def mock_get_app_interface_vault_settings(mocker: MockerFixture) -> Any:
31
+ return mocker.patch(
32
+ "tools.cli_commands.cost_report.util.get_app_interface_vault_settings",
33
+ return_value=VAULT_SETTINGS,
34
+ )
35
+
36
+
37
+ @pytest.fixture
38
+ def mock_create_secret_reader(mocker: MockerFixture) -> Any:
39
+ mock = mocker.patch(
40
+ "tools.cli_commands.cost_report.util.create_secret_reader",
41
+ autospec=True,
42
+ )
43
+ mock.return_value.read_all_secret.return_value = COST_REPORT_SECRET
44
+ return mock
45
+
46
+
47
+ @pytest.fixture
48
+ def mock_get_cost_report_settings(mocker: MockerFixture) -> Any:
49
+ return mocker.patch(
50
+ "tools.cli_commands.cost_report.util.get_cost_report_settings",
51
+ return_value=COST_REPORT_SETTINGS,
52
+ )
53
+
54
+
55
+ def test_fetch_cost_report_secret(
56
+ mock_get_app_interface_vault_settings: Any,
57
+ mock_create_secret_reader: Any,
58
+ mock_get_cost_report_settings: Any,
59
+ ) -> None:
60
+ mock_gql_api = create_autospec(GqlApi)
61
+
62
+ secret = fetch_cost_report_secret(mock_gql_api)
63
+
64
+ assert secret == COST_REPORT_SECRET
65
+ mock_get_app_interface_vault_settings.assert_called_once_with(mock_gql_api.query)
66
+ mock_get_cost_report_settings.assert_called_once_with(mock_gql_api)
67
+ mock_create_secret_reader.assert_called_once_with(use_vault=True)
68
+ mock_create_secret_reader.return_value.read_all_secret.assert_called_once_with(
69
+ COST_REPORT_SETTINGS.credentials
70
+ )
tools/qontract_cli.py CHANGED
@@ -7,6 +7,7 @@ import logging
7
7
  import os
8
8
  import re
9
9
  import sys
10
+ import textwrap
10
11
  from collections import defaultdict
11
12
  from datetime import (
12
13
  UTC,
@@ -150,6 +151,9 @@ from reconcile.utils.state import init_state
150
151
  from reconcile.utils.terraform_client import TerraformClient as Terraform
151
152
  from tools.cli_commands.cost_report.aws import AwsCostReportCommand
152
153
  from tools.cli_commands.cost_report.openshift import OpenShiftCostReportCommand
154
+ from tools.cli_commands.cost_report.openshift_cost_optimization import (
155
+ OpenShiftCostOptimizationReportCommand,
156
+ )
153
157
  from tools.cli_commands.erv2 import (
154
158
  Erv2Cli,
155
159
  TerraformCli,
@@ -1372,53 +1376,70 @@ def aws_creds(ctx, account_name):
1372
1376
 
1373
1377
 
1374
1378
  @root.command()
1375
- @click.argument("account_name")
1376
- @click.argument("bucket")
1377
- @click.argument("src")
1378
- @click.argument("dest")
1379
- @click.argument("region", required=False, default="us-east-1")
1379
+ @click.option(
1380
+ "--account-uid",
1381
+ help="account UID of the account that owns the bucket",
1382
+ required=True,
1383
+ )
1384
+ @click.option(
1385
+ "--source-bucket",
1386
+ help="aws bucket where the source statefile is stored",
1387
+ required=True,
1388
+ )
1389
+ @click.option(
1390
+ "--source-object-path",
1391
+ help="path in the bucket where the statefile is stored",
1392
+ required=True,
1393
+ )
1394
+ @click.option(
1395
+ "--rename",
1396
+ help="optionally rename the destination repo, otherwise keep the same name for the new location",
1397
+ )
1398
+ @click.option("--region", help="AWS region")
1380
1399
  @click.option(
1381
1400
  "--force/--no-force",
1382
1401
  help="Force the copy even if a statefile already exists at the destination",
1383
1402
  default=False,
1384
1403
  )
1385
1404
  @click.pass_context
1386
- def copy_tfstate(ctx, account_name, bucket, src, dest, region, force):
1387
- """copy a manually managed terraform state file to the correct location expected by
1388
- the terraform-repo integration.
1389
-
1390
- SRC should include the full filename including the extension
1391
-
1392
- DEST should include the filename without extension.
1393
- """
1405
+ def copy_tfstate(
1406
+ ctx, source_bucket, source_object_path, account_uid, rename, region, force
1407
+ ):
1394
1408
  settings = queries.get_app_interface_settings()
1395
1409
  secret_reader = SecretReader(settings=settings)
1396
- accounts = queries.get_aws_accounts(name=account_name, terraform_state=True)
1410
+ accounts = queries.get_aws_accounts(uid=account_uid, terraform_state=True)
1397
1411
  if not accounts:
1398
- print(f"{account_name} not found.")
1412
+ print(f"{account_uid} not found in App-Interface.")
1399
1413
  sys.exit(1)
1400
1414
  account = accounts[0]
1401
1415
 
1402
- state_key = [
1416
+ # terraform repo stores its statefiles within a "folder" in AWS S3 which is defined in App-Interface
1417
+ dest_folder = [
1403
1418
  i
1404
1419
  for i in account["terraformState"]["integrations"]
1405
1420
  if i["integration"] == "terraform-repo"
1406
1421
  ]
1407
- if len(state_key) == 0:
1422
+ if not dest_folder:
1408
1423
  logging.error(
1409
- "terraform-repo is missing a section in this account's '/dependencies/terraform-state-1.yml' file, please add one using the docs in https://gitlab.cee.redhat.com/service/app-interface/-/blob/master/docs/terraform-repo/sop/migrating-existing-state.md?ref_type=heads and then try again"
1424
+ "terraform-repo is missing a section in this account's '/dependencies/terraform-state-1.yml' file, please add one using the docs in https://gitlab.cee.redhat.com/service/app-interface/-/blob/master/docs/terraform-repo/getting-started.md?ref_type=heads#step-1-setup-aws-account and then try again"
1410
1425
  )
1411
1426
  return
1412
1427
 
1413
- dest_key = f"{state_key[0]['key']}/{dest}-tf-repo.tfstate"
1428
+ dest_filename = ""
1429
+ if rename:
1430
+ dest_filename = rename.removesuffix(".tfstate")
1431
+ else:
1432
+ dest_filename = source_object_path.removesuffix(".tfstate")
1433
+
1434
+ dest_key = f"{dest_folder[0]['key']}/{dest_filename}-tf-repo.tfstate"
1414
1435
  dest_bucket = account["terraformState"]["bucket"]
1415
1436
 
1416
1437
  with AWSApi(1, accounts, settings, secret_reader) as aws:
1417
- session = aws.get_session(account_name)
1438
+ session = aws.get_session(account["name"])
1418
1439
  s3_client = aws.get_session_client(session, "s3", region)
1419
1440
  copy_source = {
1420
- "Bucket": bucket,
1421
- "Key": src,
1441
+ "Bucket": source_bucket,
1442
+ "Key": source_object_path,
1422
1443
  }
1423
1444
 
1424
1445
  dest_pretty_path = f"s3://{dest_bucket}/{dest_key}"
@@ -1438,10 +1459,25 @@ def copy_tfstate(ctx, account_name, bucket, src, dest, region, force):
1438
1459
  )
1439
1460
  return
1440
1461
 
1441
- prompt_text = f"Are you sure you want to copy 's3://{bucket}/{src}' to '{dest_pretty_path}'?"
1462
+ prompt_text = f"Are you sure you want to copy 's3://{source_bucket}/{source_object_path}' to '{dest_pretty_path}'?"
1442
1463
  if click.confirm(prompt_text):
1443
1464
  s3_client.copy(copy_source, dest_bucket, dest_key)
1444
- logging.info("successfully copied the statefile to the new location")
1465
+ print(
1466
+ textwrap.dedent(f"""
1467
+ Nicely done! Your tfstate file has been migrated. Now you can create a repo definition in App-Interface like so:
1468
+
1469
+ ---
1470
+ $schema: /aws/terraform-repo-1.yml
1471
+
1472
+ account:
1473
+ $ref: {account["path"]}
1474
+
1475
+ name: {dest_filename}
1476
+ repository: <FILL_IN>
1477
+ projectPath: <FILL_IN>
1478
+ tfVersion: <FILL_IN>
1479
+ ref: <FILL_IN>""")
1480
+ )
1445
1481
 
1446
1482
 
1447
1483
  @get.command(short_help='obtain "rosa create cluster" command by cluster name')
@@ -2739,6 +2775,13 @@ def openshift_cost_report(ctx):
2739
2775
  print(command.execute())
2740
2776
 
2741
2777
 
2778
+ @get.command()
2779
+ @click.pass_context
2780
+ def openshift_cost_optimization_report(ctx):
2781
+ command = OpenShiftCostOptimizationReportCommand.create()
2782
+ print(command.execute())
2783
+
2784
+
2742
2785
  @get.command()
2743
2786
  @click.pass_context
2744
2787
  def osd_component_versions(ctx):
@@ -171,3 +171,27 @@ def test_get_openshift_cost_report(
171
171
  assert result.output == "some report\n"
172
172
  mock_openshift_cost_report_command.create.assert_called_once_with()
173
173
  mock_openshift_cost_report_command.create.return_value.execute.assert_called_once_with()
174
+
175
+
176
+ @pytest.fixture
177
+ def mock_openshift_cost_optimization_report_command(mocker):
178
+ return mocker.patch(
179
+ "tools.qontract_cli.OpenShiftCostOptimizationReportCommand", autospec=True
180
+ )
181
+
182
+
183
+ def test_get_openshift_cost_optimization_report(
184
+ env_vars, mock_queries, mock_openshift_cost_optimization_report_command
185
+ ):
186
+ mock_openshift_cost_optimization_report_command.create.return_value.execute.return_value = "some report"
187
+ runner = CliRunner()
188
+ result = runner.invoke(
189
+ qontract_cli.get,
190
+ "openshift-cost-optimization-report",
191
+ obj={},
192
+ )
193
+
194
+ assert result.exit_code == 0
195
+ assert result.output == "some report\n"
196
+ mock_openshift_cost_optimization_report_command.create.assert_called_once_with()
197
+ mock_openshift_cost_optimization_report_command.create.return_value.execute.assert_called_once_with()