qontract-reconcile 0.10.1rc701__py3-none-any.whl → 0.10.1rc703__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.1rc701.dist-info → qontract_reconcile-0.10.1rc703.dist-info}/METADATA +1 -1
- {qontract_reconcile-0.10.1rc701.dist-info → qontract_reconcile-0.10.1rc703.dist-info}/RECORD +21 -11
- reconcile/aws_account_manager/__init__.py +0 -0
- reconcile/aws_account_manager/integration.py +342 -0
- reconcile/aws_account_manager/merge_request_manager.py +111 -0
- reconcile/aws_account_manager/reconciler.py +353 -0
- reconcile/aws_account_manager/utils.py +38 -0
- reconcile/cli.py +79 -0
- reconcile/gql_definitions/aws_account_manager/__init__.py +0 -0
- reconcile/gql_definitions/aws_account_manager/aws_accounts.py +163 -0
- reconcile/gql_definitions/fragments/aws_account_managed.py +49 -0
- reconcile/ocm_internal_notifications/integration.py +1 -1
- reconcile/utils/aws_api_typed/api.py +49 -6
- reconcile/utils/aws_api_typed/iam.py +22 -7
- reconcile/utils/aws_api_typed/organization.py +78 -30
- reconcile/utils/aws_api_typed/service_quotas.py +79 -0
- reconcile/utils/aws_api_typed/support.py +79 -0
- reconcile/utils/state.py +42 -38
- {qontract_reconcile-0.10.1rc701.dist-info → qontract_reconcile-0.10.1rc703.dist-info}/WHEEL +0 -0
- {qontract_reconcile-0.10.1rc701.dist-info → qontract_reconcile-0.10.1rc703.dist-info}/entry_points.txt +0 -0
- {qontract_reconcile-0.10.1rc701.dist-info → qontract_reconcile-0.10.1rc703.dist-info}/top_level.txt +0 -0
@@ -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):
|
File without changes
|
@@ -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")
|
@@ -63,7 +63,7 @@ class OcmInternalNotifications(QontractReconcileIntegration[NoParams]):
|
|
63
63
|
for env in environments:
|
64
64
|
ocm = init_ocm_base_client(env, self.secret_reader)
|
65
65
|
|
66
|
-
if not (env.labels and env.labels
|
66
|
+
if not (env.labels and env.labels.get("internal_notifications")):
|
67
67
|
logging.info(
|
68
68
|
f"skipping environment {env.name} due to no internal_notifications label"
|
69
69
|
)
|