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
{qontract_reconcile-0.10.1rc701.dist-info → qontract_reconcile-0.10.1rc703.dist-info}/METADATA
RENAMED
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: qontract-reconcile
|
3
|
-
Version: 0.10.
|
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
|
{qontract_reconcile-0.10.1rc701.dist-info → qontract_reconcile-0.10.1rc703.dist-info}/RECORD
RENAMED
@@ -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=
|
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=
|
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=
|
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=
|
643
|
-
reconcile/utils/aws_api_typed/iam.py,sha256=
|
644
|
-
reconcile/utils/aws_api_typed/organization.py,sha256=
|
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.
|
747
|
-
qontract_reconcile-0.10.
|
748
|
-
qontract_reconcile-0.10.
|
749
|
-
qontract_reconcile-0.10.
|
750
|
-
qontract_reconcile-0.10.
|
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
|
+
)
|