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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: qontract-reconcile
3
- Version: 0.10.1rc701
3
+ Version: 0.10.1rc703
4
4
  Summary: Collection of tools to reconcile services with their desired state as defined in the app-interface DB.
5
5
  Home-page: https://github.com/app-sre/qontract-reconcile
6
6
  Author: Red Hat App-SRE Team
@@ -9,7 +9,7 @@ reconcile/aws_iam_password_reset.py,sha256=NwErtrqgBiXr7eGCAHdtGGOx0S7-4JnSc29Ie
9
9
  reconcile/aws_support_cases_sos.py,sha256=Jk6_XjDeJSYxgRGqcEAOcynt9qJF2r5HPIPcSKmoBv8,2974
10
10
  reconcile/blackbox_exporter_endpoint_monitoring.py,sha256=W_VJagnsJR1v5oqjlI3RJJE0_nhtJ0m81RS8zWA5u5c,3538
11
11
  reconcile/checkpoint.py,sha256=R2WFXUXLTB4sWMi4GeA4eegsuf_1-Q4vH8M0Toh3Ij4,5036
12
- reconcile/cli.py,sha256=Xj_msd-518yR9r1hy39FI3Q9gBRZQsfM9HRA-Ep6CKY,93661
12
+ reconcile/cli.py,sha256=deAj6fNIYnrEKgOKv2UYZKAvuvEmYVUWj4nDSG6y99U,96070
13
13
  reconcile/closedbox_endpoint_monitoring_base.py,sha256=SMhkcQqprWvThrIJa3U_3uh5w1h-alleW1QnCJFY4Qw,4909
14
14
  reconcile/cluster_deployment_mapper.py,sha256=2Ah-nu-Mdig0pjuiZl_XLrmVAjYzFjORR3dMlCgkmw0,2352
15
15
  reconcile/dashdotdb_base.py,sha256=a5aPLVxyqPSbjdB0Ty-uliOtxwvEbbEljHJKxdK3-Zk,4813
@@ -135,6 +135,11 @@ reconcile/aus/version_gates/handler.py,sha256=S_isQPYHbG4DERiUEvQBZ6ngiFX3uMmATA
135
135
  reconcile/aus/version_gates/ingress_gate_handler.py,sha256=ZCtyggBzzcb0prtdbMpJsVkj5leYN-vS7srM9vbq9xo,1096
136
136
  reconcile/aus/version_gates/ocp_gate_handler.py,sha256=RW1ppDaCZXVegV9AzzqYXxDUu_Z_7d43Z5h2Pk_piKc,716
137
137
  reconcile/aus/version_gates/sts_version_gate_handler.py,sha256=PhJ7yBh2q-rv9CJcfFhc0H11nyDyG7NAryNS3F74xdY,3697
138
+ reconcile/aws_account_manager/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
139
+ reconcile/aws_account_manager/integration.py,sha256=TLlhxnHXRCVz2GYJQei-dBdSpeLseEkoVUwHhgi41fk,13804
140
+ reconcile/aws_account_manager/merge_request_manager.py,sha256=zZct3NxWMBQupl4QfD7ULxnt4ipt_2FBoH_NusboIuw,3781
141
+ reconcile/aws_account_manager/reconciler.py,sha256=AqAA3TIEfuYzIogHSBgwYTebxbTy1D6JhcxdLiOfCsc,13588
142
+ reconcile/aws_account_manager/utils.py,sha256=K4rAjEMK-eQ_Sv4lOf6dPynQy97xZ4h-n6cJn5Z6zVw,1248
138
143
  reconcile/aws_ami_cleanup/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
139
144
  reconcile/aws_ami_cleanup/integration.py,sha256=IW95cpMj2P5ffs-AxsR_TDQCJnYFBhLIfP2de7dz_8A,10109
140
145
  reconcile/aws_cloudwatch_log_retention/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -185,6 +190,8 @@ reconcile/gql_definitions/advanced_upgrade_service/aus_clusters.py,sha256=zrZCHa
185
190
  reconcile/gql_definitions/advanced_upgrade_service/aus_organization.py,sha256=uFx75cLe1a4xyArr6ekoAUbrzSRnhR7S2vaR3G5Fzbw,3299
186
191
  reconcile/gql_definitions/app_interface_metrics_exporter/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
187
192
  reconcile/gql_definitions/app_interface_metrics_exporter/onboarding_status.py,sha256=uVEEqU6YYmKsNTo6EWlFnoVmqha2rvBDx-wiD64VmG0,1679
193
+ reconcile/gql_definitions/aws_account_manager/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
194
+ reconcile/gql_definitions/aws_account_manager/aws_accounts.py,sha256=c3RmQwbHa9UlfGwruufkA8PUfeiRJZ-lXEDInAREZqE,4405
188
195
  reconcile/gql_definitions/aws_ami_cleanup/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
189
196
  reconcile/gql_definitions/aws_ami_cleanup/asg_namespaces.py,sha256=OJmeTu7uirLGAysZ3IQTtRXqMyL8noi_QZxPuWYxxmI,3678
190
197
  reconcile/gql_definitions/aws_saml_idp/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -240,6 +247,7 @@ reconcile/gql_definitions/dynatrace_token_provider/dynatrace_bootstrap_tokens.py
240
247
  reconcile/gql_definitions/fragments/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
241
248
  reconcile/gql_definitions/fragments/aus_organization.py,sha256=ARI87YAbC0VjFri9eVGYrRPBc4s0kWsa25RR8FFoq7E,4433
242
249
  reconcile/gql_definitions/fragments/aws_account_common.py,sha256=d_FwpS_dY8o8DCLa3NERs93FVxQLiDUIPm5tGNac-iw,2320
250
+ reconcile/gql_definitions/fragments/aws_account_managed.py,sha256=zXbux0Bb7QZ37fU4LLMKB4m0rT3Y8bD10C1TuOsH_ZQ,1470
243
251
  reconcile/gql_definitions/fragments/aws_account_sso.py,sha256=ITR3PLz4Iq1SiWAoYGWPDuHJnAmTyZ0QQqs2Zsi8pxA,979
244
252
  reconcile/gql_definitions/fragments/aws_infra_management_account.py,sha256=uAmALVRF2gBM3p_Dmez_ew6KVAtetamwOPkRIPZAlGc,1254
245
253
  reconcile/gql_definitions/fragments/aws_vpc.py,sha256=T2egTwi2Rb0IRBBmsyag8xKpu_m6GbIAy80fhZNZwk8,1434
@@ -347,7 +355,7 @@ reconcile/ldap_groups/integration.py,sha256=iVfR7bRz8up_bf5Q4aVZHuVoUT6U_aHHkcgY
347
355
  reconcile/ocm/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
348
356
  reconcile/ocm/types.py,sha256=ibJYvzfAZyyMFkcF1bP8u3rkXciYJRplt_7Z1pKHFh0,2484
349
357
  reconcile/ocm_internal_notifications/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
350
- reconcile/ocm_internal_notifications/integration.py,sha256=6JFLu6TlcMC1OB5WgTAd-ITlhH_56W5uBytggukNk3A,4391
358
+ reconcile/ocm_internal_notifications/integration.py,sha256=Gw2oB1Oe1Vvbj-fN_undhkQ2y5tCVhUfW5DenKu9ybM,4395
351
359
  reconcile/ocm_labels/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
352
360
  reconcile/ocm_labels/integration.py,sha256=SqIgCYhlHEeoH2YXJ8kWIxCo9S3fzzAlZGxt9KCnJ1s,14792
353
361
  reconcile/ocm_labels/label_sources.py,sha256=z5Rg5uMTfRkTi3QbEQK1P82LLciWrSnp65XBEpZf2-o,1877
@@ -623,7 +631,7 @@ reconcile/utils/sharding.py,sha256=gkYf0lD3IUKQPEmdRJZ70mdDT1c9qWjbdP7evRsUis4,8
623
631
  reconcile/utils/slack_api.py,sha256=OPmzU6L9rJx2XXDlZkMlxLjOWu17yC-fVCoUItzQrXw,16295
624
632
  reconcile/utils/smtp_client.py,sha256=gJNbBQJpAt5PX4t_TaeNHsXM8vt50bFgndml6yK2b5o,2800
625
633
  reconcile/utils/sqs_gateway.py,sha256=gFl9DM4DmGnptuxTOe4lS3YTyE80eSAvK42ljS8h4dA,2287
626
- reconcile/utils/state.py,sha256=zjsprjbOb0WddzmAvh8ACqAt0fcayrX2YPfz7qceRWw,16090
634
+ reconcile/utils/state.py,sha256=FK8NLT1xyumuXpYRm0Nk6pWpOE_U6-NovGn6zKCw8vw,16298
627
635
  reconcile/utils/structs.py,sha256=LcbLEg8WxfRqM6nW7NhcWN0YeqF7SQzxOgntmLs1SgY,352
628
636
  reconcile/utils/template.py,sha256=wTvRU4AnAV_o042tD4Mwls2dwWMuk7MKnde3MaCjaYg,331
629
637
  reconcile/utils/terraform_client.py,sha256=mZEKpu6nbfiQd60wRkc8-5sljBTUTOgaAKnF89itMzU,32085
@@ -639,10 +647,12 @@ reconcile/utils/acs/base.py,sha256=Qih-xZ3RBJZEE291iHHlv7lUY6ShcAvSj1PA3_aTTnM,2
639
647
  reconcile/utils/acs/policies.py,sha256=_jAz6cv8KRYtDsXjGoJgNbD8_9PUa5LSwwVlpK4A_cQ,5505
640
648
  reconcile/utils/acs/rbac.py,sha256=ugsLM9Pb7FbUbdq85E3VzXGMaB9ZovXob7tdWCxwqZ8,8808
641
649
  reconcile/utils/aws_api_typed/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
642
- reconcile/utils/aws_api_typed/api.py,sha256=ICBrpoZ1Y8ShMaNOQmzPzChmw-Ffdg0tCsu97fVwZ-w,6374
643
- reconcile/utils/aws_api_typed/iam.py,sha256=wH82lA2kUgEKR5McmyU5gB8ASfemT5xAk3Bb9cairVo,1508
644
- reconcile/utils/aws_api_typed/organization.py,sha256=dJ7J02BNHu7UDyFa9083b92vSamIX8DtHIoF8VwEijY,3675
650
+ reconcile/utils/aws_api_typed/api.py,sha256=gqfZISSQLp6tHEAwEsroLWwyU4ZdbwHi9p0rNBQyLuI,7901
651
+ reconcile/utils/aws_api_typed/iam.py,sha256=ka46H2-SzTCgy6EJYapKTzyZK9vR1bkfD0wF8bDdy1Q,2201
652
+ reconcile/utils/aws_api_typed/organization.py,sha256=oXftcLVuSs9qej6efdssl38FvjeZaQC5R2Wj3NzxX4U,5529
653
+ reconcile/utils/aws_api_typed/service_quotas.py,sha256=OU1D8LCmMw1IT87nt45LqXhguzcWwC8AaBdDTI7tz98,3018
645
654
  reconcile/utils/aws_api_typed/sts.py,sha256=5Sauncj9Fif3YDLkJYkBZrtOX0v0bGAqOmY0A5Bh9yA,1237
655
+ reconcile/utils/aws_api_typed/support.py,sha256=PH3UW96Ne4_8I1J-_Vqj-DsK73gYGeVdOH13eD1783c,2447
646
656
  reconcile/utils/cloud_resource_best_practice/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
647
657
  reconcile/utils/cloud_resource_best_practice/aws_rds.py,sha256=EvE6XKLsrZ531MJptKqPht2lOETrOjySTHXk6CzMgo0,2279
648
658
  reconcile/utils/clusterhealth/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -743,8 +753,8 @@ tools/test/test_app_interface_metrics_exporter.py,sha256=SX7qL3D1SIRKFo95FoQztvf
743
753
  tools/test/test_qontract_cli.py,sha256=UEwAW7PA_GIrbqzaLxpkCxbuVjEFLNvnVG-6VyoCGIc,4147
744
754
  tools/test/test_sd_app_sre_alert_report.py,sha256=v363r9zM7__0kR5K6mvJoGFcM9BvE33fWAayrqkpojA,2116
745
755
  tools/test/test_sre_checkpoints.py,sha256=SKqPPTl9ua0RFdSSofnoQX-JZE6dFLO3LRhfQzqtfh8,2607
746
- qontract_reconcile-0.10.1rc701.dist-info/METADATA,sha256=w6ee8LZoHXwgtGVu5XQXmwdQXUO1KINVucxNqyRJEKc,2382
747
- qontract_reconcile-0.10.1rc701.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
748
- qontract_reconcile-0.10.1rc701.dist-info/entry_points.txt,sha256=rIxI5zWtHNlfpDeq1a7pZXAPoqf7HG32KMTN3MeWK_8,429
749
- qontract_reconcile-0.10.1rc701.dist-info/top_level.txt,sha256=l5ISPoXzt0SdR4jVdkfa7RPSKNc8zAHYWAnR-Dw8Ey8,24
750
- qontract_reconcile-0.10.1rc701.dist-info/RECORD,,
756
+ qontract_reconcile-0.10.1rc703.dist-info/METADATA,sha256=yoAayu5zHJZhZK0IqR21e6WKXEMXcnLSXPoQ9gd52a8,2382
757
+ qontract_reconcile-0.10.1rc703.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
758
+ qontract_reconcile-0.10.1rc703.dist-info/entry_points.txt,sha256=rIxI5zWtHNlfpDeq1a7pZXAPoqf7HG32KMTN3MeWK_8,429
759
+ qontract_reconcile-0.10.1rc703.dist-info/top_level.txt,sha256=l5ISPoXzt0SdR4jVdkfa7RPSKNc8zAHYWAnR-Dw8Ey8,24
760
+ qontract_reconcile-0.10.1rc703.dist-info/RECORD,,
File without changes
@@ -0,0 +1,342 @@
1
+ from collections.abc import Callable, Iterable
2
+ from datetime import datetime, timezone
3
+ from typing import Any
4
+
5
+ import jinja2
6
+
7
+ from reconcile.aws_account_manager.merge_request_manager import MergeRequestManager
8
+ from reconcile.aws_account_manager.reconciler import AWSReconciler
9
+ from reconcile.aws_account_manager.utils import validate
10
+ from reconcile.gql_definitions.aws_account_manager.aws_accounts import (
11
+ AWSAccountManaged,
12
+ AWSAccountRequestV1,
13
+ AWSAccountV1,
14
+ )
15
+ from reconcile.gql_definitions.aws_account_manager.aws_accounts import (
16
+ query as aws_accounts_query,
17
+ )
18
+ from reconcile.typed_queries.app_interface_repo_url import get_app_interface_repo_url
19
+ from reconcile.typed_queries.github_orgs import get_github_orgs
20
+ from reconcile.typed_queries.gitlab_instances import get_gitlab_instances
21
+ from reconcile.utils import gql
22
+ from reconcile.utils.aws_api_typed.api import AWSApi, AWSStaticCredentials
23
+ from reconcile.utils.aws_api_typed.iam import AWSAccessKey
24
+ from reconcile.utils.defer import defer
25
+ from reconcile.utils.disabled_integrations import integration_is_enabled
26
+ from reconcile.utils.runtime.integration import (
27
+ PydanticRunParams,
28
+ QontractReconcileIntegration,
29
+ )
30
+ from reconcile.utils.semver_helper import make_semver
31
+ from reconcile.utils.state import init_state
32
+ from reconcile.utils.unleash import get_feature_toggle_state
33
+ from reconcile.utils.vcs import VCS
34
+
35
+ QONTRACT_INTEGRATION = "aws-account-manager"
36
+ QONTRACT_INTEGRATION_VERSION = make_semver(1, 0, 0)
37
+
38
+
39
+ class AwsAccountMgmtIntegrationParams(PydanticRunParams):
40
+ account_name: str | None
41
+ flavor: str
42
+ organization_account_role: str = "OrganizationAccountAccessRole"
43
+ default_tags: dict[str, str] = {}
44
+ initial_user_name: str = "terraform"
45
+ initial_user_policy_arn: str = "arn:aws:iam::aws:policy/AdministratorAccess"
46
+ initial_user_secret_vault_path: str = (
47
+ "app-sre/creds/terraform/{account_name}/config"
48
+ )
49
+ account_tmpl_resource: str = "/aws-account-manager/account-tmpl.yml"
50
+ template_collection_root_path: str = "data/templating/collections/aws-account"
51
+
52
+
53
+ class AwsAccountMgmtIntegration(
54
+ QontractReconcileIntegration[AwsAccountMgmtIntegrationParams]
55
+ ):
56
+ """Create and manage AWS accounts."""
57
+
58
+ @property
59
+ def name(self) -> str:
60
+ return QONTRACT_INTEGRATION
61
+
62
+ def get_early_exit_desired_state(
63
+ self, query_func: Callable | None = None
64
+ ) -> dict[str, Any]:
65
+ """Return the desired state for early exit."""
66
+ if not query_func:
67
+ query_func = gql.get_api().query
68
+ payer_accounts, non_organization_accounts = self.get_aws_accounts(
69
+ query_func, account_name=self.params.account_name
70
+ )
71
+ return {
72
+ "payer_accounts": [account.dict() for account in payer_accounts],
73
+ "non_organization_accounts": [
74
+ account.dict() for account in non_organization_accounts
75
+ ],
76
+ }
77
+
78
+ @staticmethod
79
+ def render_account_tmpl_file(
80
+ template: str, account_request: AWSAccountRequestV1, uid: str, settings: dict
81
+ ) -> str:
82
+ for k, v in settings.items():
83
+ if not isinstance(v, str):
84
+ continue
85
+ # render string templates with account name
86
+ settings[k] = v.format(account_name=account_request.name)
87
+ tmpl = jinja2.Template(
88
+ template,
89
+ undefined=jinja2.StrictUndefined,
90
+ trim_blocks=True,
91
+ lstrip_blocks=True,
92
+ keep_trailing_newline=True,
93
+ ).render({
94
+ "accountRequest": account_request.dict(by_alias=True),
95
+ "uid": uid,
96
+ "settings": settings,
97
+ "timestamp": int(datetime.now(tz=timezone.utc).timestamp()),
98
+ })
99
+ return tmpl
100
+
101
+ def get_aws_accounts(
102
+ self, query_func: Callable, account_name: str | None = None
103
+ ) -> tuple[list[AWSAccountV1], list[AWSAccountV1]]:
104
+ """Get all AWS payer and non-organization accounts."""
105
+ data = aws_accounts_query(query_func)
106
+
107
+ all_aws_accounts = [
108
+ account
109
+ for account in data.accounts or []
110
+ if integration_is_enabled(self.name, account)
111
+ and (not account_name or account.name == account_name)
112
+ ]
113
+ for account in all_aws_accounts:
114
+ validate(account)
115
+
116
+ payer_accounts = [
117
+ account
118
+ for account in all_aws_accounts
119
+ if account.organization_accounts or account.account_requests
120
+ ]
121
+ all_organization_account_names = {
122
+ org_account.name
123
+ for payer_account in payer_accounts
124
+ for org_account in payer_account.organization_accounts or []
125
+ }
126
+
127
+ non_organization_accounts = [
128
+ account
129
+ for account in all_aws_accounts
130
+ if account.name not in all_organization_account_names
131
+ ]
132
+ return payer_accounts, non_organization_accounts
133
+
134
+ def save_access_key(self, account: str, access_key: AWSAccessKey) -> None:
135
+ """Write the AWS secret to Vault."""
136
+ self.secret_reader.vault_client.write( # type: ignore[attr-defined] # mypy doesn't recognize the VaultClient.__new__ method
137
+ secret={
138
+ "data": {
139
+ "aws_access_key_id": access_key.access_key_id,
140
+ "aws_secret_access_key": access_key.secret_access_key,
141
+ },
142
+ "path": self.params.initial_user_secret_vault_path.format(
143
+ account_name=account
144
+ ).strip("/"),
145
+ },
146
+ decode_base64=False,
147
+ )
148
+
149
+ def create_accounts(
150
+ self,
151
+ aws_api: AWSApi,
152
+ reconciler: AWSReconciler,
153
+ merge_request_manager: MergeRequestManager,
154
+ account_template: str,
155
+ account_requests: Iterable[AWSAccountRequestV1],
156
+ ) -> None:
157
+ """Create new AWS accounts."""
158
+ for account_request in account_requests:
159
+ if not (
160
+ uid := reconciler.create_organization_account(
161
+ aws_api=aws_api,
162
+ name=account_request.name,
163
+ email=account_request.account_owner.email,
164
+ )
165
+ ):
166
+ continue
167
+
168
+ with aws_api.assume_role(
169
+ account_id=uid, role=self.params.organization_account_role
170
+ ) as account_role_api:
171
+ if access_key := reconciler.create_iam_user(
172
+ aws_api=account_role_api,
173
+ name=account_request.name,
174
+ user_name=self.params.initial_user_name,
175
+ user_policy_arn=self.params.initial_user_policy_arn,
176
+ ):
177
+ self.save_access_key(account_request.name, access_key)
178
+
179
+ merge_request_manager.create_account_file(
180
+ title=f"{account_request.name}: AWS account template collection file",
181
+ account_tmpl_file_path=f"{self.params.template_collection_root_path}/{account_request.name}.yml",
182
+ account_tmpl_file_content=self.render_account_tmpl_file(
183
+ template=account_template,
184
+ account_request=account_request,
185
+ uid=uid,
186
+ settings=self.params.dict(),
187
+ ),
188
+ account_request_file_path=f"data/{account_request.path.strip('/')}",
189
+ )
190
+
191
+ def reconcile_organization_accounts(
192
+ self,
193
+ aws_api: AWSApi,
194
+ reconciler: AWSReconciler,
195
+ organization_accounts: Iterable[AWSAccountManaged],
196
+ ) -> None:
197
+ """Reconcile organization accounts."""
198
+ for account in organization_accounts:
199
+ assert account.organization # mypy
200
+ reconciler.reconcile_organization_account(
201
+ aws_api=aws_api,
202
+ name=account.name,
203
+ uid=account.uid,
204
+ ou=account.organization.ou,
205
+ tags=self.params.default_tags
206
+ | account.organization.tags
207
+ | {"app-interface-name": account.name},
208
+ enterprise_support=account.premium_support,
209
+ )
210
+
211
+ with aws_api.assume_role(
212
+ account_id=account.uid, role=self.params.organization_account_role
213
+ ) as account_role_api:
214
+ self.reconcile_account(account_role_api, reconciler, account)
215
+
216
+ def reconcile_account(
217
+ self,
218
+ aws_api: AWSApi,
219
+ reconciler: AWSReconciler,
220
+ account: AWSAccountManaged,
221
+ create_initial_user: bool = True,
222
+ ) -> None:
223
+ """Reconcile an AWS account."""
224
+ reconciler.reconcile_account(
225
+ aws_api=aws_api,
226
+ name=account.name,
227
+ alias=account.alias,
228
+ quotas=[q for ql in account.quota_limits or [] for q in ql.quotas],
229
+ )
230
+
231
+ def reconcile_payer_accounts(
232
+ self,
233
+ reconciler: AWSReconciler,
234
+ merge_request_manager: MergeRequestManager,
235
+ default_state_path: str,
236
+ account_template: str,
237
+ payer_accounts: Iterable[AWSAccountV1],
238
+ ) -> None:
239
+ """Reconcile all payer accounts including account creation."""
240
+ # reconcile accounts within payer accounts, aka organization accounts
241
+ for payer_account in payer_accounts:
242
+ # having a state per flavor and payer account makes it easier in a shared environment
243
+ reconciler.state.state_path = default_state_path
244
+ reconciler.state.state_path += f"/{payer_account.name}"
245
+ aws_account_manager_role = (
246
+ payer_account.automation_role.aws_account_manager
247
+ if payer_account.automation_role
248
+ else None
249
+ )
250
+ if not aws_account_manager_role:
251
+ raise ValueError(
252
+ f"awsAccountManager role is not defined for account {payer_account.name}"
253
+ )
254
+
255
+ secret = self.secret_reader.read_all_secret(payer_account.automation_token)
256
+ with AWSApi(
257
+ AWSStaticCredentials(
258
+ access_key_id=secret["aws_access_key_id"],
259
+ secret_access_key=secret["aws_secret_access_key"],
260
+ region=payer_account.resources_default_region,
261
+ )
262
+ ) as payer_account_aws_api:
263
+ with payer_account_aws_api.assume_role(
264
+ account_id=payer_account.uid,
265
+ role=aws_account_manager_role,
266
+ ) as acct_manager_role_aws_api:
267
+ self.create_accounts(
268
+ acct_manager_role_aws_api,
269
+ reconciler,
270
+ merge_request_manager,
271
+ account_template,
272
+ payer_account.account_requests or [],
273
+ )
274
+ self.reconcile_organization_accounts(
275
+ acct_manager_role_aws_api,
276
+ reconciler,
277
+ payer_account.organization_accounts or [],
278
+ )
279
+
280
+ def reconcile_non_organization_accounts(
281
+ self,
282
+ reconciler: AWSReconciler,
283
+ default_state_path: str,
284
+ non_organization_accounts: Iterable[AWSAccountV1],
285
+ ) -> None:
286
+ """Reconcile accounts not part of an organization via a payer account (e.g. payer accounts themselves)"""
287
+ for account in non_organization_accounts:
288
+ reconciler.state.state_path = default_state_path
289
+ reconciler.state.state_path += f"/{account.name}"
290
+ secret = self.secret_reader.read_all_secret(account.automation_token)
291
+ with AWSApi(
292
+ AWSStaticCredentials(
293
+ access_key_id=secret["aws_access_key_id"],
294
+ secret_access_key=secret["aws_secret_access_key"],
295
+ region=account.resources_default_region,
296
+ )
297
+ ) as account_aws_api:
298
+ self.reconcile_account(account_aws_api, reconciler, account)
299
+
300
+ @defer
301
+ def run(self, dry_run: bool, defer: Callable | None = None) -> None:
302
+ """Run the integration."""
303
+ gql_api = gql.get_api()
304
+ payer_accounts, non_organization_accounts = self.get_aws_accounts(
305
+ gql_api.query, account_name=self.params.account_name
306
+ )
307
+ state = init_state(self.name, self.secret_reader)
308
+ default_state_path = f"state/{self.name}/{self.params.flavor}"
309
+ reconciler = AWSReconciler(state, dry_run)
310
+ vcs = VCS(
311
+ secret_reader=self.secret_reader,
312
+ github_orgs=get_github_orgs(),
313
+ gitlab_instances=get_gitlab_instances(),
314
+ app_interface_repo_url=get_app_interface_repo_url(),
315
+ dry_run=dry_run,
316
+ allow_deleting_mrs=False,
317
+ allow_opening_mrs=True,
318
+ )
319
+ if defer:
320
+ defer(vcs.cleanup)
321
+ merge_request_manager = MergeRequestManager(
322
+ vcs=vcs,
323
+ auto_merge_enabled=get_feature_toggle_state(
324
+ integration_name=f"{self.name}-allow-auto-merge-mrs", default=False
325
+ ),
326
+ )
327
+ merge_request_manager.fetch_open_merge_requests()
328
+ account_template = gql_api.get_resource(path=self.params.account_tmpl_resource)[
329
+ "content"
330
+ ]
331
+ self.reconcile_payer_accounts(
332
+ reconciler=reconciler,
333
+ merge_request_manager=merge_request_manager,
334
+ default_state_path=default_state_path,
335
+ account_template=account_template,
336
+ payer_accounts=payer_accounts,
337
+ )
338
+ self.reconcile_non_organization_accounts(
339
+ reconciler=reconciler,
340
+ default_state_path=default_state_path,
341
+ non_organization_accounts=non_organization_accounts,
342
+ )
@@ -0,0 +1,111 @@
1
+ import logging
2
+
3
+ from gitlab.exceptions import GitlabGetError
4
+ from gitlab.v4.objects import ProjectMergeRequest
5
+
6
+ from reconcile.utils.gitlab_api import GitLabApi
7
+ from reconcile.utils.mr.base import MergeRequestBase
8
+ from reconcile.utils.mr.labels import AUTO_MERGE
9
+ from reconcile.utils.vcs import VCS
10
+
11
+ AWS_MGR = "aws-account-manager"
12
+
13
+
14
+ class AwsAccountMR(MergeRequestBase):
15
+ name = "AwsAccount"
16
+
17
+ def __init__(
18
+ self,
19
+ title: str,
20
+ description: str,
21
+ account_tmpl_file_path: str,
22
+ account_tmpl_file_content: str,
23
+ account_request_file_path: str,
24
+ labels: list[str],
25
+ ):
26
+ super().__init__()
27
+ self._title = title
28
+ self._description = description
29
+ self._account_tmpl_file_path = account_tmpl_file_path.lstrip("/")
30
+ self._account_tmpl_file_content = account_tmpl_file_content
31
+ self._account_request_file_path = account_request_file_path.lstrip("/")
32
+ self.labels = labels
33
+
34
+ @property
35
+ def title(self) -> str:
36
+ return self._title
37
+
38
+ @property
39
+ def description(self) -> str:
40
+ return self._description
41
+
42
+ def process(self, gitlab_cli: GitLabApi) -> None:
43
+ gitlab_cli.create_file(
44
+ branch_name=self.branch,
45
+ file_path=self._account_tmpl_file_path,
46
+ commit_message="add account template file",
47
+ content=self._account_tmpl_file_content,
48
+ )
49
+ gitlab_cli.delete_file(
50
+ branch_name=self.branch,
51
+ file_path=self._account_request_file_path,
52
+ commit_message="delete account request file",
53
+ )
54
+
55
+
56
+ class MergeRequestManager:
57
+ """Manager for AWS account merge requests."""
58
+
59
+ def __init__(self, vcs: VCS, auto_merge_enabled: bool):
60
+ self._open_mrs: list[ProjectMergeRequest] = []
61
+ self._vcs = vcs
62
+ self._auto_merge_enabled = auto_merge_enabled
63
+
64
+ def _merge_request_already_exists(self, aws_acccount_file_path: str) -> bool:
65
+ return any(
66
+ aws_acccount_file_path == diff["new_path"]
67
+ for mr in self._open_mrs
68
+ for diff in mr.changes()["changes"]
69
+ )
70
+
71
+ def fetch_open_merge_requests(self) -> None:
72
+ all_open_mrs = self._vcs.get_open_app_interface_merge_requests()
73
+ self._open_mrs = [mr for mr in all_open_mrs if AWS_MGR in mr.labels]
74
+
75
+ def create_account_file(
76
+ self,
77
+ title: str,
78
+ account_tmpl_file_path: str,
79
+ account_tmpl_file_content: str,
80
+ account_request_file_path: str,
81
+ ) -> None:
82
+ """Open new MR (if not already present) for an AWS account and remove the account request file."""
83
+ if self._merge_request_already_exists(account_tmpl_file_path):
84
+ return None
85
+
86
+ try:
87
+ self._vcs.get_file_content_from_app_interface_master(
88
+ file_path=account_tmpl_file_path
89
+ )
90
+ # File already exists
91
+ raise FileExistsError(
92
+ f"File {account_tmpl_file_path} already exists in the repository"
93
+ )
94
+ except GitlabGetError as e:
95
+ if e.response_code != 404:
96
+ raise e
97
+
98
+ logging.info("Open MR for %s", account_tmpl_file_path)
99
+ mr_labels = [AWS_MGR]
100
+ if self._auto_merge_enabled:
101
+ mr_labels.append(AUTO_MERGE)
102
+ self._vcs.open_app_interface_merge_request(
103
+ mr=AwsAccountMR(
104
+ title=title,
105
+ description=f"New AWS account template collection file {account_tmpl_file_path}",
106
+ account_tmpl_file_path=account_tmpl_file_path,
107
+ account_tmpl_file_content=account_tmpl_file_content,
108
+ account_request_file_path=account_request_file_path,
109
+ labels=mr_labels,
110
+ )
111
+ )