qontract-reconcile 0.10.1rc58__py3-none-any.whl → 0.10.1rc60__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.1rc58.dist-info → qontract_reconcile-0.10.1rc60.dist-info}/METADATA +1 -1
- {qontract_reconcile-0.10.1rc58.dist-info → qontract_reconcile-0.10.1rc60.dist-info}/RECORD +14 -9
- reconcile/aws_ami_cleanup/__init__.py +0 -0
- reconcile/aws_ami_cleanup/integration.py +283 -0
- reconcile/change_owners/change_owners.py +2 -32
- reconcile/change_owners/self_service_roles.py +49 -7
- reconcile/cli.py +9 -0
- reconcile/gql_definitions/aws_ami_cleanup/__init__.py +0 -0
- reconcile/gql_definitions/aws_ami_cleanup/asg_namespaces.py +132 -0
- reconcile/queries.py +12 -0
- reconcile/test/test_aws_ami_cleanup.py +296 -0
- {qontract_reconcile-0.10.1rc58.dist-info → qontract_reconcile-0.10.1rc60.dist-info}/WHEEL +0 -0
- {qontract_reconcile-0.10.1rc58.dist-info → qontract_reconcile-0.10.1rc60.dist-info}/entry_points.txt +0 -0
- {qontract_reconcile-0.10.1rc58.dist-info → qontract_reconcile-0.10.1rc60.dist-info}/top_level.txt +0 -0
{qontract_reconcile-0.10.1rc58.dist-info → qontract_reconcile-0.10.1rc60.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.1rc60
|
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
|
@@ -7,7 +7,7 @@ reconcile/aws_iam_password_reset.py,sha256=NwErtrqgBiXr7eGCAHdtGGOx0S7-4JnSc29Ie
|
|
7
7
|
reconcile/aws_support_cases_sos.py,sha256=i6bSWnlH9fh14P14PjVhFLwNl-q3fD733_rXKM_O51c,2992
|
8
8
|
reconcile/blackbox_exporter_endpoint_monitoring.py,sha256=W_VJagnsJR1v5oqjlI3RJJE0_nhtJ0m81RS8zWA5u5c,3538
|
9
9
|
reconcile/checkpoint.py,sha256=figtZRuWUvdpdSnkhAqeGvO5dI02TT6J3heyeFhlwqM,5016
|
10
|
-
reconcile/cli.py,sha256=
|
10
|
+
reconcile/cli.py,sha256=c3tSmmo4ZwH23ve5iHDRA7zFLJvyG4DPw9G-LdWJkN0,71473
|
11
11
|
reconcile/closedbox_endpoint_monitoring_base.py,sha256=0xg_d8dwd36Y8GY1mE-LLO1LQpPEMM77bzAfc_KdgzU,4870
|
12
12
|
reconcile/cluster_deployment_mapper.py,sha256=2Ah-nu-Mdig0pjuiZl_XLrmVAjYzFjORR3dMlCgkmw0,2352
|
13
13
|
reconcile/dashdotdb_base.py,sha256=Ca75-OQiu5HeA8Q6zQpEYuhyCSjeuWe99K4y9ipTORM,4032
|
@@ -94,7 +94,7 @@ reconcile/quay_mirror.py,sha256=9pwl1gLzRpsVXF5yPULM4ET_C5F8_xPmH8Mv8AS2AfI,1340
|
|
94
94
|
reconcile/quay_mirror_org.py,sha256=E1OdRe-ppxTkNCwu20iVRhEdG1fPDBroLY02NgiMN7c,10381
|
95
95
|
reconcile/quay_permissions.py,sha256=_3PCWjNWoU7VHlYgHzUevvL_jJmEMsWfXV_nzjeiyhU,4099
|
96
96
|
reconcile/quay_repos.py,sha256=7609RBVQihis96FNOOe-i9tCTYwcTVy4WpKAL6HpnkU,7031
|
97
|
-
reconcile/queries.py,sha256=
|
97
|
+
reconcile/queries.py,sha256=UMUfwqFmujdUJ9z1X_RN-Mktjag144An6jRgoZza57o,49432
|
98
98
|
reconcile/query_validator.py,sha256=oLEZIAsQCzxmmZ7b9dSw-OKuEjpI1dbVu4XfCfjpmi8,1503
|
99
99
|
reconcile/requests_sender.py,sha256=914iluuF4UVgG3VyxxtnHOu4yf6YKS2fIy6PViSsFTQ,3875
|
100
100
|
reconcile/resource_scraper.py,sha256=vo1N9vLJCYWvXlTwFRIpEuWjx_39ZV9zxJlpoPq4g3U,2330
|
@@ -127,16 +127,18 @@ reconcile/aus/models.py,sha256=kRrs276iJClnNnEeWnJma2Gvx8E9kes77I_XbgKoVzk,4722
|
|
127
127
|
reconcile/aus/ocm_addons_upgrade_scheduler_org.py,sha256=UjKFUeomkVpwVVkejPfNBMN7CQywIs5uOipUckfI8Y0,7659
|
128
128
|
reconcile/aus/ocm_upgrade_scheduler.py,sha256=ndruHjyheMzjZgcIQQEG7qjLnhKL4pF_kIExxBrYZZM,5153
|
129
129
|
reconcile/aus/ocm_upgrade_scheduler_org.py,sha256=tH0oQFJsO4UrEYspqiq-ne6-ULXA4NLY3g3hxaJKGEA,2867
|
130
|
+
reconcile/aws_ami_cleanup/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
131
|
+
reconcile/aws_ami_cleanup/integration.py,sha256=5-znzuQAGz9hcepBrVowzylqUQuAmVpAPRZKBl5QFZg,8752
|
130
132
|
reconcile/change_owners/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
131
133
|
reconcile/change_owners/approver.py,sha256=L7XJWJ-rgn8BOmeMb6lBDV8lHFCUaNoHGDSD7OH03vA,2244
|
132
134
|
reconcile/change_owners/bundle.py,sha256=VTRXDMlcNYykqCPRIcCDTCA188Ouxglt7Sxeu1qep4E,5102
|
133
|
-
reconcile/change_owners/change_owners.py,sha256=
|
135
|
+
reconcile/change_owners/change_owners.py,sha256=WX-ekjI3IhdrJtvwieyeE32ho6cOsIOzFY3uzS25X5E,13268
|
134
136
|
reconcile/change_owners/change_types.py,sha256=rWYmrw3rcE-lEZ-SQb3D38an5GhasaydpAtYpg5nPBU,28405
|
135
137
|
reconcile/change_owners/changes.py,sha256=uGFukXgMkcO9xAXzF_LL0HLdNcQtSq0IyojsI1YIJTs,15889
|
136
138
|
reconcile/change_owners/decision.py,sha256=uFuk06kzgifCg4ExaMRHEX6LIKnwaMkSQG8AeNG2uJo,4811
|
137
139
|
reconcile/change_owners/diff.py,sha256=84pbx19C-QL1SgK16MxPqv3Ssiy1g8j0i978iSDb2kM,8840
|
138
140
|
reconcile/change_owners/implicit_ownership.py,sha256=xga4X0OiZityQf5YRGLUSaIdr2Ij74H879-sjWzgTDk,4161
|
139
|
-
reconcile/change_owners/self_service_roles.py,sha256=
|
141
|
+
reconcile/change_owners/self_service_roles.py,sha256=Ya5NPHCFpzMCDmxHrrjueXCYAap7jXzm0woudUEH6lc,6754
|
140
142
|
reconcile/change_owners/tester.py,sha256=BfJM8dsurubnyDF9UR3oV2N3ClyjLsqDNq0Hy_UdjOk,8949
|
141
143
|
reconcile/cna/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
142
144
|
reconcile/cna/client.py,sha256=t9gJDrKf4ApBlgu8c4QUbmzrYoSo1QPsnAGfucva2_U,1562
|
@@ -155,6 +157,8 @@ reconcile/gql_definitions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJW
|
|
155
157
|
reconcile/gql_definitions/advanced_upgrade_service/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
156
158
|
reconcile/gql_definitions/advanced_upgrade_service/aus_clusters.py,sha256=eFo-836XT566MwEzlbb4W7eNdPmh_gUzDUUFwCzX7Kk,3731
|
157
159
|
reconcile/gql_definitions/advanced_upgrade_service/aus_organization.py,sha256=rzxC3w1Bff3VSpI4zGN1wR5cSOmPxLYKBuwFGGwraPU,2872
|
160
|
+
reconcile/gql_definitions/aws_ami_cleanup/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
161
|
+
reconcile/gql_definitions/aws_ami_cleanup/asg_namespaces.py,sha256=hu0f4DQ5a6mP4L27DR9vbh01tuKYFS-vBE-PttOnFHE,3746
|
158
162
|
reconcile/gql_definitions/change_owners/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
159
163
|
reconcile/gql_definitions/change_owners/queries/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
160
164
|
reconcile/gql_definitions/change_owners/queries/change_types.py,sha256=LViPCCECF0BQLk9RoB_4uUrkqQ3dRo73bVtxElqYLxQ,4993
|
@@ -297,6 +301,7 @@ reconcile/test/fixtures.py,sha256=VhvLXH0AWXEyu3FgPp7bcSTPmDPfMEa2v-_9cd8dCmw,57
|
|
297
301
|
reconcile/test/test_aggregated_list.py,sha256=iiWitQuNYC58aimWaiBoE4NROHjr1NCgQ91MnHEG_Ro,6412
|
298
302
|
reconcile/test/test_amtool.py,sha256=vxRhGieeydMBOb9UI2ziMHjJa8puMeGNsUhGhy-yMnk,1032
|
299
303
|
reconcile/test/test_auto_promoter.py,sha256=4EtLLN0FAJGJnFoSCRsB5hHyY2-H3GMfnn5-p7kNCTg,10340
|
304
|
+
reconcile/test/test_aws_ami_cleanup.py,sha256=vHSldoVnZtfc1s_7KDh7GkzPTr7hF84ABj_vMNBR06s,8633
|
300
305
|
reconcile/test/test_aws_ami_share.py,sha256=eSITdDoXs8mMY7P2lFxAX2DA0sJ9RW6D1tG8Rek0gLE,1981
|
301
306
|
reconcile/test/test_aws_iam_keys.py,sha256=MfE9EvItyPNPAl5QaLlJFUvvrZFiar518TM2wWNjJn4,1829
|
302
307
|
reconcile/test/test_aws_iam_password_reset.py,sha256=fnkqB90adR7W4L4saNdrtIiwnQB9bXgqJ9R1CKxjSnk,860
|
@@ -584,8 +589,8 @@ tools/sre_checkpoints/util.py,sha256=zEDbGr18ZeHNQwW8pUsr2JRjuXIPz--WAGJxZo9sv_Y
|
|
584
589
|
tools/test/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
585
590
|
tools/test/test_qontract_cli.py,sha256=awwTHEc2DWlykuqGIYM0WOBoSL0KRnOraCLk3C7izis,1401
|
586
591
|
tools/test/test_sre_checkpoints.py,sha256=SKqPPTl9ua0RFdSSofnoQX-JZE6dFLO3LRhfQzqtfh8,2607
|
587
|
-
qontract_reconcile-0.10.
|
588
|
-
qontract_reconcile-0.10.
|
589
|
-
qontract_reconcile-0.10.
|
590
|
-
qontract_reconcile-0.10.
|
591
|
-
qontract_reconcile-0.10.
|
592
|
+
qontract_reconcile-0.10.1rc60.dist-info/METADATA,sha256=JAgKlKekEmzyQTqdv9oMiVAsoeAyIaDysoW4U1WavCw,2290
|
593
|
+
qontract_reconcile-0.10.1rc60.dist-info/WHEEL,sha256=pkctZYzUS4AYVn6dJ-7367OJZivF2e8RA9b_ZBjif18,92
|
594
|
+
qontract_reconcile-0.10.1rc60.dist-info/entry_points.txt,sha256=Af70EWPJxsTiCNF6gA-pWdw1A0Heqn-PZF-oBc5NmiU,302
|
595
|
+
qontract_reconcile-0.10.1rc60.dist-info/top_level.txt,sha256=l5ISPoXzt0SdR4jVdkfa7RPSKNc8zAHYWAnR-Dw8Ey8,24
|
596
|
+
qontract_reconcile-0.10.1rc60.dist-info/RECORD,,
|
File without changes
|
@@ -0,0 +1,283 @@
|
|
1
|
+
import logging
|
2
|
+
import re
|
3
|
+
import sys
|
4
|
+
from collections.abc import (
|
5
|
+
Callable,
|
6
|
+
Mapping,
|
7
|
+
)
|
8
|
+
from datetime import (
|
9
|
+
datetime,
|
10
|
+
timedelta,
|
11
|
+
)
|
12
|
+
from typing import (
|
13
|
+
TYPE_CHECKING,
|
14
|
+
Any,
|
15
|
+
Optional,
|
16
|
+
)
|
17
|
+
|
18
|
+
from botocore.exceptions import ClientError
|
19
|
+
from pydantic import (
|
20
|
+
BaseModel,
|
21
|
+
Field,
|
22
|
+
)
|
23
|
+
|
24
|
+
from reconcile import queries
|
25
|
+
from reconcile.gql_definitions.aws_ami_cleanup.asg_namespaces import (
|
26
|
+
ASGImageGitV1,
|
27
|
+
ASGImageStaticV1,
|
28
|
+
NamespaceTerraformProviderResourceAWSV1,
|
29
|
+
NamespaceTerraformResourceASGV1,
|
30
|
+
NamespaceV1,
|
31
|
+
)
|
32
|
+
from reconcile.gql_definitions.aws_ami_cleanup.asg_namespaces import (
|
33
|
+
query as query_asg_namespaces,
|
34
|
+
)
|
35
|
+
from reconcile.status import ExitCodes
|
36
|
+
from reconcile.typed_queries.app_interface_vault_settings import (
|
37
|
+
get_app_interface_vault_settings,
|
38
|
+
)
|
39
|
+
from reconcile.utils import gql
|
40
|
+
from reconcile.utils.aws_api import AWSApi
|
41
|
+
from reconcile.utils.defer import defer
|
42
|
+
from reconcile.utils.parse_dhms_duration import dhms_to_seconds
|
43
|
+
from reconcile.utils.secret_reader import create_secret_reader
|
44
|
+
from reconcile.utils.terrascript_aws_client import TerrascriptClient as Terrascript
|
45
|
+
|
46
|
+
if TYPE_CHECKING:
|
47
|
+
from mypy_boto3_ec2 import EC2Client
|
48
|
+
else:
|
49
|
+
EC2Client = object
|
50
|
+
|
51
|
+
QONTRACT_INTEGRATION = "aws_ami_cleanup"
|
52
|
+
MANAGED_TAG = {"Key": "managed_by_integration", "Value": QONTRACT_INTEGRATION}
|
53
|
+
|
54
|
+
|
55
|
+
class CannotCompareTagsError(Exception):
|
56
|
+
pass
|
57
|
+
|
58
|
+
|
59
|
+
class AmiTag(BaseModel):
|
60
|
+
key: str = Field(alias="Key")
|
61
|
+
value: str = Field(alias="Value")
|
62
|
+
|
63
|
+
class Config:
|
64
|
+
allow_population_by_field_name = True
|
65
|
+
frozen = True
|
66
|
+
|
67
|
+
|
68
|
+
class AWSAmi(BaseModel):
|
69
|
+
name: str
|
70
|
+
image_id: str
|
71
|
+
tags: set[AmiTag]
|
72
|
+
creation_date: datetime
|
73
|
+
|
74
|
+
class Config:
|
75
|
+
frozen = True
|
76
|
+
|
77
|
+
|
78
|
+
class AIAmi(BaseModel):
|
79
|
+
identifier: str
|
80
|
+
tags: set[AmiTag]
|
81
|
+
|
82
|
+
class Config:
|
83
|
+
frozen = True
|
84
|
+
|
85
|
+
|
86
|
+
def get_aws_amis(
|
87
|
+
aws_api: AWSApi,
|
88
|
+
ec2_client: EC2Client,
|
89
|
+
owner: str,
|
90
|
+
regex: str,
|
91
|
+
age_in_seconds: int,
|
92
|
+
utc_now: datetime,
|
93
|
+
region: str,
|
94
|
+
) -> list[AWSAmi]:
|
95
|
+
"""Get amis that match regex older than given age"""
|
96
|
+
|
97
|
+
images = aws_api.paginate(
|
98
|
+
ec2_client, "describe_images", "Images", {"Owners": [owner]}
|
99
|
+
)
|
100
|
+
|
101
|
+
pattern = re.compile(regex)
|
102
|
+
results = []
|
103
|
+
for i in images:
|
104
|
+
if not re.search(pattern, i["Name"]):
|
105
|
+
continue
|
106
|
+
|
107
|
+
creation_date = datetime.strptime(i["CreationDate"], "%Y-%m-%dT%H:%M:%S.%fZ")
|
108
|
+
current_delta = utc_now - creation_date
|
109
|
+
delete_delta = timedelta(seconds=age_in_seconds)
|
110
|
+
|
111
|
+
if current_delta < delete_delta:
|
112
|
+
continue
|
113
|
+
|
114
|
+
# We have nothing to do with untagged AMIs since we will need tags to verify if AMI are
|
115
|
+
# in use or not.
|
116
|
+
if not i.get("Tags"):
|
117
|
+
continue
|
118
|
+
|
119
|
+
tags = {AmiTag(**tag) for tag in i.get("Tags")}
|
120
|
+
results.append(
|
121
|
+
AWSAmi(
|
122
|
+
name=i["Name"],
|
123
|
+
image_id=i["ImageId"],
|
124
|
+
tags=tags,
|
125
|
+
creation_date=creation_date,
|
126
|
+
)
|
127
|
+
)
|
128
|
+
|
129
|
+
return results
|
130
|
+
|
131
|
+
|
132
|
+
def get_region(
|
133
|
+
cleanup: Mapping[str, Any],
|
134
|
+
account: Mapping[str, Any],
|
135
|
+
) -> str:
|
136
|
+
"""Defines the region to search for AMIs."""
|
137
|
+
region = cleanup.get("region") or account["resourcesDefaultRegion"]
|
138
|
+
if region not in account["supportedDeploymentRegions"]:
|
139
|
+
raise ValueError(f"region {region} is not supported in {account['name']}")
|
140
|
+
|
141
|
+
return region
|
142
|
+
|
143
|
+
|
144
|
+
def get_app_interface_amis(
|
145
|
+
namespaces: Optional[list[NamespaceV1]], ts: Terrascript
|
146
|
+
) -> list[AIAmi]:
|
147
|
+
"""Returns all the ami referenced in ASGs in app-interface."""
|
148
|
+
app_interface_amis = []
|
149
|
+
for n in namespaces or []:
|
150
|
+
for er in n.external_resources or []:
|
151
|
+
if not isinstance(er, NamespaceTerraformProviderResourceAWSV1):
|
152
|
+
continue
|
153
|
+
|
154
|
+
for r in er.resources:
|
155
|
+
if not isinstance(r, NamespaceTerraformResourceASGV1):
|
156
|
+
continue
|
157
|
+
|
158
|
+
tags = set()
|
159
|
+
for i in r.image:
|
160
|
+
if isinstance(i, ASGImageGitV1):
|
161
|
+
tags.add(
|
162
|
+
AmiTag(
|
163
|
+
key=i.tag_name,
|
164
|
+
value=ts.get_commit_sha(i.dict(by_alias=True)),
|
165
|
+
)
|
166
|
+
)
|
167
|
+
elif isinstance(i, ASGImageStaticV1):
|
168
|
+
tags.add(AmiTag(key=i.tag_name, value=i.value))
|
169
|
+
|
170
|
+
app_interface_amis.append(AIAmi(identifier=r.identifier, tags=tags))
|
171
|
+
|
172
|
+
return app_interface_amis
|
173
|
+
|
174
|
+
|
175
|
+
def check_aws_ami_in_use(
|
176
|
+
aws_ami: AWSAmi, app_interface_amis: list[AIAmi]
|
177
|
+
) -> Optional[str]:
|
178
|
+
"""Verifies if the given AWS ami is in use in a defined app-interface ASG."""
|
179
|
+
for ai_ami in app_interface_amis:
|
180
|
+
# This can happen if the ASG init template has changed over the time. We don't have a way
|
181
|
+
# to properly delete these automatically since we cannot assure they are not in use.
|
182
|
+
# The integration will fail in this case and these amis will need to be handled manually.
|
183
|
+
if len(ai_ami.tags) > len(aws_ami.tags):
|
184
|
+
raise CannotCompareTagsError(
|
185
|
+
f"{ai_ami.identifier} AI AMI has more tags than {aws_ami.image_id} AWS AMI"
|
186
|
+
)
|
187
|
+
|
188
|
+
if ai_ami.tags.issubset(aws_ami.tags):
|
189
|
+
return ai_ami.identifier
|
190
|
+
|
191
|
+
return None
|
192
|
+
|
193
|
+
|
194
|
+
@defer
|
195
|
+
def run(dry_run: bool, thread_pool_size: int, defer: Optional[Callable] = None) -> None:
|
196
|
+
exit_code = ExitCodes.SUCCESS
|
197
|
+
|
198
|
+
# We still use here a non-typed query; accounts is passed to AWSApi and Terrascript classes
|
199
|
+
# which contain a vast amount of magic based on keys from that dict. Since this integration
|
200
|
+
# cannot still be properly monitored (see https://issues.redhat.com/browse/APPSRE-7674),
|
201
|
+
# it's easy that it breaks without being noticed. Once it is properly monitored, this should
|
202
|
+
# be moved to a typed query.
|
203
|
+
cleanup_accounts = [
|
204
|
+
a
|
205
|
+
for a in queries.get_aws_accounts(terraform_state=True, cleanup=True)
|
206
|
+
if a.get("cleanup")
|
207
|
+
]
|
208
|
+
|
209
|
+
vault_settings = get_app_interface_vault_settings()
|
210
|
+
|
211
|
+
ts = Terrascript(
|
212
|
+
QONTRACT_INTEGRATION,
|
213
|
+
"",
|
214
|
+
thread_pool_size,
|
215
|
+
cleanup_accounts,
|
216
|
+
settings=vault_settings.dict(by_alias=True),
|
217
|
+
)
|
218
|
+
|
219
|
+
gqlapi = gql.get_api()
|
220
|
+
namespaces = query_asg_namespaces(query_func=gqlapi.query).namespaces or []
|
221
|
+
app_interface_amis = get_app_interface_amis(namespaces, ts)
|
222
|
+
|
223
|
+
secret_reader = create_secret_reader(use_vault=vault_settings.vault)
|
224
|
+
aws_api = AWSApi(1, cleanup_accounts, secret_reader=secret_reader, init_users=False)
|
225
|
+
if defer: # defer is provided by the method decorator; this makes just mypy happy.
|
226
|
+
defer(aws_api.cleanup)
|
227
|
+
|
228
|
+
utc_now = datetime.utcnow()
|
229
|
+
for account in cleanup_accounts:
|
230
|
+
for cleanup in account["cleanup"]:
|
231
|
+
if cleanup["provider"] != "ami":
|
232
|
+
continue
|
233
|
+
|
234
|
+
region = get_region(cleanup, account)
|
235
|
+
regex = cleanup["regex"]
|
236
|
+
age_in_seconds = dhms_to_seconds(cleanup["age"])
|
237
|
+
|
238
|
+
session = aws_api.get_session(account["name"])
|
239
|
+
ec2_client = aws_api.get_session_client(session, "ec2", region)
|
240
|
+
|
241
|
+
amis = get_aws_amis(
|
242
|
+
aws_api=aws_api,
|
243
|
+
ec2_client=ec2_client,
|
244
|
+
owner=account["uid"],
|
245
|
+
regex=regex,
|
246
|
+
age_in_seconds=age_in_seconds,
|
247
|
+
utc_now=utc_now,
|
248
|
+
region=region,
|
249
|
+
)
|
250
|
+
|
251
|
+
for aws_ami in amis:
|
252
|
+
try:
|
253
|
+
if identifier := check_aws_ami_in_use(aws_ami, app_interface_amis):
|
254
|
+
logging.info(
|
255
|
+
"Discarding ami %s with id %s as it is used in app-interface in %s",
|
256
|
+
aws_ami.name,
|
257
|
+
aws_ami.image_id,
|
258
|
+
identifier,
|
259
|
+
)
|
260
|
+
continue
|
261
|
+
except CannotCompareTagsError as e:
|
262
|
+
exit_code = ExitCodes.ERROR
|
263
|
+
logging.error(e)
|
264
|
+
continue
|
265
|
+
|
266
|
+
logging.info(
|
267
|
+
"Deregistering image %s with id %s created in %s",
|
268
|
+
aws_ami.name,
|
269
|
+
aws_ami.image_id,
|
270
|
+
aws_ami.creation_date,
|
271
|
+
)
|
272
|
+
|
273
|
+
try:
|
274
|
+
ec2_client.deregister_image(
|
275
|
+
ImageId=aws_ami.image_id, DryRun=dry_run
|
276
|
+
)
|
277
|
+
except ClientError as e:
|
278
|
+
if "DryRunOperation" in str(e):
|
279
|
+
logging.info(e)
|
280
|
+
continue
|
281
|
+
raise
|
282
|
+
|
283
|
+
sys.exit(exit_code)
|
@@ -29,12 +29,9 @@ from reconcile.change_owners.implicit_ownership import (
|
|
29
29
|
)
|
30
30
|
from reconcile.change_owners.self_service_roles import (
|
31
31
|
cover_changes_with_self_service_roles,
|
32
|
+
fetch_self_service_roles,
|
32
33
|
)
|
33
|
-
from reconcile.gql_definitions.change_owners.queries import
|
34
|
-
change_types,
|
35
|
-
self_service_roles,
|
36
|
-
)
|
37
|
-
from reconcile.gql_definitions.change_owners.queries.self_service_roles import RoleV1
|
34
|
+
from reconcile.gql_definitions.change_owners.queries import change_types
|
38
35
|
from reconcile.utils import gql
|
39
36
|
from reconcile.utils.gitlab_api import GitLabApi
|
40
37
|
from reconcile.utils.mr.labels import (
|
@@ -78,33 +75,6 @@ def cover_changes(
|
|
78
75
|
)
|
79
76
|
|
80
77
|
|
81
|
-
def validate_self_service_role(role: RoleV1) -> None:
|
82
|
-
for ssc in role.self_service or []:
|
83
|
-
if ssc.change_type.context_schema:
|
84
|
-
# check that all referenced datafiles have a schema that
|
85
|
-
# is compatible with the change-type
|
86
|
-
incompatible_datafiles = [
|
87
|
-
df.path
|
88
|
-
for df in ssc.datafiles or []
|
89
|
-
if df.datafile_schema != ssc.change_type.context_schema
|
90
|
-
]
|
91
|
-
if incompatible_datafiles:
|
92
|
-
raise ValueError(
|
93
|
-
f"The datafiles {incompatible_datafiles} are not compatible with the "
|
94
|
-
f"{ssc.change_type.name} change-types contextSchema {ssc.change_type.context_schema}"
|
95
|
-
)
|
96
|
-
|
97
|
-
|
98
|
-
def fetch_self_service_roles(gql_api: gql.GqlApi) -> list[RoleV1]:
|
99
|
-
roles: list[RoleV1] = []
|
100
|
-
for r in self_service_roles.query(gql_api.query).roles or []:
|
101
|
-
if not r.self_service:
|
102
|
-
continue
|
103
|
-
validate_self_service_role(r)
|
104
|
-
roles.append(r)
|
105
|
-
return roles
|
106
|
-
|
107
|
-
|
108
78
|
def fetch_change_type_processors(
|
109
79
|
gql_api: gql.GqlApi, file_diff_resolver: FileDiffResolver
|
110
80
|
) -> list[ChangeTypeProcessor]:
|
@@ -12,15 +12,62 @@ from reconcile.change_owners.change_types import (
|
|
12
12
|
ChangeTypeProcessor,
|
13
13
|
)
|
14
14
|
from reconcile.change_owners.changes import BundleFileChange
|
15
|
+
from reconcile.gql_definitions.change_owners.queries import self_service_roles
|
15
16
|
from reconcile.gql_definitions.change_owners.queries.self_service_roles import (
|
16
17
|
PermissionGitlabGroupMembershipV1,
|
17
18
|
PermissionSlackUsergroupV1,
|
18
19
|
RoleV1,
|
19
20
|
)
|
21
|
+
from reconcile.utils import gql
|
20
22
|
|
21
23
|
|
22
|
-
class
|
23
|
-
|
24
|
+
class NoApproversInSelfServiceRoleError(Exception):
|
25
|
+
"""
|
26
|
+
Thrown when a self-service role has no approvers
|
27
|
+
"""
|
28
|
+
|
29
|
+
|
30
|
+
class DatafileIncompatibleWithChangeTypeError(Exception):
|
31
|
+
"""
|
32
|
+
Thrown when a datafile and a change type are hooked up
|
33
|
+
in a self-service role, but are not compatible schema wise.
|
34
|
+
"""
|
35
|
+
|
36
|
+
|
37
|
+
def fetch_self_service_roles(gql_api: gql.GqlApi) -> list[RoleV1]:
|
38
|
+
roles: list[RoleV1] = []
|
39
|
+
for r in self_service_roles.query(gql_api.query).roles or []:
|
40
|
+
if not r.self_service:
|
41
|
+
continue
|
42
|
+
validate_self_service_role(r)
|
43
|
+
roles.append(r)
|
44
|
+
return roles
|
45
|
+
|
46
|
+
|
47
|
+
def validate_self_service_role(role: RoleV1) -> None:
|
48
|
+
"""
|
49
|
+
Validate that a self-service role has approvers and that the referenced
|
50
|
+
change-types and datafiles/resources are compatible.
|
51
|
+
"""
|
52
|
+
if not role.users and not role.bots:
|
53
|
+
raise NoApproversInSelfServiceRoleError(
|
54
|
+
f"The role {role.name} has no users or bots "
|
55
|
+
"to drive the self-service process. Add approvers to the roles."
|
56
|
+
)
|
57
|
+
for ssc in role.self_service or []:
|
58
|
+
if ssc.change_type.context_schema:
|
59
|
+
# check that all referenced datafiles have a schema that
|
60
|
+
# is compatible with the change-type
|
61
|
+
incompatible_datafiles = [
|
62
|
+
df.path
|
63
|
+
for df in ssc.datafiles or []
|
64
|
+
if df.datafile_schema != ssc.change_type.context_schema
|
65
|
+
]
|
66
|
+
if incompatible_datafiles:
|
67
|
+
raise DatafileIncompatibleWithChangeTypeError(
|
68
|
+
f"The datafiles {incompatible_datafiles} are not compatible with the "
|
69
|
+
f"{ssc.change_type.name} change-types contextSchema {ssc.change_type.context_schema}"
|
70
|
+
)
|
24
71
|
|
25
72
|
|
26
73
|
def cover_changes_with_self_service_roles(
|
@@ -66,11 +113,6 @@ def change_type_contexts_for_self_service_roles(
|
|
66
113
|
role_lookup[
|
67
114
|
(BundleFileType.RESOURCEFILE, res, ss.change_type.name)
|
68
115
|
].append(r)
|
69
|
-
if orphaned_roles:
|
70
|
-
raise EmptySelfServiceRoleError(
|
71
|
-
f"The roles {', '.join([r.name for r in orphaned_roles])} have no users or bots "
|
72
|
-
"to drive the self-service process. Add approvers to the roles."
|
73
|
-
)
|
74
116
|
|
75
117
|
# match every BundleChange with every relevant ChangeTypeV1
|
76
118
|
change_type_contexts = []
|
reconcile/cli.py
CHANGED
@@ -966,6 +966,15 @@ def aws_ami_share(ctx):
|
|
966
966
|
run_integration(reconcile.aws_ami_share, ctx.obj)
|
967
967
|
|
968
968
|
|
969
|
+
@integration.command(short_help="Cleanup old and unused AMIs.")
|
970
|
+
@threaded()
|
971
|
+
@click.pass_context
|
972
|
+
def aws_ami_cleanup(ctx, thread_pool_size):
|
973
|
+
import reconcile.aws_ami_cleanup.integration
|
974
|
+
|
975
|
+
run_integration(reconcile.aws_ami_cleanup.integration, ctx.obj, thread_pool_size)
|
976
|
+
|
977
|
+
|
969
978
|
@integration.command(
|
970
979
|
short_help="Generate AWS ECR image pull secrets and store them in Vault."
|
971
980
|
)
|
File without changes
|
@@ -0,0 +1,132 @@
|
|
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
|
+
DEFINITION = """
|
22
|
+
query ASGNamespaces {
|
23
|
+
namespaces: namespaces_v1 {
|
24
|
+
name
|
25
|
+
externalResources {
|
26
|
+
provider
|
27
|
+
provisioner {
|
28
|
+
name
|
29
|
+
}
|
30
|
+
... on NamespaceTerraformProviderResourceAWS_v1 {
|
31
|
+
resources {
|
32
|
+
provider
|
33
|
+
... on NamespaceTerraformResourceASG_v1 {
|
34
|
+
identifier
|
35
|
+
image {
|
36
|
+
provider
|
37
|
+
... on ASGImageGit_v1 {
|
38
|
+
tag_name
|
39
|
+
url
|
40
|
+
ref
|
41
|
+
}
|
42
|
+
... on ASGImageStatic_v1 {
|
43
|
+
tag_name
|
44
|
+
value
|
45
|
+
}
|
46
|
+
}
|
47
|
+
}
|
48
|
+
}
|
49
|
+
}
|
50
|
+
}
|
51
|
+
}
|
52
|
+
}
|
53
|
+
"""
|
54
|
+
|
55
|
+
|
56
|
+
class ConfiguredBaseModel(BaseModel):
|
57
|
+
class Config:
|
58
|
+
smart_union = True
|
59
|
+
extra = Extra.forbid
|
60
|
+
|
61
|
+
|
62
|
+
class ExternalResourcesProvisionerV1(ConfiguredBaseModel):
|
63
|
+
name: str = Field(..., alias="name")
|
64
|
+
|
65
|
+
|
66
|
+
class NamespaceExternalResourceV1(ConfiguredBaseModel):
|
67
|
+
provider: str = Field(..., alias="provider")
|
68
|
+
provisioner: ExternalResourcesProvisionerV1 = Field(..., alias="provisioner")
|
69
|
+
|
70
|
+
|
71
|
+
class NamespaceTerraformResourceAWSV1(ConfiguredBaseModel):
|
72
|
+
provider: str = Field(..., alias="provider")
|
73
|
+
|
74
|
+
|
75
|
+
class ASGImageV1(ConfiguredBaseModel):
|
76
|
+
provider: str = Field(..., alias="provider")
|
77
|
+
|
78
|
+
|
79
|
+
class ASGImageGitV1(ASGImageV1):
|
80
|
+
tag_name: str = Field(..., alias="tag_name")
|
81
|
+
url: str = Field(..., alias="url")
|
82
|
+
ref: str = Field(..., alias="ref")
|
83
|
+
|
84
|
+
|
85
|
+
class ASGImageStaticV1(ASGImageV1):
|
86
|
+
tag_name: str = Field(..., alias="tag_name")
|
87
|
+
value: str = Field(..., alias="value")
|
88
|
+
|
89
|
+
|
90
|
+
class NamespaceTerraformResourceASGV1(NamespaceTerraformResourceAWSV1):
|
91
|
+
identifier: str = Field(..., alias="identifier")
|
92
|
+
image: list[Union[ASGImageGitV1, ASGImageStaticV1, ASGImageV1]] = Field(
|
93
|
+
..., alias="image"
|
94
|
+
)
|
95
|
+
|
96
|
+
|
97
|
+
class NamespaceTerraformProviderResourceAWSV1(NamespaceExternalResourceV1):
|
98
|
+
resources: list[
|
99
|
+
Union[NamespaceTerraformResourceASGV1, NamespaceTerraformResourceAWSV1]
|
100
|
+
] = Field(..., alias="resources")
|
101
|
+
|
102
|
+
|
103
|
+
class NamespaceV1(ConfiguredBaseModel):
|
104
|
+
name: str = Field(..., alias="name")
|
105
|
+
external_resources: Optional[
|
106
|
+
list[
|
107
|
+
Union[NamespaceTerraformProviderResourceAWSV1, NamespaceExternalResourceV1]
|
108
|
+
]
|
109
|
+
] = Field(..., alias="externalResources")
|
110
|
+
|
111
|
+
|
112
|
+
class ASGNamespacesQueryData(ConfiguredBaseModel):
|
113
|
+
namespaces: Optional[list[NamespaceV1]] = Field(..., alias="namespaces")
|
114
|
+
|
115
|
+
|
116
|
+
def query(query_func: Callable, **kwargs: Any) -> ASGNamespacesQueryData:
|
117
|
+
"""
|
118
|
+
This is a convenience function which queries and parses the data into
|
119
|
+
concrete types. It should be compatible with most GQL clients.
|
120
|
+
You do not have to use it to consume the generated data classes.
|
121
|
+
Alternatively, you can also mime and alternate the behavior
|
122
|
+
of this function in the caller.
|
123
|
+
|
124
|
+
Parameters:
|
125
|
+
query_func (Callable): Function which queries your GQL Server
|
126
|
+
kwargs: optional arguments that will be passed to the query function
|
127
|
+
|
128
|
+
Returns:
|
129
|
+
ASGNamespacesQueryData: queried data parsed into generated classes
|
130
|
+
"""
|
131
|
+
raw_data: dict[Any, Any] = query_func(DEFINITION, **kwargs)
|
132
|
+
return ASGNamespacesQueryData(**raw_data)
|
reconcile/queries.py
CHANGED
@@ -535,6 +535,16 @@ AWS_ACCOUNTS_QUERY = """
|
|
535
535
|
}
|
536
536
|
}
|
537
537
|
{% endif %}
|
538
|
+
{% if cleanup %}
|
539
|
+
cleanup {
|
540
|
+
provider
|
541
|
+
... on AWSAccountCleanupOptionAMI_v1 {
|
542
|
+
regex
|
543
|
+
age
|
544
|
+
region
|
545
|
+
}
|
546
|
+
}
|
547
|
+
{% endif %}
|
538
548
|
}
|
539
549
|
}
|
540
550
|
"""
|
@@ -547,6 +557,7 @@ def get_aws_accounts(
|
|
547
557
|
sharing=False,
|
548
558
|
terraform_state=False,
|
549
559
|
ecrs=True,
|
560
|
+
cleanup=False,
|
550
561
|
):
|
551
562
|
"""Returns all AWS accounts"""
|
552
563
|
gqlapi = gql.get_api()
|
@@ -559,6 +570,7 @@ def get_aws_accounts(
|
|
559
570
|
sharing=sharing,
|
560
571
|
terraform_state=terraform_state,
|
561
572
|
ecrs=ecrs,
|
573
|
+
cleanup=cleanup,
|
562
574
|
)
|
563
575
|
return gqlapi.query(query)["accounts"]
|
564
576
|
|
@@ -0,0 +1,296 @@
|
|
1
|
+
# pylint does not consider frozen BaseModels as hashable and then complains that they cannot
|
2
|
+
# be members of a set.
|
3
|
+
# pylint: disable=unhashable-member
|
4
|
+
|
5
|
+
from collections.abc import Generator
|
6
|
+
from datetime import (
|
7
|
+
datetime,
|
8
|
+
timedelta,
|
9
|
+
)
|
10
|
+
from typing import (
|
11
|
+
TYPE_CHECKING,
|
12
|
+
Any,
|
13
|
+
)
|
14
|
+
from unittest.mock import MagicMock
|
15
|
+
|
16
|
+
import boto3
|
17
|
+
import pytest
|
18
|
+
from moto import mock_ec2
|
19
|
+
from pytest_mock import MockerFixture
|
20
|
+
|
21
|
+
from reconcile.aws_ami_cleanup.integration import (
|
22
|
+
AIAmi,
|
23
|
+
AmiTag,
|
24
|
+
AWSAmi,
|
25
|
+
CannotCompareTagsError,
|
26
|
+
check_aws_ami_in_use,
|
27
|
+
get_app_interface_amis,
|
28
|
+
get_aws_amis,
|
29
|
+
)
|
30
|
+
from reconcile.gql_definitions.aws_ami_cleanup.asg_namespaces import (
|
31
|
+
ASGNamespacesQueryData,
|
32
|
+
)
|
33
|
+
from reconcile.test.fixtures import Fixtures
|
34
|
+
from reconcile.utils.aws_api import AWSApi
|
35
|
+
from reconcile.utils.terrascript_aws_client import TerrascriptClient as Terrascript
|
36
|
+
|
37
|
+
if TYPE_CHECKING:
|
38
|
+
from mypy_boto3_ec2 import EC2Client
|
39
|
+
from mypy_boto3_ec2.type_defs import CreateImageResultTypeDef
|
40
|
+
else:
|
41
|
+
EC2Client = object
|
42
|
+
CreateImageResultTypeDef = dict
|
43
|
+
|
44
|
+
MOTO_DEFAULT_ACCOUNT = "123456789012"
|
45
|
+
|
46
|
+
|
47
|
+
@pytest.fixture
|
48
|
+
def accounts() -> list[dict[str, Any]]:
|
49
|
+
return [
|
50
|
+
{
|
51
|
+
"name": "some-account",
|
52
|
+
"automationToken": {
|
53
|
+
"path": "path",
|
54
|
+
},
|
55
|
+
"resourcesDefaultRegion": "default-region",
|
56
|
+
}
|
57
|
+
]
|
58
|
+
|
59
|
+
|
60
|
+
@pytest.fixture
|
61
|
+
def aws_api(accounts: list[dict[str, Any]], mocker: MockerFixture) -> AWSApi:
|
62
|
+
mock_secret_reader = mocker.patch(
|
63
|
+
"reconcile.utils.aws_api.SecretReader", autospec=True
|
64
|
+
)
|
65
|
+
mock_secret_reader.return_value.read_all.return_value = {
|
66
|
+
"aws_access_key_id": "key_id",
|
67
|
+
"aws_secret_access_key": "access_key",
|
68
|
+
"region": "tf_state_bucket_region",
|
69
|
+
}
|
70
|
+
return AWSApi(1, accounts, init_users=False)
|
71
|
+
|
72
|
+
|
73
|
+
@pytest.fixture
|
74
|
+
def ec2_client() -> Generator[EC2Client, None, None]:
|
75
|
+
with mock_ec2():
|
76
|
+
yield boto3.client("ec2", region_name="us-east-1")
|
77
|
+
|
78
|
+
|
79
|
+
@pytest.fixture
|
80
|
+
def rhel_image(ec2_client: EC2Client) -> CreateImageResultTypeDef:
|
81
|
+
# RHEL7 ami from moto/ec2/resources/amis.json
|
82
|
+
reservation = ec2_client.run_instances(
|
83
|
+
ImageId="ami-bb9a6bc2", MinCount=1, MaxCount=1
|
84
|
+
)
|
85
|
+
instance_id = reservation["Instances"][0]["InstanceId"]
|
86
|
+
|
87
|
+
return ec2_client.create_image(
|
88
|
+
InstanceId=instance_id,
|
89
|
+
Name="ci-int-jenkins-worker-rhel7-sha-123456",
|
90
|
+
TagSpecifications=[
|
91
|
+
{
|
92
|
+
"ResourceType": "image",
|
93
|
+
"Tags": [
|
94
|
+
{"Key": "infra_commit", "Value": "sha-123456"},
|
95
|
+
{"Key": "type", "Value": "ci-int-jenkins-worker-rhel7"},
|
96
|
+
],
|
97
|
+
},
|
98
|
+
],
|
99
|
+
)
|
100
|
+
|
101
|
+
|
102
|
+
@pytest.fixture
|
103
|
+
def suse_image(ec2_client: EC2Client) -> CreateImageResultTypeDef:
|
104
|
+
# SUSE AMI from moto/ec2/resources/amis.json
|
105
|
+
reservation = ec2_client.run_instances(
|
106
|
+
ImageId="ami-35e92e4c", MinCount=1, MaxCount=1
|
107
|
+
)
|
108
|
+
instance_id = reservation["Instances"][0]["InstanceId"]
|
109
|
+
|
110
|
+
return ec2_client.create_image(
|
111
|
+
InstanceId=instance_id,
|
112
|
+
Name="ci-int-jenkins-worker-suse12-sha-789012",
|
113
|
+
TagSpecifications=[
|
114
|
+
{
|
115
|
+
"ResourceType": "image",
|
116
|
+
"Tags": [
|
117
|
+
{"Key": "infra_commit", "Value": "sha-789012"},
|
118
|
+
{"Key": "arch", "Value": "ci-int-jenkins-worker-suse12"},
|
119
|
+
],
|
120
|
+
},
|
121
|
+
],
|
122
|
+
)
|
123
|
+
|
124
|
+
|
125
|
+
@pytest.fixture
|
126
|
+
def ai_amis_fxt() -> list[AIAmi]:
|
127
|
+
return [
|
128
|
+
AIAmi(
|
129
|
+
identifier="ci-int-jenkins-worker-app-sre",
|
130
|
+
tags={
|
131
|
+
AmiTag(key="type", value="ci-int-jenkins-worker-app-sre"),
|
132
|
+
AmiTag(
|
133
|
+
key="infra_commit",
|
134
|
+
value="sha-0123",
|
135
|
+
),
|
136
|
+
},
|
137
|
+
),
|
138
|
+
AIAmi(
|
139
|
+
identifier="ci-int-jenkins-worker-app-interface",
|
140
|
+
tags={
|
141
|
+
AmiTag(key="type", value="ci-int-jenkins-worker-app-interface"),
|
142
|
+
AmiTag(
|
143
|
+
key="infra_commit",
|
144
|
+
value="sha-4567",
|
145
|
+
),
|
146
|
+
},
|
147
|
+
),
|
148
|
+
]
|
149
|
+
|
150
|
+
|
151
|
+
def test_get_aws_amis_success(
|
152
|
+
ec2_client: EC2Client,
|
153
|
+
aws_api: AWSApi,
|
154
|
+
rhel_image: CreateImageResultTypeDef,
|
155
|
+
suse_image: CreateImageResultTypeDef,
|
156
|
+
) -> None:
|
157
|
+
utc_now = datetime.utcnow() + timedelta(seconds=60)
|
158
|
+
amis = get_aws_amis(
|
159
|
+
aws_api=aws_api,
|
160
|
+
ec2_client=ec2_client,
|
161
|
+
owner=MOTO_DEFAULT_ACCOUNT,
|
162
|
+
regex="ci-int-jenkins-worker-rhel7.*",
|
163
|
+
age_in_seconds=30,
|
164
|
+
utc_now=utc_now,
|
165
|
+
region="us-east-1",
|
166
|
+
)
|
167
|
+
|
168
|
+
assert len(amis) == 1
|
169
|
+
assert amis[0].image_id == rhel_image["ImageId"]
|
170
|
+
|
171
|
+
|
172
|
+
def test_get_aws_amis_unmatched_regex(
|
173
|
+
ec2_client: EC2Client,
|
174
|
+
aws_api: AWSApi,
|
175
|
+
rhel_image: CreateImageResultTypeDef,
|
176
|
+
suse_image: CreateImageResultTypeDef,
|
177
|
+
) -> None:
|
178
|
+
utc_now = datetime.utcnow() + timedelta(seconds=60)
|
179
|
+
amis = get_aws_amis(
|
180
|
+
aws_api=aws_api,
|
181
|
+
ec2_client=ec2_client,
|
182
|
+
owner=MOTO_DEFAULT_ACCOUNT,
|
183
|
+
regex="ci-int-jenkins-worker-centos7.*",
|
184
|
+
age_in_seconds=30,
|
185
|
+
utc_now=utc_now,
|
186
|
+
region="us-east-1",
|
187
|
+
)
|
188
|
+
|
189
|
+
assert len(amis) == 0
|
190
|
+
|
191
|
+
|
192
|
+
def test_get_aws_amis_different_account(
|
193
|
+
ec2_client: EC2Client,
|
194
|
+
aws_api: AWSApi,
|
195
|
+
rhel_image: CreateImageResultTypeDef,
|
196
|
+
suse_image: CreateImageResultTypeDef,
|
197
|
+
) -> None:
|
198
|
+
utc_now = datetime.utcnow() + timedelta(seconds=60)
|
199
|
+
amis = get_aws_amis(
|
200
|
+
aws_api=aws_api,
|
201
|
+
ec2_client=ec2_client,
|
202
|
+
owner="789123456789",
|
203
|
+
regex="ci-int-jenkins-worker-rhel7.*",
|
204
|
+
age_in_seconds=30,
|
205
|
+
utc_now=utc_now,
|
206
|
+
region="us-east-1",
|
207
|
+
)
|
208
|
+
|
209
|
+
assert len(amis) == 0
|
210
|
+
|
211
|
+
|
212
|
+
def test_get_aws_amis_too_young(
|
213
|
+
ec2_client: EC2Client,
|
214
|
+
aws_api: AWSApi,
|
215
|
+
rhel_image: CreateImageResultTypeDef,
|
216
|
+
suse_image: CreateImageResultTypeDef,
|
217
|
+
) -> None:
|
218
|
+
utc_now = datetime.utcnow() + timedelta(seconds=60)
|
219
|
+
amis = get_aws_amis(
|
220
|
+
aws_api=aws_api,
|
221
|
+
ec2_client=ec2_client,
|
222
|
+
owner=MOTO_DEFAULT_ACCOUNT,
|
223
|
+
regex="ci-int-jenkins-worker-rhel7.*",
|
224
|
+
age_in_seconds=90,
|
225
|
+
utc_now=utc_now,
|
226
|
+
region="us-east-1",
|
227
|
+
)
|
228
|
+
|
229
|
+
assert len(amis) == 0
|
230
|
+
|
231
|
+
|
232
|
+
def test_get_app_interface_amis(ai_amis_fxt: list[AIAmi]) -> None:
|
233
|
+
fixture = Fixtures("aws_ami_cleanup").get_anymarkup("namespaces.yaml")
|
234
|
+
namespaces = ASGNamespacesQueryData(**fixture).namespaces
|
235
|
+
ts = MagicMock(spec=Terrascript)
|
236
|
+
ts.get_commit_sha.side_effect = ["sha-0123", "sha-4567"]
|
237
|
+
|
238
|
+
app_interface_amis = get_app_interface_amis(namespaces, ts)
|
239
|
+
assert app_interface_amis[0].identifier == ai_amis_fxt[0].identifier
|
240
|
+
assert app_interface_amis[0].tags == ai_amis_fxt[0].tags
|
241
|
+
assert app_interface_amis[1].identifier == ai_amis_fxt[1].identifier
|
242
|
+
assert app_interface_amis[1].tags == ai_amis_fxt[1].tags
|
243
|
+
|
244
|
+
|
245
|
+
def test_check_aws_ami_in_use(ai_amis_fxt: list[AIAmi]) -> None:
|
246
|
+
utc_now = datetime.utcnow()
|
247
|
+
aws_ami = AWSAmi(
|
248
|
+
name="ci-int-jenkins-worker-app-sre-sha-0123",
|
249
|
+
image_id="ami-123456",
|
250
|
+
creation_date=utc_now,
|
251
|
+
tags={
|
252
|
+
AmiTag(key="infra_commit", value="sha-0123"),
|
253
|
+
AmiTag(key="type", value="ci-int-jenkins-worker-app-sre"),
|
254
|
+
},
|
255
|
+
)
|
256
|
+
assert check_aws_ami_in_use(aws_ami, ai_amis_fxt) == "ci-int-jenkins-worker-app-sre"
|
257
|
+
|
258
|
+
aws_ami = AWSAmi(
|
259
|
+
name="ci-int-jenkins-worker-app-interface-sha-4567",
|
260
|
+
image_id="ami-823445",
|
261
|
+
creation_date=utc_now,
|
262
|
+
tags={
|
263
|
+
AmiTag(key="type", value="ci-int-jenkins-worker-app-interface"),
|
264
|
+
AmiTag(key="infra_commit", value="sha-4567"),
|
265
|
+
},
|
266
|
+
)
|
267
|
+
assert (
|
268
|
+
check_aws_ami_in_use(aws_ami, ai_amis_fxt)
|
269
|
+
== "ci-int-jenkins-worker-app-interface"
|
270
|
+
)
|
271
|
+
|
272
|
+
aws_ami = AWSAmi(
|
273
|
+
name="ci-int-jenkins-worker-app-interface-a-different-sha",
|
274
|
+
image_id="ami-823445",
|
275
|
+
creation_date=utc_now,
|
276
|
+
tags={
|
277
|
+
AmiTag(key="type", value="ci-int-jenkins-worker-app-interface"),
|
278
|
+
AmiTag(key="infra_commit", value="a-different-sha"),
|
279
|
+
},
|
280
|
+
)
|
281
|
+
|
282
|
+
assert not check_aws_ami_in_use(aws_ami, ai_amis_fxt)
|
283
|
+
|
284
|
+
aws_ami = AWSAmi(
|
285
|
+
name="ci-int-jenkins-worker-app-interface-a-weird-one",
|
286
|
+
image_id="ami-823445",
|
287
|
+
creation_date=utc_now,
|
288
|
+
tags={
|
289
|
+
AmiTag(key="type", value="ci-int-jenkins-worker-app-interface"),
|
290
|
+
},
|
291
|
+
)
|
292
|
+
|
293
|
+
with pytest.raises(CannotCompareTagsError) as excinfo:
|
294
|
+
check_aws_ami_in_use(aws_ami, ai_amis_fxt)
|
295
|
+
|
296
|
+
assert "AI AMI has more tags than" in str(excinfo.value)
|
File without changes
|
{qontract_reconcile-0.10.1rc58.dist-info → qontract_reconcile-0.10.1rc60.dist-info}/entry_points.txt
RENAMED
File without changes
|
{qontract_reconcile-0.10.1rc58.dist-info → qontract_reconcile-0.10.1rc60.dist-info}/top_level.txt
RENAMED
File without changes
|