qontract-reconcile 0.10.1rc696__py3-none-any.whl → 0.10.1rc702__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 (42) hide show
  1. {qontract_reconcile-0.10.1rc696.dist-info → qontract_reconcile-0.10.1rc702.dist-info}/METADATA +1 -1
  2. {qontract_reconcile-0.10.1rc696.dist-info → qontract_reconcile-0.10.1rc702.dist-info}/RECORD +42 -18
  3. reconcile/aws_account_manager/__init__.py +0 -0
  4. reconcile/aws_account_manager/integration.py +342 -0
  5. reconcile/aws_account_manager/merge_request_manager.py +111 -0
  6. reconcile/aws_account_manager/reconciler.py +353 -0
  7. reconcile/aws_account_manager/utils.py +38 -0
  8. reconcile/aws_saml_idp/integration.py +2 -0
  9. reconcile/aws_version_sync/integration.py +12 -11
  10. reconcile/aws_version_sync/merge_request_manager/merge_request_manager.py +39 -112
  11. reconcile/cli.py +79 -0
  12. reconcile/gql_definitions/aws_account_manager/__init__.py +0 -0
  13. reconcile/gql_definitions/aws_account_manager/aws_accounts.py +163 -0
  14. reconcile/gql_definitions/cost_report/__init__.py +0 -0
  15. reconcile/gql_definitions/cost_report/app_names.py +68 -0
  16. reconcile/gql_definitions/cost_report/settings.py +77 -0
  17. reconcile/gql_definitions/fragments/aws_account_managed.py +49 -0
  18. reconcile/queries.py +7 -1
  19. reconcile/templating/lib/merge_request_manager.py +8 -82
  20. reconcile/templating/renderer.py +2 -2
  21. reconcile/typed_queries/cost_report/__init__.py +0 -0
  22. reconcile/typed_queries/cost_report/app_names.py +22 -0
  23. reconcile/typed_queries/cost_report/settings.py +15 -0
  24. reconcile/utils/aws_api_typed/api.py +49 -6
  25. reconcile/utils/aws_api_typed/iam.py +22 -7
  26. reconcile/utils/aws_api_typed/organization.py +78 -30
  27. reconcile/utils/aws_api_typed/service_quotas.py +79 -0
  28. reconcile/utils/aws_api_typed/support.py +79 -0
  29. reconcile/utils/merge_request_manager/merge_request_manager.py +102 -0
  30. reconcile/utils/oauth2_backend_application_session.py +102 -0
  31. reconcile/utils/state.py +42 -38
  32. tools/cli_commands/cost_report/__init__.py +0 -0
  33. tools/cli_commands/cost_report/command.py +172 -0
  34. tools/cli_commands/cost_report/cost_management_api.py +57 -0
  35. tools/cli_commands/cost_report/model.py +29 -0
  36. tools/cli_commands/cost_report/response.py +48 -0
  37. tools/cli_commands/cost_report/view.py +333 -0
  38. tools/qontract_cli.py +10 -2
  39. tools/test/test_qontract_cli.py +20 -0
  40. {qontract_reconcile-0.10.1rc696.dist-info → qontract_reconcile-0.10.1rc702.dist-info}/WHEEL +0 -0
  41. {qontract_reconcile-0.10.1rc696.dist-info → qontract_reconcile-0.10.1rc702.dist-info}/entry_points.txt +0 -0
  42. {qontract_reconcile-0.10.1rc696.dist-info → qontract_reconcile-0.10.1rc702.dist-info}/top_level.txt +0 -0
reconcile/cli.py CHANGED
@@ -951,6 +951,85 @@ def aws_saml_roles(
951
951
  )
952
952
 
953
953
 
954
+ @integration.command(short_help="Create and manage AWS accounts.")
955
+ @account_name
956
+ @click.option(
957
+ "--flavor",
958
+ help="Flavor of the AWS account manager.",
959
+ required=True,
960
+ default="app-interface-commercial",
961
+ )
962
+ @click.option(
963
+ "--tag",
964
+ "-t",
965
+ type=(str, str),
966
+ multiple=True,
967
+ default=[("managed-by", "app-interface")],
968
+ )
969
+ @click.option(
970
+ "--initial-user-name",
971
+ help="The name of the initial user to be created in the account.",
972
+ required=True,
973
+ default="terraform",
974
+ )
975
+ @click.option(
976
+ "--initial-user-policy-arn",
977
+ help="The ARN of the policy that is attached to the initial user.",
978
+ required=True,
979
+ default="arn:aws:iam::aws:policy/AdministratorAccess",
980
+ )
981
+ @click.option(
982
+ "--initial-user-secret-vault-path",
983
+ help="The path in Vault to store the initial user secret. Python format string with access to 'account_name' attribute.",
984
+ required=True,
985
+ default="app-sre/creds/terraform/{account_name}/config",
986
+ )
987
+ @click.option(
988
+ "--account-tmpl-resource",
989
+ help="Resource name of the account template-collection template in the app-interface.",
990
+ required=True,
991
+ default="/aws-account-manager/account-tmpl.yml",
992
+ )
993
+ @click.option(
994
+ "--template-collection-root-path",
995
+ help="File path to the root directory to store new account template-collections.",
996
+ required=True,
997
+ default="data/templating/collections/aws-account",
998
+ )
999
+ @click.pass_context
1000
+ def aws_account_manager(
1001
+ ctx,
1002
+ account_name,
1003
+ flavor,
1004
+ tag,
1005
+ initial_user_name,
1006
+ initial_user_policy_arn,
1007
+ initial_user_secret_vault_path,
1008
+ account_tmpl_resource,
1009
+ template_collection_root_path,
1010
+ ):
1011
+ from reconcile.aws_account_manager.integration import (
1012
+ AwsAccountMgmtIntegration,
1013
+ AwsAccountMgmtIntegrationParams,
1014
+ )
1015
+
1016
+ run_class_integration(
1017
+ integration=AwsAccountMgmtIntegration(
1018
+ AwsAccountMgmtIntegrationParams(
1019
+ account_name=account_name,
1020
+ flavor=flavor,
1021
+ default_tags=dict(tag),
1022
+ initial_user_name=initial_user_name,
1023
+ initial_user_policy_arn=initial_user_policy_arn,
1024
+ initial_user_secret_vault_path=initial_user_secret_vault_path,
1025
+ account_tmpl_resource=account_tmpl_resource,
1026
+ template_collection_root_path=template_collection_root_path,
1027
+ )
1028
+ ),
1029
+ ctx=ctx.obj,
1030
+ )
1031
+
1032
+
954
1033
  @integration.command(short_help="Manage Jenkins roles association via REST API.")
955
1034
  @click.pass_context
956
1035
  def jenkins_roles(ctx):
@@ -0,0 +1,163 @@
1
+ """
2
+ Generated by qenerate plugin=pydantic_v1. DO NOT MODIFY MANUALLY!
3
+ """
4
+ from collections.abc import Callable # noqa: F401 # pylint: disable=W0611
5
+ from datetime import datetime # noqa: F401 # pylint: disable=W0611
6
+ from enum import Enum # noqa: F401 # pylint: disable=W0611
7
+ from typing import ( # noqa: F401 # pylint: disable=W0611
8
+ Any,
9
+ Optional,
10
+ Union,
11
+ )
12
+
13
+ from pydantic import ( # noqa: F401 # pylint: disable=W0611
14
+ BaseModel,
15
+ Extra,
16
+ Field,
17
+ Json,
18
+ )
19
+
20
+ from reconcile.gql_definitions.fragments.aws_account_managed import AWSAccountManaged
21
+ from reconcile.gql_definitions.fragments.vault_secret import VaultSecret
22
+
23
+
24
+ DEFINITION = """
25
+ fragment AWSAccountManaged on AWSAccount_v1 {
26
+ name
27
+ uid
28
+ alias
29
+ premiumSupport
30
+ organization {
31
+ ou
32
+ tags
33
+ }
34
+ quotaLimits {
35
+ name
36
+ quotas {
37
+ serviceCode
38
+ quotaCode
39
+ value
40
+ }
41
+ }
42
+ }
43
+
44
+ fragment VaultSecret on VaultSecret_v1 {
45
+ path
46
+ field
47
+ version
48
+ format
49
+ }
50
+
51
+ query AWSAccountManagerAccounts {
52
+ accounts: awsaccounts_v1 {
53
+ ... AWSAccountManaged
54
+ resourcesDefaultRegion
55
+ automationToken {
56
+ ...VaultSecret
57
+ }
58
+ disable {
59
+ integrations
60
+ }
61
+ automationRole {
62
+ awsAccountManager
63
+ }
64
+ # for the requests via "payer account"
65
+ account_requests {
66
+ path
67
+ name
68
+ description
69
+ accountOwner {
70
+ name
71
+ email
72
+ }
73
+ organization {
74
+ ou
75
+ tags
76
+ payerAccount {
77
+ path
78
+ }
79
+ }
80
+ quotaLimits {
81
+ path
82
+ }
83
+ }
84
+ organization_accounts {
85
+ ... AWSAccountManaged
86
+ }
87
+ }
88
+ }
89
+ """
90
+
91
+
92
+ class ConfiguredBaseModel(BaseModel):
93
+ class Config:
94
+ smart_union=True
95
+ extra=Extra.forbid
96
+
97
+
98
+ class DisableClusterAutomationsV1(ConfiguredBaseModel):
99
+ integrations: Optional[list[str]] = Field(..., alias="integrations")
100
+
101
+
102
+ class AWSAutomationRoleV1(ConfiguredBaseModel):
103
+ aws_account_manager: Optional[str] = Field(..., alias="awsAccountManager")
104
+
105
+
106
+ class OwnerV1(ConfiguredBaseModel):
107
+ name: str = Field(..., alias="name")
108
+ email: str = Field(..., alias="email")
109
+
110
+
111
+ class AWSOrganizationV1_AWSAccountV1(ConfiguredBaseModel):
112
+ path: str = Field(..., alias="path")
113
+
114
+
115
+ class AWSOrganizationV1(ConfiguredBaseModel):
116
+ ou: str = Field(..., alias="ou")
117
+ tags: Json = Field(..., alias="tags")
118
+ payer_account: AWSOrganizationV1_AWSAccountV1 = Field(..., alias="payerAccount")
119
+
120
+
121
+ class AWSQuotaLimitsV1(ConfiguredBaseModel):
122
+ path: str = Field(..., alias="path")
123
+
124
+
125
+ class AWSAccountRequestV1(ConfiguredBaseModel):
126
+ path: str = Field(..., alias="path")
127
+ name: str = Field(..., alias="name")
128
+ description: str = Field(..., alias="description")
129
+ account_owner: OwnerV1 = Field(..., alias="accountOwner")
130
+ organization: AWSOrganizationV1 = Field(..., alias="organization")
131
+ quota_limits: Optional[list[AWSQuotaLimitsV1]] = Field(..., alias="quotaLimits")
132
+
133
+
134
+ class AWSAccountV1(AWSAccountManaged):
135
+ resources_default_region: str = Field(..., alias="resourcesDefaultRegion")
136
+ automation_token: VaultSecret = Field(..., alias="automationToken")
137
+ disable: Optional[DisableClusterAutomationsV1] = Field(..., alias="disable")
138
+ automation_role: Optional[AWSAutomationRoleV1] = Field(..., alias="automationRole")
139
+ account_requests: Optional[list[AWSAccountRequestV1]] = Field(..., alias="account_requests")
140
+ organization_accounts: Optional[list[AWSAccountManaged]] = Field(..., alias="organization_accounts")
141
+
142
+
143
+ class AWSAccountManagerAccountsQueryData(ConfiguredBaseModel):
144
+ accounts: Optional[list[AWSAccountV1]] = Field(..., alias="accounts")
145
+
146
+
147
+ def query(query_func: Callable, **kwargs: Any) -> AWSAccountManagerAccountsQueryData:
148
+ """
149
+ This is a convenience function which queries and parses the data into
150
+ concrete types. It should be compatible with most GQL clients.
151
+ You do not have to use it to consume the generated data classes.
152
+ Alternatively, you can also mime and alternate the behavior
153
+ of this function in the caller.
154
+
155
+ Parameters:
156
+ query_func (Callable): Function which queries your GQL Server
157
+ kwargs: optional arguments that will be passed to the query function
158
+
159
+ Returns:
160
+ AWSAccountManagerAccountsQueryData: queried data parsed into generated classes
161
+ """
162
+ raw_data: dict[Any, Any] = query_func(DEFINITION, **kwargs)
163
+ return AWSAccountManagerAccountsQueryData(**raw_data)
File without changes
@@ -0,0 +1,68 @@
1
+ """
2
+ Generated by qenerate plugin=pydantic_v1. DO NOT MODIFY MANUALLY!
3
+ """
4
+ from collections.abc import Callable # noqa: F401 # pylint: disable=W0611
5
+ from datetime import datetime # noqa: F401 # pylint: disable=W0611
6
+ from enum import Enum # noqa: F401 # pylint: disable=W0611
7
+ from typing import ( # noqa: F401 # pylint: disable=W0611
8
+ Any,
9
+ Optional,
10
+ Union,
11
+ )
12
+
13
+ from pydantic import ( # noqa: F401 # pylint: disable=W0611
14
+ BaseModel,
15
+ Extra,
16
+ Field,
17
+ Json,
18
+ )
19
+
20
+
21
+ DEFINITION = """
22
+ query AppNames {
23
+ apps: apps_v1 {
24
+ name
25
+ parentApp {
26
+ name
27
+ }
28
+ }
29
+ }
30
+ """
31
+
32
+
33
+ class ConfiguredBaseModel(BaseModel):
34
+ class Config:
35
+ smart_union=True
36
+ extra=Extra.forbid
37
+
38
+
39
+ class AppV1_AppV1(ConfiguredBaseModel):
40
+ name: str = Field(..., alias="name")
41
+
42
+
43
+ class AppV1(ConfiguredBaseModel):
44
+ name: str = Field(..., alias="name")
45
+ parent_app: Optional[AppV1_AppV1] = Field(..., alias="parentApp")
46
+
47
+
48
+ class AppNamesQueryData(ConfiguredBaseModel):
49
+ apps: Optional[list[AppV1]] = Field(..., alias="apps")
50
+
51
+
52
+ def query(query_func: Callable, **kwargs: Any) -> AppNamesQueryData:
53
+ """
54
+ This is a convenience function which queries and parses the data into
55
+ concrete types. It should be compatible with most GQL clients.
56
+ You do not have to use it to consume the generated data classes.
57
+ Alternatively, you can also mime and alternate the behavior
58
+ of this function in the caller.
59
+
60
+ Parameters:
61
+ query_func (Callable): Function which queries your GQL Server
62
+ kwargs: optional arguments that will be passed to the query function
63
+
64
+ Returns:
65
+ AppNamesQueryData: queried data parsed into generated classes
66
+ """
67
+ raw_data: dict[Any, Any] = query_func(DEFINITION, **kwargs)
68
+ return AppNamesQueryData(**raw_data)
@@ -0,0 +1,77 @@
1
+ """
2
+ Generated by qenerate plugin=pydantic_v1. DO NOT MODIFY MANUALLY!
3
+ """
4
+ from collections.abc import Callable # noqa: F401 # pylint: disable=W0611
5
+ from datetime import datetime # noqa: F401 # pylint: disable=W0611
6
+ from enum import Enum # noqa: F401 # pylint: disable=W0611
7
+ from typing import ( # noqa: F401 # pylint: disable=W0611
8
+ Any,
9
+ Optional,
10
+ Union,
11
+ )
12
+
13
+ from pydantic import ( # noqa: F401 # pylint: disable=W0611
14
+ BaseModel,
15
+ Extra,
16
+ Field,
17
+ Json,
18
+ )
19
+
20
+ from reconcile.gql_definitions.fragments.vault_secret import VaultSecret
21
+
22
+
23
+ DEFINITION = """
24
+ fragment VaultSecret on VaultSecret_v1 {
25
+ path
26
+ field
27
+ version
28
+ format
29
+ }
30
+
31
+ query CostReportAppInterfaceSettings {
32
+ settings: app_interface_settings_v1 {
33
+ costReport {
34
+ credentials {
35
+ ...VaultSecret
36
+ }
37
+ }
38
+ }
39
+ }
40
+ """
41
+
42
+
43
+ class ConfiguredBaseModel(BaseModel):
44
+ class Config:
45
+ smart_union=True
46
+ extra=Extra.forbid
47
+
48
+
49
+ class CostReportSettingsV1(ConfiguredBaseModel):
50
+ credentials: VaultSecret = Field(..., alias="credentials")
51
+
52
+
53
+ class AppInterfaceSettingsV1(ConfiguredBaseModel):
54
+ cost_report: Optional[CostReportSettingsV1] = Field(..., alias="costReport")
55
+
56
+
57
+ class CostReportAppInterfaceSettingsQueryData(ConfiguredBaseModel):
58
+ settings: Optional[list[AppInterfaceSettingsV1]] = Field(..., alias="settings")
59
+
60
+
61
+ def query(query_func: Callable, **kwargs: Any) -> CostReportAppInterfaceSettingsQueryData:
62
+ """
63
+ This is a convenience function which queries and parses the data into
64
+ concrete types. It should be compatible with most GQL clients.
65
+ You do not have to use it to consume the generated data classes.
66
+ Alternatively, you can also mime and alternate the behavior
67
+ of this function in the caller.
68
+
69
+ Parameters:
70
+ query_func (Callable): Function which queries your GQL Server
71
+ kwargs: optional arguments that will be passed to the query function
72
+
73
+ Returns:
74
+ CostReportAppInterfaceSettingsQueryData: queried data parsed into generated classes
75
+ """
76
+ raw_data: dict[Any, Any] = query_func(DEFINITION, **kwargs)
77
+ return CostReportAppInterfaceSettingsQueryData(**raw_data)
@@ -0,0 +1,49 @@
1
+ """
2
+ Generated by qenerate plugin=pydantic_v1. DO NOT MODIFY MANUALLY!
3
+ """
4
+ from collections.abc import Callable # noqa: F401 # pylint: disable=W0611
5
+ from datetime import datetime # noqa: F401 # pylint: disable=W0611
6
+ from enum import Enum # noqa: F401 # pylint: disable=W0611
7
+ from typing import ( # noqa: F401 # pylint: disable=W0611
8
+ Any,
9
+ Optional,
10
+ Union,
11
+ )
12
+
13
+ from pydantic import ( # noqa: F401 # pylint: disable=W0611
14
+ BaseModel,
15
+ Extra,
16
+ Field,
17
+ Json,
18
+ )
19
+
20
+
21
+ class ConfiguredBaseModel(BaseModel):
22
+ class Config:
23
+ smart_union=True
24
+ extra=Extra.forbid
25
+
26
+
27
+ class AWSOrganizationV1(ConfiguredBaseModel):
28
+ ou: str = Field(..., alias="ou")
29
+ tags: Json = Field(..., alias="tags")
30
+
31
+
32
+ class AWSQuotaV1(ConfiguredBaseModel):
33
+ service_code: str = Field(..., alias="serviceCode")
34
+ quota_code: str = Field(..., alias="quotaCode")
35
+ value: float = Field(..., alias="value")
36
+
37
+
38
+ class AWSQuotaLimitsV1(ConfiguredBaseModel):
39
+ name: str = Field(..., alias="name")
40
+ quotas: list[AWSQuotaV1] = Field(..., alias="quotas")
41
+
42
+
43
+ class AWSAccountManaged(ConfiguredBaseModel):
44
+ name: str = Field(..., alias="name")
45
+ uid: str = Field(..., alias="uid")
46
+ alias: Optional[str] = Field(..., alias="alias")
47
+ premium_support: bool = Field(..., alias="premiumSupport")
48
+ organization: Optional[AWSOrganizationV1] = Field(..., alias="organization")
49
+ quota_limits: Optional[list[AWSQuotaLimitsV1]] = Field(..., alias="quotaLimits")
reconcile/queries.py CHANGED
@@ -583,7 +583,13 @@ def get_aws_accounts(
583
583
  ecrs=ecrs,
584
584
  cleanup=cleanup,
585
585
  )
586
- return gqlapi.query(query)["accounts"]
586
+ accounts = gqlapi.query(query)["accounts"]
587
+ if terraform_state:
588
+ # a new account does not have a terraform state yet, ignore it until terraform-init does its job
589
+ return [
590
+ account for account in accounts if account.get("terraformState") is not None
591
+ ]
592
+ return accounts
587
593
 
588
594
 
589
595
  def get_state_aws_accounts(reset_passwords=False):
@@ -1,17 +1,16 @@
1
1
  import logging
2
2
  import re
3
3
  import string
4
- from dataclasses import dataclass
5
4
 
6
- from gitlab.v4.objects import ProjectMergeRequest
7
5
  from pydantic import BaseModel
8
6
 
9
7
  from reconcile.templating.lib.model import TemplateOutput
10
8
  from reconcile.utils.gitlab_api import GitLabApi
9
+ from reconcile.utils.merge_request_manager.merge_request_manager import (
10
+ MergeRequestManagerBase,
11
+ )
11
12
  from reconcile.utils.merge_request_manager.parser import (
12
13
  Parser,
13
- ParserError,
14
- ParserVersionError,
15
14
  )
16
15
  from reconcile.utils.mr import MergeRequestBase
17
16
  from reconcile.utils.vcs import VCS
@@ -79,12 +78,6 @@ class TemplateInfo(BaseModel):
79
78
  collection_hash: str
80
79
 
81
80
 
82
- @dataclass
83
- class OpenMergeRequest:
84
- raw: ProjectMergeRequest
85
- template_info: TemplateInfo
86
-
87
-
88
81
  class TemplateRenderingMR(MergeRequestBase):
89
82
  name = "TemplateRendering"
90
83
 
@@ -127,78 +120,11 @@ class TemplateRenderingMR(MergeRequestBase):
127
120
  )
128
121
 
129
122
 
130
- class MergeRequestManager:
131
- # TODO: Create base class for Merge Request Manager, to make it reusable
132
- """ """
133
-
123
+ class MergeRequestManager(MergeRequestManagerBase[TemplateInfo]):
134
124
  def __init__(self, vcs: VCS, parser: Parser):
135
- self._vcs = vcs
136
- self._parser = parser
137
- self._open_mrs: list[OpenMergeRequest] = []
138
- self._open_mrs_with_problems: list[OpenMergeRequest] = []
139
- self._housekeeping_ran = False
140
-
141
- def _merge_request_already_exists(
142
- self,
143
- collection: str,
144
- ) -> OpenMergeRequest | None:
145
- for mr in self._open_mrs:
146
- if mr.template_info.collection == collection:
147
- return mr
148
-
149
- return None
150
-
151
- def _fetch_avs_managed_open_merge_requests(self) -> list[ProjectMergeRequest]:
152
- all_open_mrs = self._vcs.get_open_app_interface_merge_requests()
153
- return [mr for mr in all_open_mrs if TR_LABEL in mr.labels]
154
-
155
- def housekeeping(self) -> None:
156
- """
157
- Close bad MRs:
158
- - bad description format
159
- - wrong version
160
- - merge conflict
161
-
162
- --> if we update the template output, we automatically close
163
- old open MRs and replace them with new ones.
164
- """
165
- for mr in self._fetch_avs_managed_open_merge_requests():
166
- attrs = mr.attributes
167
- desc = attrs.get("description")
168
- has_conflicts = attrs.get("has_conflicts", False)
169
- if has_conflicts:
170
- logging.info(
171
- "Merge-conflict detected. Closing %s",
172
- mr.attributes.get("web_url", "NO_WEBURL"),
173
- )
174
- self._vcs.close_app_interface_mr(
175
- mr, "Closing this MR because of a merge-conflict."
176
- )
177
- continue
178
- try:
179
- template_info = self._parser.parse(description=desc)
180
- except ParserVersionError:
181
- logging.info(
182
- "Old MR version detected! Closing %s",
183
- mr.attributes.get("web_url", "NO_WEBURL"),
184
- )
185
- self._vcs.close_app_interface_mr(
186
- mr, "Closing this MR because it has an outdated integration version"
187
- )
188
- continue
189
- except ParserError:
190
- logging.info(
191
- "Bad MR description format. Closing %s",
192
- mr.attributes.get("web_url", "NO_WEBURL"),
193
- )
194
- self._vcs.close_app_interface_mr(
195
- mr, "Closing this MR because of bad description format."
196
- )
197
- continue
198
- self._open_mrs.append(OpenMergeRequest(raw=mr, template_info=template_info))
199
- self._housekeeping_ran = True
125
+ super().__init__(vcs, parser, TR_LABEL)
200
126
 
201
- def create_tr_merge_request(self, output: list[TemplateOutput]) -> None:
127
+ def create_merge_request(self, output: list[TemplateOutput]) -> None:
202
128
  if not self._housekeeping_ran:
203
129
  self.housekeeping()
204
130
 
@@ -211,8 +137,8 @@ class MergeRequestManager:
211
137
  collection_hash = collection_hashes.pop()
212
138
 
213
139
  """Create a new MR with the rendered template."""
214
- if mr := self._merge_request_already_exists(collection):
215
- if mr.template_info.collection_hash == collection_hash:
140
+ if mr := self._merge_request_already_exists({"collection": collection}):
141
+ if mr.mr_info.collection_hash == collection_hash:
216
142
  logging.info(
217
143
  "MR already exists and has the same template hash. Skipping",
218
144
  )
@@ -97,7 +97,7 @@ class GitlabFilePersistence(FilePersistence):
97
97
 
98
98
  def write(self, outputs: list[TemplateOutput]) -> None:
99
99
  self.mr_manager.housekeeping()
100
- self.mr_manager.create_tr_merge_request(outputs)
100
+ self.mr_manager.create_merge_request(outputs)
101
101
 
102
102
  def read(self, path: str) -> Optional[str]:
103
103
  try:
@@ -182,10 +182,10 @@ class TemplateRendererIntegration(QontractReconcileIntegration):
182
182
  ruamel_instance: yaml.YAML,
183
183
  state: Optional[State] = None,
184
184
  ) -> None:
185
- outputs: list[TemplateOutput] = []
186
185
  gql_no_validation = init_from_config(validate_schemas=False)
187
186
 
188
187
  for c in get_template_collections():
188
+ outputs: list[TemplateOutput] = []
189
189
  variables = {}
190
190
  if c.variables:
191
191
  variables = {
File without changes
@@ -0,0 +1,22 @@
1
+ from pydantic import BaseModel
2
+
3
+ from reconcile.gql_definitions.cost_report.app_names import query
4
+ from reconcile.utils.gql import GqlApi
5
+
6
+
7
+ class App(BaseModel):
8
+ name: str
9
+ parent_app_name: str | None
10
+
11
+
12
+ def get_app_names(
13
+ gql_api: GqlApi,
14
+ ) -> list[App]:
15
+ apps = query(gql_api.query).apps or []
16
+ return [
17
+ App(
18
+ name=app.name,
19
+ parent_app_name=app.parent_app.name if app.parent_app else None,
20
+ )
21
+ for app in apps
22
+ ]
@@ -0,0 +1,15 @@
1
+ from reconcile.gql_definitions.cost_report.settings import CostReportSettingsV1, query
2
+ from reconcile.utils.exceptions import AppInterfaceSettingsError
3
+ from reconcile.utils.gql import GqlApi
4
+
5
+
6
+ def get_cost_report_settings(
7
+ gql_api: GqlApi,
8
+ ) -> CostReportSettingsV1:
9
+ data = query(gql_api.query)
10
+ if not data.settings:
11
+ raise AppInterfaceSettingsError("No settings configured")
12
+ cost_report_settings = data.settings[0].cost_report
13
+ if cost_report_settings is None:
14
+ raise AppInterfaceSettingsError("No cost report configured")
15
+ return cost_report_settings