qontract-reconcile 0.10.1rc701__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.
@@ -0,0 +1,353 @@
1
+ import logging
2
+ from collections.abc import Iterable
3
+ from textwrap import dedent
4
+ from typing import Any, Protocol
5
+
6
+ from reconcile.aws_account_manager.utils import state_key
7
+ from reconcile.utils.aws_api_typed.api import AWSApi
8
+ from reconcile.utils.aws_api_typed.iam import (
9
+ AWSAccessKey,
10
+ )
11
+ from reconcile.utils.aws_api_typed.organization import AwsOrganizationOU
12
+ from reconcile.utils.aws_api_typed.service_quotas import (
13
+ AWSResourceAlreadyExistsException,
14
+ )
15
+ from reconcile.utils.aws_api_typed.support import SUPPORT_PLAN
16
+ from reconcile.utils.state import AbortStateTransaction, State
17
+
18
+ TASK_CREATE_ACCOUNT = "create-account"
19
+ TASK_DESCRIBE_ACCOUNT = "describe-account"
20
+ TASK_TAG_ACCOUNT = "tag-account"
21
+ TASK_MOVE_ACCOUNT = "move-account"
22
+ TASK_ACCOUNT_ALIAS = "account-alias"
23
+ TASK_CREATE_IAM_USER = "create-iam-user"
24
+ TASK_REQUEST_SERVICE_QUOTA = "request-service-quota"
25
+ TASK_CHECK_SERVICE_QUOTA_STATUS = "check-service-quota-status"
26
+ TASK_ENABLE_ENTERPRISE_SUPPORT = "enable-enterprise-support"
27
+ TASK_CHECK_ENTERPRISE_SUPPORT_STATUS = "check-enterprise-support-status"
28
+
29
+
30
+ class Quota(Protocol):
31
+ service_code: str
32
+ quota_code: str
33
+ value: float
34
+
35
+ def dict(self) -> dict[str, Any]: ...
36
+
37
+
38
+ class AWSReconciler:
39
+ def __init__(self, state: State, dry_run: bool) -> None:
40
+ self.state = state
41
+ self.dry_run = dry_run
42
+
43
+ def _create_account(
44
+ self,
45
+ aws_api: AWSApi,
46
+ name: str,
47
+ email: str,
48
+ ) -> str | None:
49
+ """Create the organization account and return the creation status ID."""
50
+ with self.state.transaction(state_key(name, TASK_CREATE_ACCOUNT)) as _state:
51
+ if _state.exists:
52
+ # account already exists, nothing to do
53
+ return _state.value
54
+
55
+ logging.info(f"Creating account {name}")
56
+ if self.dry_run:
57
+ raise AbortStateTransaction("Dry run")
58
+
59
+ status = aws_api.organizations.create_account(email=email, name=name)
60
+ # store the status id for future reference
61
+ _state.value = status.id
62
+ return status.id
63
+
64
+ def _org_account_exists(
65
+ self,
66
+ aws_api: AWSApi,
67
+ name: str,
68
+ create_account_request_id: str,
69
+ ) -> str | None:
70
+ """Check if the organization account exists and return its ID."""
71
+ with self.state.transaction(state_key(name, TASK_DESCRIBE_ACCOUNT)) as _state:
72
+ if _state.exists:
73
+ # account checked and exists, nothing to do
74
+ return _state.value
75
+
76
+ logging.info(f"Checking account creation status {name}")
77
+ status = aws_api.organizations.describe_create_account_status(
78
+ create_account_request_id=create_account_request_id
79
+ )
80
+ match status.state:
81
+ case "SUCCEEDED":
82
+ _state.value = status.uid
83
+ return status.uid
84
+ case "FAILED":
85
+ raise RuntimeError(
86
+ f"Account creation failed: {status.failure_reason}"
87
+ )
88
+ case "IN_PROGRESS":
89
+ raise AbortStateTransaction("Account creation still in progress")
90
+ case _:
91
+ raise RuntimeError(
92
+ f"Unexpected account creation status: {status.state}"
93
+ )
94
+
95
+ def _tag_account(
96
+ self, aws_api: AWSApi, name: str, uid: str, tags: dict[str, str]
97
+ ) -> None:
98
+ with self.state.transaction(state_key(name, TASK_TAG_ACCOUNT)) as _state:
99
+ if _state.exists and _state.value == tags:
100
+ # account already tagged, nothing to do
101
+ return
102
+
103
+ logging.info(f"Tagging account {name}: {tags}")
104
+ _state.value = tags
105
+ if self.dry_run:
106
+ raise AbortStateTransaction("Dry run")
107
+
108
+ if _state.exists:
109
+ aws_api.organizations.untag_resource(
110
+ resource_id=uid, tag_keys=_state.value.keys()
111
+ )
112
+ aws_api.organizations.tag_resource(resource_id=uid, tags=tags)
113
+
114
+ def _get_destination_ou(
115
+ self, aws_api: AWSApi, destination_path: str
116
+ ) -> AwsOrganizationOU:
117
+ org_tree_root = aws_api.organizations.get_organizational_units_tree()
118
+ return org_tree_root.find(destination_path)
119
+
120
+ def _move_account(self, aws_api: AWSApi, name: str, uid: str, ou: str) -> None:
121
+ with self.state.transaction(state_key(name, TASK_MOVE_ACCOUNT)) as _state:
122
+ if _state.exists and _state.value == ou:
123
+ # account already moved, nothing to do
124
+ return
125
+
126
+ logging.info(f"Moving account {name} to {ou}")
127
+ destination = self._get_destination_ou(aws_api, destination_path=ou)
128
+ if self.dry_run:
129
+ raise AbortStateTransaction("Dry run")
130
+
131
+ aws_api.organizations.move_account(
132
+ uid=uid,
133
+ destination_parent_id=destination.id,
134
+ )
135
+ _state.value = ou
136
+
137
+ def _set_account_alias(self, aws_api: AWSApi, name: str, alias: str | None) -> None:
138
+ """Create an account alias."""
139
+ new_alias = alias or name
140
+ with self.state.transaction(
141
+ state_key(name, TASK_ACCOUNT_ALIAS), new_alias
142
+ ) as _state:
143
+ if _state.exists and _state.value == new_alias:
144
+ return
145
+
146
+ logging.info(f"Set account alias '{new_alias}' for {name}")
147
+ if self.dry_run:
148
+ raise AbortStateTransaction("Dry run")
149
+
150
+ aws_api.iam.set_account_alias(account_alias=new_alias)
151
+
152
+ def _request_quotas(
153
+ self, aws_api: AWSApi, name: str, quotas: Iterable[Quota]
154
+ ) -> list[str] | None:
155
+ """Request service quota changes."""
156
+ quotas_dict = [q.dict() for q in quotas]
157
+ with self.state.transaction(
158
+ state_key(name, TASK_REQUEST_SERVICE_QUOTA)
159
+ ) as _state:
160
+ if _state.exists and _state.value["last_applied_quotas"] == quotas_dict:
161
+ return _state.value["ids"]
162
+
163
+ # ATTENTION: reverting previously applied quotas or lowering them is not supported
164
+ new_quotas = []
165
+ for q in quotas:
166
+ quota = aws_api.service_quotas.get_service_quota(
167
+ service_code=q.service_code, quota_code=q.quota_code
168
+ )
169
+ if quota.value > q.value:
170
+ # a quota can be already higher than requested, because it was may set manually or enforced by the payer account
171
+ logging.info(
172
+ f"Cannot lower quota {q.service_code=}, {q.quota_code=}: {quota.value} -> {q.value}. Skipping."
173
+ )
174
+ elif quota.value < q.value:
175
+ quota.value = q.value
176
+ new_quotas.append(quota)
177
+
178
+ for q in new_quotas:
179
+ logging.info(
180
+ f"Setting quota for {name}: {q.service_name}/{q.quota_name} ({q.service_code}/{q.quota_code}) -> {q.value}"
181
+ )
182
+
183
+ if self.dry_run:
184
+ raise AbortStateTransaction("Dry run")
185
+
186
+ ids = []
187
+ for new_quota in new_quotas:
188
+ try:
189
+ req = aws_api.service_quotas.request_service_quota_change(
190
+ service_code=new_quota.service_code,
191
+ quota_code=new_quota.quota_code,
192
+ desired_value=new_quota.value,
193
+ )
194
+ except AWSResourceAlreadyExistsException:
195
+ raise AbortStateTransaction(
196
+ f"A quota increase for this {new_quota.service_code}/{new_quota.quota_code} already exists. Try it again later."
197
+ )
198
+ ids.append(req.id)
199
+
200
+ _state.value = {"last_applied_quotas": quotas_dict, "ids": ids}
201
+ return ids
202
+
203
+ def _check_quota_change_requests(
204
+ self,
205
+ aws_api: AWSApi,
206
+ name: str,
207
+ request_ids: Iterable[str],
208
+ ) -> None:
209
+ """Check the status of the quota change requests."""
210
+ with self.state.transaction(
211
+ state_key(name, TASK_CHECK_SERVICE_QUOTA_STATUS)
212
+ ) as _state:
213
+ if _state.exists and _state.value == request_ids:
214
+ return
215
+
216
+ logging.info(f"Checking quota change requests for {name}")
217
+ if self.dry_run:
218
+ raise AbortStateTransaction("Dry run")
219
+
220
+ _state.value = []
221
+ for request_id in request_ids:
222
+ req = aws_api.service_quotas.get_requested_service_quota_change(
223
+ request_id=request_id
224
+ )
225
+ match req.status:
226
+ case "CASE_CLOSED" | "APPROVED":
227
+ _state.value.append(request_id)
228
+ case "DENIED" | "INVALID_REQUEST" | "NOT_APPROVED":
229
+ raise RuntimeError(
230
+ f"Quota change request {request_id} failed: {req.status}"
231
+ )
232
+ case _:
233
+ # everything else is considered in progress
234
+ pass
235
+
236
+ def _enable_enterprise_support(
237
+ self, aws_api: AWSApi, name: str, uid: str
238
+ ) -> str | None:
239
+ """Enable enterprise support for the account."""
240
+ with self.state.transaction(
241
+ state_key(name, TASK_ENABLE_ENTERPRISE_SUPPORT), ""
242
+ ) as _state:
243
+ if _state.exists:
244
+ return _state.value
245
+
246
+ if aws_api.support.get_support_level() == SUPPORT_PLAN.ENTERPRISE:
247
+ if self.dry_run:
248
+ raise AbortStateTransaction("Dry run")
249
+ return None
250
+
251
+ logging.info(f"Enabling enterprise support for {name}")
252
+ if self.dry_run:
253
+ raise AbortStateTransaction("Dry run")
254
+
255
+ case_id = aws_api.support.create_case(
256
+ subject=f"Add account {uid} to Enterprise Support",
257
+ message=dedent(f"""
258
+ Hello AWS,
259
+
260
+ Please enable Enterprise Support on AWS account {uid} and resolve this support case.
261
+
262
+ Thanks.
263
+
264
+ [rh-internal-account-name: {name}]
265
+ """),
266
+ )
267
+ _state.value = case_id
268
+ return case_id
269
+
270
+ def _check_enterprise_support_status(self, aws_api: AWSApi, case_id: str) -> None:
271
+ """Check the status of the enterprise support case."""
272
+ with self.state.transaction(
273
+ state_key(case_id, TASK_CHECK_ENTERPRISE_SUPPORT_STATUS), True
274
+ ) as _state:
275
+ if _state.exists:
276
+ return
277
+
278
+ logging.info(f"Checking enterprise support case {case_id}")
279
+ if self.dry_run:
280
+ raise AbortStateTransaction("Dry run")
281
+
282
+ case = aws_api.support.describe_case(case_id=case_id)
283
+ if case.status == "resolved":
284
+ return
285
+
286
+ logging.info(
287
+ f"Enterprise support case {case_id} is still open. Current status: {case.status}"
288
+ )
289
+ raise AbortStateTransaction("Enterprise support case still open")
290
+
291
+ #
292
+ # Public methods
293
+ #
294
+ def create_organization_account(
295
+ self, aws_api: AWSApi, name: str, email: str
296
+ ) -> str | None:
297
+ """Create an organization account and return the creation status ID."""
298
+ if create_account_request_id := self._create_account(aws_api, name, email):
299
+ if uid := self._org_account_exists(
300
+ aws_api, name, create_account_request_id
301
+ ):
302
+ return uid
303
+ return None
304
+
305
+ def create_iam_user(
306
+ self,
307
+ aws_api: AWSApi,
308
+ name: str,
309
+ user_name: str,
310
+ user_policy_arn: str,
311
+ ) -> AWSAccessKey | None:
312
+ """Create an IAM user and return its access key."""
313
+ with self.state.transaction(
314
+ state_key(name, TASK_CREATE_IAM_USER), user_name
315
+ ) as _state:
316
+ if _state.exists and _state.value == user_name:
317
+ return None
318
+
319
+ logging.info(f"Creating IAM user '{user_name}' for {name}")
320
+ if self.dry_run:
321
+ raise AbortStateTransaction("Dry run")
322
+
323
+ aws_api.iam.create_user(user_name=user_name)
324
+ aws_api.iam.attach_user_policy(
325
+ user_name=user_name,
326
+ policy_arn=user_policy_arn,
327
+ )
328
+ return aws_api.iam.create_access_key(user_name=user_name)
329
+
330
+ def reconcile_organization_account(
331
+ self,
332
+ aws_api: AWSApi,
333
+ name: str,
334
+ uid: str,
335
+ ou: str,
336
+ tags: dict[str, str],
337
+ enterprise_support: bool,
338
+ ) -> None:
339
+ """Reconcile the AWS account on the organization level."""
340
+ self._tag_account(aws_api, name, uid, tags)
341
+ self._move_account(aws_api, name, uid, ou)
342
+ if enterprise_support and (
343
+ case_id := self._enable_enterprise_support(aws_api, name, uid)
344
+ ):
345
+ self._check_enterprise_support_status(aws_api, case_id)
346
+
347
+ def reconcile_account(
348
+ self, aws_api: AWSApi, name: str, alias: str | None, quotas: Iterable[Quota]
349
+ ) -> None:
350
+ """Reconcile/update the AWS account. Return the initial user access key if a new user was created."""
351
+ self._set_account_alias(aws_api, name, alias)
352
+ if request_ids := self._request_quotas(aws_api, name, quotas):
353
+ self._check_quota_change_requests(aws_api, name, request_ids)
@@ -0,0 +1,38 @@
1
+ from collections import Counter
2
+
3
+ from reconcile.gql_definitions.aws_account_manager.aws_accounts import (
4
+ AWSAccountV1,
5
+ )
6
+
7
+
8
+ def validate(account: AWSAccountV1) -> bool:
9
+ """Validate the account configurations."""
10
+ # check referenced quotas don't overlap
11
+ quotas = Counter([
12
+ (quota.service_code, quota.quota_code)
13
+ for quota_limit in account.quota_limits or []
14
+ for quota in quota_limit.quotas or []
15
+ ])
16
+ errors = [
17
+ ValueError(
18
+ f"Quota service_code={service_code}, quota_code={quota_code} is referenced multiple times in account {account.name}"
19
+ )
20
+ for (service_code, quota_code), cnt in quotas.items()
21
+ if cnt > 1
22
+ ]
23
+ if errors:
24
+ raise ExceptionGroup("Multiple quotas are referenced in the account", errors)
25
+
26
+ if account.organization_accounts or account.account_requests:
27
+ # it's payer account
28
+ if not account.premium_support:
29
+ raise ValueError(
30
+ f"Premium support is required for payer account {account.name}"
31
+ )
32
+
33
+ return True
34
+
35
+
36
+ def state_key(account: str, task: str) -> str:
37
+ """Compute a state key based on the organization account and the task name."""
38
+ return f"task.{account}.{task}"
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)
@@ -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")