qontract-reconcile 0.10.1rc602__py3-none-any.whl → 0.10.1rc604__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.1rc602.dist-info → qontract_reconcile-0.10.1rc604.dist-info}/METADATA +2 -2
- {qontract_reconcile-0.10.1rc602.dist-info → qontract_reconcile-0.10.1rc604.dist-info}/RECORD +11 -6
- reconcile/utils/aws_api.py +0 -114
- reconcile/utils/aws_api_typed/__init__.py +0 -0
- reconcile/utils/aws_api_typed/api.py +190 -0
- reconcile/utils/aws_api_typed/iam.py +50 -0
- reconcile/utils/aws_api_typed/organization.py +104 -0
- reconcile/utils/aws_api_typed/sts.py +36 -0
- {qontract_reconcile-0.10.1rc602.dist-info → qontract_reconcile-0.10.1rc604.dist-info}/WHEEL +0 -0
- {qontract_reconcile-0.10.1rc602.dist-info → qontract_reconcile-0.10.1rc604.dist-info}/entry_points.txt +0 -0
- {qontract_reconcile-0.10.1rc602.dist-info → qontract_reconcile-0.10.1rc604.dist-info}/top_level.txt +0 -0
{qontract_reconcile-0.10.1rc602.dist-info → qontract_reconcile-0.10.1rc604.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.1rc604
|
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
|
@@ -11,7 +11,7 @@ Classifier: Programming Language :: Python
|
|
11
11
|
Classifier: Programming Language :: Python :: 3
|
12
12
|
Classifier: Programming Language :: Python :: 3.11
|
13
13
|
Requires-Python: >=3.11
|
14
|
-
Requires-Dist: sretoolbox ~=2.5.
|
14
|
+
Requires-Dist: sretoolbox ~=2.5.2
|
15
15
|
Requires-Dist: Click <9.0,>=7.0
|
16
16
|
Requires-Dist: gql ==3.1.0
|
17
17
|
Requires-Dist: toml <0.11.0,>=0.10.0
|
{qontract_reconcile-0.10.1rc602.dist-info → qontract_reconcile-0.10.1rc604.dist-info}/RECORD
RENAMED
@@ -539,7 +539,7 @@ reconcile/typed_queries/terraform_tgw_attachments/aws_accounts.py,sha256=T5HSeyB
|
|
539
539
|
reconcile/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
540
540
|
reconcile/utils/aggregated_list.py,sha256=pkYoBj7WwmaNgEefETqEOFTnQMcUzHE3mdsVdzGYj60,3372
|
541
541
|
reconcile/utils/amtool.py,sha256=JV5-to_e_FaIcvJWTKYA9d6L3LwzwijM0MjUWn83eD4,2204
|
542
|
-
reconcile/utils/aws_api.py,sha256=
|
542
|
+
reconcile/utils/aws_api.py,sha256=sgmxCXYle0jjmMHPpBOAKl-kWTDWwuc0AlLO2NmDB8M,65612
|
543
543
|
reconcile/utils/aws_helper.py,sha256=6Nfgsz0aQ97LBAJ0JBRdnPaFTAkEBSqXvCH6_pVIWdw,2006
|
544
544
|
reconcile/utils/binary.py,sha256=3IBnwjKakHM367skPPvG6yVSQYjKt5muQlFNdoa63DU,2352
|
545
545
|
reconcile/utils/config.py,sha256=aId5zrPjM_84u_T4yTRE_Psu3zo5-5_JCR6_7Wgv5UQ,990
|
@@ -618,6 +618,11 @@ reconcile/utils/acs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSu
|
|
618
618
|
reconcile/utils/acs/base.py,sha256=Qih-xZ3RBJZEE291iHHlv7lUY6ShcAvSj1PA3_aTTnM,2276
|
619
619
|
reconcile/utils/acs/policies.py,sha256=_jAz6cv8KRYtDsXjGoJgNbD8_9PUa5LSwwVlpK4A_cQ,5505
|
620
620
|
reconcile/utils/acs/rbac.py,sha256=ugsLM9Pb7FbUbdq85E3VzXGMaB9ZovXob7tdWCxwqZ8,8808
|
621
|
+
reconcile/utils/aws_api_typed/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
622
|
+
reconcile/utils/aws_api_typed/api.py,sha256=ICBrpoZ1Y8ShMaNOQmzPzChmw-Ffdg0tCsu97fVwZ-w,6374
|
623
|
+
reconcile/utils/aws_api_typed/iam.py,sha256=wH82lA2kUgEKR5McmyU5gB8ASfemT5xAk3Bb9cairVo,1508
|
624
|
+
reconcile/utils/aws_api_typed/organization.py,sha256=dJ7J02BNHu7UDyFa9083b92vSamIX8DtHIoF8VwEijY,3675
|
625
|
+
reconcile/utils/aws_api_typed/sts.py,sha256=5Sauncj9Fif3YDLkJYkBZrtOX0v0bGAqOmY0A5Bh9yA,1237
|
621
626
|
reconcile/utils/cloud_resource_best_practice/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
622
627
|
reconcile/utils/cloud_resource_best_practice/aws_rds.py,sha256=EvE6XKLsrZ531MJptKqPht2lOETrOjySTHXk6CzMgo0,2279
|
623
628
|
reconcile/utils/glitchtip/__init__.py,sha256=FT6iBhGqoe7KExFdbgL8AYUb64iW_4snF5__Dcl7yt0,258
|
@@ -704,8 +709,8 @@ tools/test/test_app_interface_metrics_exporter.py,sha256=SX7qL3D1SIRKFo95FoQztvf
|
|
704
709
|
tools/test/test_qontract_cli.py,sha256=OvalpVRfY4pNmpMaWHHYqBjV68b1eGQjX8SCyTAXb1w,3501
|
705
710
|
tools/test/test_sd_app_sre_alert_report.py,sha256=v363r9zM7__0kR5K6mvJoGFcM9BvE33fWAayrqkpojA,2116
|
706
711
|
tools/test/test_sre_checkpoints.py,sha256=SKqPPTl9ua0RFdSSofnoQX-JZE6dFLO3LRhfQzqtfh8,2607
|
707
|
-
qontract_reconcile-0.10.
|
708
|
-
qontract_reconcile-0.10.
|
709
|
-
qontract_reconcile-0.10.
|
710
|
-
qontract_reconcile-0.10.
|
711
|
-
qontract_reconcile-0.10.
|
712
|
+
qontract_reconcile-0.10.1rc604.dist-info/METADATA,sha256=8DqZZvzBIt5WiKjrw88w6gjkqWMvmmFJteu3Hskyuyo,2349
|
713
|
+
qontract_reconcile-0.10.1rc604.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
|
714
|
+
qontract_reconcile-0.10.1rc604.dist-info/entry_points.txt,sha256=rTjAv28I_CHLM8ID3OPqMI_suoQ9s7tFbim4aYjn9kk,376
|
715
|
+
qontract_reconcile-0.10.1rc604.dist-info/top_level.txt,sha256=l5ISPoXzt0SdR4jVdkfa7RPSKNc8zAHYWAnR-Dw8Ey8,24
|
716
|
+
qontract_reconcile-0.10.1rc604.dist-info/RECORD,,
|
reconcile/utils/aws_api.py
CHANGED
@@ -1,9 +1,7 @@
|
|
1
1
|
import logging
|
2
2
|
import os
|
3
3
|
import re
|
4
|
-
import textwrap
|
5
4
|
import time
|
6
|
-
from abc import ABC, abstractmethod
|
7
5
|
from collections.abc import (
|
8
6
|
Iterable,
|
9
7
|
Iterator,
|
@@ -89,118 +87,6 @@ KeyStatus = Union[Literal["Active"], Literal["Inactive"]]
|
|
89
87
|
GOVCLOUD_PARTITION = "aws-us-gov"
|
90
88
|
|
91
89
|
|
92
|
-
class AWSCredentials(ABC):
|
93
|
-
@abstractmethod
|
94
|
-
def as_env_vars(self) -> dict[str, str]:
|
95
|
-
"""
|
96
|
-
Returns a dictionary of environment variables that can be used to authenticate with AWS.
|
97
|
-
"""
|
98
|
-
...
|
99
|
-
|
100
|
-
@abstractmethod
|
101
|
-
def as_credentials_file(self, profile_name: str = "default") -> str:
|
102
|
-
"""
|
103
|
-
Returns a string that can be used to write an AWS credentials file.
|
104
|
-
"""
|
105
|
-
...
|
106
|
-
|
107
|
-
@abstractmethod
|
108
|
-
def build_session(self) -> Session:
|
109
|
-
"""
|
110
|
-
Builds an AWS session using these credentials.
|
111
|
-
"""
|
112
|
-
...
|
113
|
-
|
114
|
-
def get_temporary_credentials(
|
115
|
-
self, duration_seconds: int = 900
|
116
|
-
) -> "AWSTemporaryCredentials":
|
117
|
-
"""
|
118
|
-
Builds temporary AWS credentials from a session. This is similar to assuming a role,
|
119
|
-
in the sense that the credentials will expire after a certain amount of time.
|
120
|
-
|
121
|
-
These temporary credentials have the same permissions as the session they were built from, except:
|
122
|
-
- they can't be used for anything IAM related
|
123
|
-
- for the STS API only the AssumeRole and GetSessionToken actions are allowed
|
124
|
-
"""
|
125
|
-
session = self.build_session()
|
126
|
-
response = session.client("sts").get_session_token(
|
127
|
-
DurationSeconds=duration_seconds
|
128
|
-
)
|
129
|
-
tmp_creds = response["Credentials"]
|
130
|
-
return AWSTemporaryCredentials(
|
131
|
-
access_key_id=tmp_creds["AccessKeyId"],
|
132
|
-
secret_access_key=tmp_creds["SecretAccessKey"],
|
133
|
-
session_token=tmp_creds["SessionToken"],
|
134
|
-
region=session.region_name,
|
135
|
-
)
|
136
|
-
|
137
|
-
|
138
|
-
class AWSStaticCredentials(BaseModel, AWSCredentials):
|
139
|
-
"""
|
140
|
-
A model representing AWS credentials.
|
141
|
-
"""
|
142
|
-
|
143
|
-
access_key_id: str
|
144
|
-
secret_access_key: str
|
145
|
-
region: str
|
146
|
-
|
147
|
-
def as_env_vars(self) -> dict[str, str]:
|
148
|
-
return {
|
149
|
-
"AWS_ACCESS_KEY_ID": self.access_key_id,
|
150
|
-
"AWS_SECRET_ACCESS_KEY": self.secret_access_key,
|
151
|
-
"AWS_REGION": self.region,
|
152
|
-
}
|
153
|
-
|
154
|
-
def as_credentials_file(self, profile_name: str = "default") -> str:
|
155
|
-
return textwrap.dedent(
|
156
|
-
f"""\
|
157
|
-
[{profile_name}]
|
158
|
-
aws_access_key_id = {self.access_key_id}
|
159
|
-
aws_secret_access_key = {self.secret_access_key}
|
160
|
-
region = {self.region}
|
161
|
-
"""
|
162
|
-
)
|
163
|
-
|
164
|
-
def build_session(self) -> Session:
|
165
|
-
return Session(
|
166
|
-
aws_access_key_id=self.access_key_id,
|
167
|
-
aws_secret_access_key=self.secret_access_key,
|
168
|
-
region_name=self.region,
|
169
|
-
)
|
170
|
-
|
171
|
-
|
172
|
-
class AWSTemporaryCredentials(AWSStaticCredentials):
|
173
|
-
"""
|
174
|
-
A model representing temporary AWS credentials.
|
175
|
-
"""
|
176
|
-
|
177
|
-
session_token: str
|
178
|
-
|
179
|
-
def as_env_vars(self) -> dict[str, str]:
|
180
|
-
env_vars = super().as_env_vars()
|
181
|
-
env_vars["AWS_SESSION_TOKEN"] = self.session_token
|
182
|
-
return env_vars
|
183
|
-
|
184
|
-
def as_credentials_file(self, profile_name: str = "default") -> str:
|
185
|
-
return textwrap.dedent(
|
186
|
-
f"""\
|
187
|
-
[{profile_name}]
|
188
|
-
aws_access_key_id = {self.access_key_id}
|
189
|
-
aws_secret_access_key = {self.secret_access_key}
|
190
|
-
aws_session_token = {self.session_token}
|
191
|
-
region = {self.region}
|
192
|
-
"""
|
193
|
-
)
|
194
|
-
|
195
|
-
def build_session(self) -> Session:
|
196
|
-
return Session(
|
197
|
-
aws_access_key_id=self.access_key_id,
|
198
|
-
aws_secret_access_key=self.secret_access_key,
|
199
|
-
aws_session_token=self.session_token,
|
200
|
-
region_name=self.region,
|
201
|
-
)
|
202
|
-
|
203
|
-
|
204
90
|
class AmiTag(BaseModel):
|
205
91
|
name: str
|
206
92
|
value: str
|
File without changes
|
@@ -0,0 +1,190 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import textwrap
|
4
|
+
from abc import ABC, abstractmethod
|
5
|
+
from functools import cached_property
|
6
|
+
from typing import Any, TypeVar
|
7
|
+
|
8
|
+
from boto3 import Session
|
9
|
+
from botocore.client import BaseClient
|
10
|
+
from pydantic import BaseModel
|
11
|
+
|
12
|
+
import reconcile.utils.aws_api_typed.iam
|
13
|
+
import reconcile.utils.aws_api_typed.organization
|
14
|
+
import reconcile.utils.aws_api_typed.sts
|
15
|
+
from reconcile.utils.aws_api_typed.iam import AWSApiIam
|
16
|
+
from reconcile.utils.aws_api_typed.organization import AWSApiOrganizations
|
17
|
+
from reconcile.utils.aws_api_typed.sts import AWSApiSts
|
18
|
+
|
19
|
+
SubApi = TypeVar("SubApi")
|
20
|
+
|
21
|
+
|
22
|
+
class AWSCredentials(ABC):
|
23
|
+
@abstractmethod
|
24
|
+
def as_env_vars(self) -> dict[str, str]:
|
25
|
+
"""
|
26
|
+
Returns a dictionary of environment variables that can be used to authenticate with AWS.
|
27
|
+
"""
|
28
|
+
...
|
29
|
+
|
30
|
+
@abstractmethod
|
31
|
+
def as_credentials_file(self, profile_name: str = "default") -> str:
|
32
|
+
"""
|
33
|
+
Returns a string that can be used to write an AWS credentials file.
|
34
|
+
"""
|
35
|
+
...
|
36
|
+
|
37
|
+
@abstractmethod
|
38
|
+
def build_session(self) -> Session:
|
39
|
+
"""
|
40
|
+
Builds an AWS session using these credentials.
|
41
|
+
"""
|
42
|
+
...
|
43
|
+
|
44
|
+
|
45
|
+
class AWSStaticCredentials(BaseModel, AWSCredentials):
|
46
|
+
"""
|
47
|
+
A model representing AWS credentials.
|
48
|
+
"""
|
49
|
+
|
50
|
+
access_key_id: str
|
51
|
+
secret_access_key: str
|
52
|
+
region: str
|
53
|
+
|
54
|
+
def as_env_vars(self) -> dict[str, str]:
|
55
|
+
return {
|
56
|
+
"AWS_ACCESS_KEY_ID": self.access_key_id,
|
57
|
+
"AWS_SECRET_ACCESS_KEY": self.secret_access_key,
|
58
|
+
"AWS_REGION": self.region,
|
59
|
+
}
|
60
|
+
|
61
|
+
def as_credentials_file(self, profile_name: str = "default") -> str:
|
62
|
+
return textwrap.dedent(
|
63
|
+
f"""\
|
64
|
+
[{profile_name}]
|
65
|
+
aws_access_key_id = {self.access_key_id}
|
66
|
+
aws_secret_access_key = {self.secret_access_key}
|
67
|
+
region = {self.region}
|
68
|
+
"""
|
69
|
+
)
|
70
|
+
|
71
|
+
def build_session(self) -> Session:
|
72
|
+
return Session(
|
73
|
+
aws_access_key_id=self.access_key_id,
|
74
|
+
aws_secret_access_key=self.secret_access_key,
|
75
|
+
region_name=self.region,
|
76
|
+
)
|
77
|
+
|
78
|
+
|
79
|
+
class AWSTemporaryCredentials(AWSStaticCredentials):
|
80
|
+
"""
|
81
|
+
A model representing temporary AWS credentials.
|
82
|
+
"""
|
83
|
+
|
84
|
+
session_token: str
|
85
|
+
|
86
|
+
def as_env_vars(self) -> dict[str, str]:
|
87
|
+
env_vars = super().as_env_vars()
|
88
|
+
env_vars["AWS_SESSION_TOKEN"] = self.session_token
|
89
|
+
return env_vars
|
90
|
+
|
91
|
+
def as_credentials_file(self, profile_name: str = "default") -> str:
|
92
|
+
return textwrap.dedent(
|
93
|
+
f"""\
|
94
|
+
[{profile_name}]
|
95
|
+
aws_access_key_id = {self.access_key_id}
|
96
|
+
aws_secret_access_key = {self.secret_access_key}
|
97
|
+
aws_session_token = {self.session_token}
|
98
|
+
region = {self.region}
|
99
|
+
"""
|
100
|
+
)
|
101
|
+
|
102
|
+
def build_session(self) -> Session:
|
103
|
+
return Session(
|
104
|
+
aws_access_key_id=self.access_key_id,
|
105
|
+
aws_secret_access_key=self.secret_access_key,
|
106
|
+
aws_session_token=self.session_token,
|
107
|
+
region_name=self.region,
|
108
|
+
)
|
109
|
+
|
110
|
+
|
111
|
+
class AWSApi:
|
112
|
+
def __init__(self, aws_credentials: AWSCredentials) -> None:
|
113
|
+
self.session = aws_credentials.build_session()
|
114
|
+
self._session_clients: list[BaseClient] = []
|
115
|
+
|
116
|
+
def __enter__(self) -> AWSApi:
|
117
|
+
return self
|
118
|
+
|
119
|
+
def __exit__(self, *exec: Any) -> None:
|
120
|
+
self.close()
|
121
|
+
|
122
|
+
def close(self) -> None:
|
123
|
+
"""Close all clients created by this API instance."""
|
124
|
+
for client in self._session_clients:
|
125
|
+
client.close()
|
126
|
+
self._session_clients = []
|
127
|
+
|
128
|
+
def _init_sub_api(self, api_cls: type[SubApi]) -> SubApi:
|
129
|
+
"""Return a new or cached sub api client."""
|
130
|
+
match api_cls:
|
131
|
+
case reconcile.utils.aws_api_typed.iam.AWSApiIam:
|
132
|
+
client = self.session.client("iam")
|
133
|
+
api = api_cls(client) # type: ignore # mypy bug, it doesn't recognize that api_cls is callable
|
134
|
+
case reconcile.utils.aws_api_typed.organization.AWSApiOrganizations:
|
135
|
+
client = self.session.client("organizations")
|
136
|
+
api = api_cls(client)
|
137
|
+
case reconcile.utils.aws_api_typed.sts.AWSApiSts:
|
138
|
+
client = self.session.client("sts")
|
139
|
+
api = api_cls(client)
|
140
|
+
case _:
|
141
|
+
raise ValueError(f"Unknown API class: {api_cls}")
|
142
|
+
|
143
|
+
self._session_clients.append(client)
|
144
|
+
return api
|
145
|
+
|
146
|
+
@cached_property
|
147
|
+
def sts(self) -> AWSApiSts:
|
148
|
+
"""Return an AWS STS Api client."""
|
149
|
+
return self._init_sub_api(AWSApiSts)
|
150
|
+
|
151
|
+
@cached_property
|
152
|
+
def organizations(self) -> AWSApiOrganizations:
|
153
|
+
"""Return an AWS Organizations Api client."""
|
154
|
+
return self._init_sub_api(AWSApiOrganizations)
|
155
|
+
|
156
|
+
@cached_property
|
157
|
+
def iam(self) -> AWSApiIam:
|
158
|
+
"""Return an AWS IAM Api client."""
|
159
|
+
return self._init_sub_api(AWSApiIam)
|
160
|
+
|
161
|
+
def assume_role(self, account_id: str, role: str) -> AWSApi:
|
162
|
+
"""Return a new AWSApi with the assumed role."""
|
163
|
+
credentials = self.sts.assume_role(account_id=account_id, role=role)
|
164
|
+
return AWSApi(
|
165
|
+
AWSTemporaryCredentials(
|
166
|
+
access_key_id=credentials.access_key_id,
|
167
|
+
secret_access_key=credentials.secret_access_key,
|
168
|
+
session_token=credentials.session_token,
|
169
|
+
region=self.session.region_name,
|
170
|
+
)
|
171
|
+
)
|
172
|
+
|
173
|
+
def temporary_session(self, duration_seconds: int = 900) -> AWSApi:
|
174
|
+
"""Return a new AWSAPI with temporary AWS credentials from a session.
|
175
|
+
|
176
|
+
This is similar to assuming a role, in the sense that the credentials will expire after a certain amount of time.
|
177
|
+
|
178
|
+
These temporary credentials have the same permissions as the session they were built from, except:
|
179
|
+
- they can't be used for anything IAM related
|
180
|
+
- for the STS API only the AssumeRole and GetSessionToken actions are allowed
|
181
|
+
"""
|
182
|
+
tmp_creds = self.sts.get_session_token(duration_seconds=duration_seconds)
|
183
|
+
return AWSApi(
|
184
|
+
AWSTemporaryCredentials(
|
185
|
+
access_key_id=tmp_creds.access_key_id,
|
186
|
+
secret_access_key=tmp_creds.secret_access_key,
|
187
|
+
session_token=tmp_creds.session_token,
|
188
|
+
region=self.session.region_name,
|
189
|
+
)
|
190
|
+
)
|
@@ -0,0 +1,50 @@
|
|
1
|
+
from typing import TYPE_CHECKING
|
2
|
+
|
3
|
+
from pydantic import BaseModel, Field
|
4
|
+
|
5
|
+
if TYPE_CHECKING:
|
6
|
+
from mypy_boto3_iam import IAMClient
|
7
|
+
else:
|
8
|
+
IAMClient = object
|
9
|
+
|
10
|
+
|
11
|
+
class AWSAccessKey(BaseModel):
|
12
|
+
access_key_id: str = Field(..., alias="AccessKeyId")
|
13
|
+
secret_access_key: str = Field(..., alias="SecretAccessKey")
|
14
|
+
|
15
|
+
|
16
|
+
class AWSUser(BaseModel):
|
17
|
+
user_name: str = Field(..., alias="UserName")
|
18
|
+
user_id: str = Field(..., alias="UserId")
|
19
|
+
arn: str = Field(..., alias="Arn")
|
20
|
+
path: str = Field(..., alias="Path")
|
21
|
+
|
22
|
+
|
23
|
+
class AWSApiIam:
|
24
|
+
def __init__(self, client: IAMClient) -> None:
|
25
|
+
self.client = client
|
26
|
+
|
27
|
+
def create_access_key(self, user_name: str) -> AWSAccessKey:
|
28
|
+
"""Create an access key for a given user."""
|
29
|
+
credentials = self.client.create_access_key(
|
30
|
+
UserName=user_name,
|
31
|
+
)
|
32
|
+
return AWSAccessKey(**credentials["AccessKey"])
|
33
|
+
|
34
|
+
def create_user(self, user_name: str) -> AWSUser:
|
35
|
+
"""Create a new IAM user."""
|
36
|
+
user = self.client.create_user(
|
37
|
+
UserName=user_name,
|
38
|
+
)
|
39
|
+
return AWSUser(**user["User"])
|
40
|
+
|
41
|
+
def attach_user_policy(self, user_name: str, policy_arn: str) -> None:
|
42
|
+
"""Attach a policy to a user."""
|
43
|
+
self.client.attach_user_policy(
|
44
|
+
UserName=user_name,
|
45
|
+
PolicyArn=policy_arn,
|
46
|
+
)
|
47
|
+
|
48
|
+
def create_account_alias(self, account_alias: str) -> None:
|
49
|
+
"""Create an account alias."""
|
50
|
+
self.client.create_account_alias(AccountAlias=account_alias)
|
@@ -0,0 +1,104 @@
|
|
1
|
+
from collections.abc import Mapping
|
2
|
+
from typing import TYPE_CHECKING
|
3
|
+
|
4
|
+
from pydantic import BaseModel, Field
|
5
|
+
|
6
|
+
if TYPE_CHECKING:
|
7
|
+
from mypy_boto3_organizations import OrganizationsClient
|
8
|
+
from mypy_boto3_organizations.literals import CreateAccountFailureReasonType
|
9
|
+
else:
|
10
|
+
OrganizationsClient = object
|
11
|
+
CreateAccountFailureReasonType = object
|
12
|
+
|
13
|
+
|
14
|
+
class AwsOrganizationOU(BaseModel):
|
15
|
+
id: str = Field(..., alias="Id")
|
16
|
+
arn: str = Field(..., alias="Arn")
|
17
|
+
name: str = Field(..., alias="Name")
|
18
|
+
children: list["AwsOrganizationOU"] = []
|
19
|
+
|
20
|
+
def find(self, path: str) -> "AwsOrganizationOU":
|
21
|
+
"""Return an organizational unit by its path."""
|
22
|
+
name, *rest = path.strip("/").split("/")
|
23
|
+
subs = "/".join(rest)
|
24
|
+
if self.name == name:
|
25
|
+
if not rest:
|
26
|
+
return self
|
27
|
+
for child in self.children:
|
28
|
+
try:
|
29
|
+
return child.find(subs)
|
30
|
+
except KeyError:
|
31
|
+
pass
|
32
|
+
raise KeyError(f"OU not found: {path}")
|
33
|
+
|
34
|
+
|
35
|
+
class AWSAccountStatus(BaseModel):
|
36
|
+
id: str = Field(..., alias="Id")
|
37
|
+
account_name: str = Field(..., alias="AccountName")
|
38
|
+
account_id: str = Field(..., alias="AccountId")
|
39
|
+
state: str = Field(..., alias="State")
|
40
|
+
failure_reason: CreateAccountFailureReasonType | None = Field(alias="FailureReason")
|
41
|
+
|
42
|
+
|
43
|
+
class AWSAccountCreationException(Exception):
|
44
|
+
pass
|
45
|
+
|
46
|
+
|
47
|
+
class AWSApiOrganizations:
|
48
|
+
def __init__(self, client: OrganizationsClient) -> None:
|
49
|
+
self.client = client
|
50
|
+
|
51
|
+
def get_organizational_units_tree(
|
52
|
+
self, root: AwsOrganizationOU | None = None
|
53
|
+
) -> AwsOrganizationOU:
|
54
|
+
"""List all organizational units for a given root recursively."""
|
55
|
+
if not root:
|
56
|
+
root = AwsOrganizationOU(**self.client.list_roots()["Roots"][0])
|
57
|
+
|
58
|
+
paginator = self.client.get_paginator("list_organizational_units_for_parent")
|
59
|
+
for page in paginator.paginate(ParentId=root.id):
|
60
|
+
for ou_raw in page["OrganizationalUnits"]:
|
61
|
+
ou = AwsOrganizationOU(**ou_raw)
|
62
|
+
root.children.append(ou)
|
63
|
+
self.get_organizational_units_tree(root=ou)
|
64
|
+
return root
|
65
|
+
|
66
|
+
def create_account(
|
67
|
+
self,
|
68
|
+
email: str,
|
69
|
+
account_name: str,
|
70
|
+
tags: Mapping[str, str],
|
71
|
+
access_to_billing: bool = True,
|
72
|
+
) -> AWSAccountStatus:
|
73
|
+
"""Create a new account in the organization."""
|
74
|
+
resp = self.client.create_account(
|
75
|
+
Email=email,
|
76
|
+
AccountName=account_name,
|
77
|
+
IamUserAccessToBilling="ALLOW" if access_to_billing else "DENY",
|
78
|
+
Tags=[{"Key": k, "Value": v} for k, v in tags.items()],
|
79
|
+
)
|
80
|
+
status = AWSAccountStatus(**resp["CreateAccountStatus"])
|
81
|
+
if status.state == "FAILED":
|
82
|
+
raise AWSAccountCreationException(
|
83
|
+
f"Account creation failed: {status.failure_reason}"
|
84
|
+
)
|
85
|
+
return status
|
86
|
+
|
87
|
+
def describe_create_account_status(
|
88
|
+
self, create_account_request_id: str
|
89
|
+
) -> AWSAccountStatus:
|
90
|
+
"""Return the status of a create account request."""
|
91
|
+
resp = self.client.describe_create_account_status(
|
92
|
+
CreateAccountRequestId=create_account_request_id
|
93
|
+
)
|
94
|
+
return AWSAccountStatus(**resp["CreateAccountStatus"])
|
95
|
+
|
96
|
+
def move_account(
|
97
|
+
self, account_id: str, source_parent_id: str, destination_parent_id: str
|
98
|
+
) -> None:
|
99
|
+
"""Move an account to a different organizational unit."""
|
100
|
+
self.client.move_account(
|
101
|
+
AccountId=account_id,
|
102
|
+
SourceParentId=source_parent_id,
|
103
|
+
DestinationParentId=destination_parent_id,
|
104
|
+
)
|
@@ -0,0 +1,36 @@
|
|
1
|
+
from datetime import datetime
|
2
|
+
from typing import TYPE_CHECKING
|
3
|
+
|
4
|
+
from pydantic import BaseModel, Field
|
5
|
+
|
6
|
+
if TYPE_CHECKING:
|
7
|
+
from mypy_boto3_sts import STSClient
|
8
|
+
else:
|
9
|
+
STSClient = object
|
10
|
+
|
11
|
+
|
12
|
+
class AWSCredentials(BaseModel):
|
13
|
+
access_key_id: str = Field(..., alias="AccessKeyId")
|
14
|
+
secret_access_key: str = Field(..., alias="SecretAccessKey")
|
15
|
+
session_token: str = Field(..., alias="SessionToken")
|
16
|
+
expiration: datetime = Field(..., alias="Expiration")
|
17
|
+
|
18
|
+
|
19
|
+
class AWSApiSts:
|
20
|
+
def __init__(self, client: STSClient) -> None:
|
21
|
+
self.client = client
|
22
|
+
|
23
|
+
def assume_role(self, account_id: str, role: str) -> AWSCredentials:
|
24
|
+
"""Assume a role and return temporary credentials."""
|
25
|
+
assumed_role_object = self.client.assume_role(
|
26
|
+
RoleArn=f"arn:aws:iam::{account_id}:role/{role}",
|
27
|
+
RoleSessionName=role,
|
28
|
+
)
|
29
|
+
return AWSCredentials(**assumed_role_object["Credentials"])
|
30
|
+
|
31
|
+
def get_session_token(self, duration_seconds: int = 900) -> AWSCredentials:
|
32
|
+
"""Return temporary credentials."""
|
33
|
+
assumed_role_object = self.client.get_session_token(
|
34
|
+
DurationSeconds=duration_seconds
|
35
|
+
)
|
36
|
+
return AWSCredentials(**assumed_role_object["Credentials"])
|
File without changes
|
File without changes
|
{qontract_reconcile-0.10.1rc602.dist-info → qontract_reconcile-0.10.1rc604.dist-info}/top_level.txt
RENAMED
File without changes
|