qontract-reconcile 0.10.1rc1052__py3-none-any.whl → 0.10.1rc1054__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: qontract-reconcile
3
- Version: 0.10.1rc1052
3
+ Version: 0.10.1rc1054
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
@@ -145,7 +145,7 @@ reconcile/aws_account_manager/metrics.py,sha256=YB10ea4kIGwJfs5N14RF-RoXPb-QQWaD
145
145
  reconcile/aws_account_manager/reconciler.py,sha256=8mwwcWVVNoUzmttzxgnLyePqN823v1t_dQCNCrx1mG0,15035
146
146
  reconcile/aws_account_manager/utils.py,sha256=iYPPOtbZ7FiKkz9v5f1YXRIHw5YFOtSavUkF8oMwfJY,1439
147
147
  reconcile/aws_ami_cleanup/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
148
- reconcile/aws_ami_cleanup/integration.py,sha256=JkKOcH_7uKBmjdRK1UUoWseUik3kBIjwdtRdPMUEkBY,10086
148
+ reconcile/aws_ami_cleanup/integration.py,sha256=KG7g9NpbKmoaveDD3oi9SinqUE29NaM-4lGo-6YuHlM,9302
149
149
  reconcile/aws_cloudwatch_log_retention/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
150
150
  reconcile/aws_cloudwatch_log_retention/integration.py,sha256=0UcSZIrGvnGY4m9fj87oejIolIP_qTxtJInpmW9jrQ0,7772
151
151
  reconcile/aws_saml_idp/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -222,7 +222,7 @@ reconcile/gql_definitions/app_interface_metrics_exporter/onboarding_status.py,sh
222
222
  reconcile/gql_definitions/aws_account_manager/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
223
223
  reconcile/gql_definitions/aws_account_manager/aws_accounts.py,sha256=JdqtE3gMpeodymPJST-aFVkYP_MO--_CcwjF070R5Cs,4883
224
224
  reconcile/gql_definitions/aws_ami_cleanup/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
225
- reconcile/gql_definitions/aws_ami_cleanup/asg_namespaces.py,sha256=OJmeTu7uirLGAysZ3IQTtRXqMyL8noi_QZxPuWYxxmI,3678
225
+ reconcile/gql_definitions/aws_ami_cleanup/aws_accounts.py,sha256=jIgOa888MYLLvVsn1ir3nbkhWLG5T6dBg7oDnp1q8BI,4108
226
226
  reconcile/gql_definitions/aws_saml_idp/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
227
227
  reconcile/gql_definitions/aws_saml_idp/aws_accounts.py,sha256=pR9Qm6P9Roe4OJaDXvfm8AcfkSSAtriQdlwLwW7UdUU,2666
228
228
  reconcile/gql_definitions/aws_saml_roles/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -269,7 +269,7 @@ reconcile/gql_definitions/common/pipeline_providers.py,sha256=JJgmmghqLIwjKOdcWY
269
269
  reconcile/gql_definitions/common/quay_instances.py,sha256=toBkdYYVTmEafezAHZKgaW-mQ29xEW6jeronzsAlNyI,1786
270
270
  reconcile/gql_definitions/common/quay_orgs.py,sha256=NhA8kqvVUDbrsryEvEL5mlIv5R3T4XNhSRXtfL_yptY,1788
271
271
  reconcile/gql_definitions/common/reserved_networks.py,sha256=yP9qSQCaSQcva-ZgTnZp09qH27ur5_qK080ToIs04MY,2560
272
- reconcile/gql_definitions/common/saas_files.py,sha256=JZdFKBygaZVxGwBUYMki9EbnsGdmfmxuAreMZ3dchwo,16702
272
+ reconcile/gql_definitions/common/saas_files.py,sha256=i0U0Ot4DwE3LjigD4ex_ASOPWfSZEpX_4ZrRaWlUhW8,16854
273
273
  reconcile/gql_definitions/common/saas_target_namespaces.py,sha256=4VYP2VbwY8WVwtSFk2-jsUNhSmRD3X4FWKxetOKvmd0,2835
274
274
  reconcile/gql_definitions/common/saasherder_settings.py,sha256=nqQLcMwYxLseqq0BEcVvmrpIj2eQq0h8XDSpLN6GGCw,1793
275
275
  reconcile/gql_definitions/common/slack_workspaces.py,sha256=2o0kgi4QiaRuNmZJnc_By4F6NsKIdRaXkrufRQw7Nok,1753
@@ -443,8 +443,8 @@ reconcile/rhidp/sso_client/metrics.py,sha256=Tq7tSOsqL3XdcPUdozxqzSPIodUeOV87UCT
443
443
  reconcile/saas_auto_promotions_manager/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
444
444
  reconcile/saas_auto_promotions_manager/integration.py,sha256=haXTpwi0rEQtN9-MadBzFvSNtbxw5QQuFpMQLg3bZlQ,6707
445
445
  reconcile/saas_auto_promotions_manager/meta.py,sha256=76Jp50r6Y_KyJoXFfSjrt5YrCtXyg_A4FXXxHYiS3TE,161
446
- reconcile/saas_auto_promotions_manager/publisher.py,sha256=IZGu-PMffyk3fNL8QcZ2VBmYTZ5zigoCwNJ4_Ak6-C8,2966
447
- reconcile/saas_auto_promotions_manager/s3_exporter.py,sha256=IKlVWZmiPnvl7sKeF6JgAlhXZe5CovKTxQc0SNkNSx4,2583
446
+ reconcile/saas_auto_promotions_manager/publisher.py,sha256=_VdmCTiPpOBQBkGRpD6528TGdOV90wRme1q8ep_qo48,2993
447
+ reconcile/saas_auto_promotions_manager/s3_exporter.py,sha256=Y-r5R6viiAzglUHbYKItYSjT_axmLlPEJVmu_H6N170,2682
448
448
  reconcile/saas_auto_promotions_manager/subscriber.py,sha256=kIJKT1Xg5FF7EGT5ayK5ROyu_S7FbsYK91WBqrtYjfA,10030
449
449
  reconcile/saas_auto_promotions_manager/merge_request_manager/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
450
450
  reconcile/saas_auto_promotions_manager/merge_request_manager/batcher.py,sha256=R2CRtjdOggY5lSqt-A5qz2ymqomx4opVeVV_oqBAW0A,7804
@@ -456,7 +456,7 @@ reconcile/saas_auto_promotions_manager/merge_request_manager/mr_parser.py,sha256
456
456
  reconcile/saas_auto_promotions_manager/merge_request_manager/open_merge_requests.py,sha256=-qGQOh6Jdp4lomNDij3zWVC0pl6uPHFWS5Woqcp5HQk,410
457
457
  reconcile/saas_auto_promotions_manager/merge_request_manager/renderer.py,sha256=rnY9CR_0ONDJdJhGaGOQ9yBlKV0bPXQKh9Ped-NA2To,7245
458
458
  reconcile/saas_auto_promotions_manager/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
459
- reconcile/saas_auto_promotions_manager/utils/saas_files_inventory.py,sha256=DZOT8jvM9tmLnipY884XN5T4ViJljAlcEz6XKdNCK5w,8860
459
+ reconcile/saas_auto_promotions_manager/utils/saas_files_inventory.py,sha256=wj7x3DIoetLX5QeaorTH0i8QC5GWsqahbUOrUb0AzJk,8830
460
460
  reconcile/skupper_network/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
461
461
  reconcile/skupper_network/integration.py,sha256=oZIVDBRcQPC-lWxNFiJhGbtCM7Yj7fjwAzYZ8JvVe3I,10789
462
462
  reconcile/skupper_network/models.py,sha256=HEwlVKsbmMaKaaBGvITIiSYNEVdjwXVhLaOJgLSZ2xQ,6604
@@ -500,7 +500,7 @@ reconcile/test/test_acs_policies.py,sha256=8pwnXpAO-0OI-6oubjf_oPPlpZjVldeZfJJ9u
500
500
  reconcile/test/test_acs_rbac.py,sha256=lvNd8GY0-GHzcOdOn13QWdrqbBXXKzNT7EEDHNH7cjM,28272
501
501
  reconcile/test/test_aggregated_list.py,sha256=iiWitQuNYC58aimWaiBoE4NROHjr1NCgQ91MnHEG_Ro,6412
502
502
  reconcile/test/test_amtool.py,sha256=vxRhGieeydMBOb9UI2ziMHjJa8puMeGNsUhGhy-yMnk,1032
503
- reconcile/test/test_aws_ami_cleanup.py,sha256=cHumkZD33vIIblNbrMKNCva-WeE3sMv8kiFA0A96wwk,8733
503
+ reconcile/test/test_aws_ami_cleanup.py,sha256=s9E82ENpKrzsy9_KTj0tbQN2MVXeacq-_Ztw7FWbq7Q,6603
504
504
  reconcile/test/test_aws_ami_share.py,sha256=eSITdDoXs8mMY7P2lFxAX2DA0sJ9RW6D1tG8Rek0gLE,1981
505
505
  reconcile/test/test_aws_cloudwatch_log_retention.py,sha256=fOUq_vJldGPjXKBdRQsSv4zFz_qenVtRKJFKpbz8BlA,13712
506
506
  reconcile/test/test_aws_iam_keys.py,sha256=MfE9EvItyPNPAl5QaLlJFUvvrZFiar518TM2wWNjJn4,1829
@@ -553,7 +553,7 @@ reconcile/test/test_quay_repos.py,sha256=TdkcRF_a8PLp01Kti9eZZN-vGup2yPBT4Iba3k0
553
553
  reconcile/test/test_queries.py,sha256=SpH3RmNpBjEr_ne3VjAMCgKK8RE1z1zo7bypkT5uoO4,1946
554
554
  reconcile/test/test_repo_owners.py,sha256=uRYMLbMmh-9usF0TerabZTZV-Z1CS4I6ybT-LQqCLe8,1423
555
555
  reconcile/test/test_requests_sender.py,sha256=7fd9C2kEFS0-CYtlsif66N1kO9c44pzuBPAJKR9igqU,5385
556
- reconcile/test/test_saasherder.py,sha256=SXhjDhpmWNM3xWtOGWn5j6BeznuXDwlttVcUtp9drpo,58014
556
+ reconcile/test/test_saasherder.py,sha256=-WGgAsrTm_SJ0OQDLXd4JwEu1Mc6i4AePJqjZQTaRew,58114
557
557
  reconcile/test/test_saasherder_allowed_secret_paths.py,sha256=5NHQwNJO66at6HiyMZ5sVRTQDwxdvlOQo0KmkBWCw5Q,4853
558
558
  reconcile/test/test_secret_reader.py,sha256=kz7nzcPjvA08cytnvcA_PMA98AEyqJWsESkYeRn5xCk,4994
559
559
  reconcile/test/test_slack_base.py,sha256=pTUGvJ2S2wF3PhJyGWmiNXG52QtXKy2cbu-G8Ymrv6I,5019
@@ -578,7 +578,7 @@ reconcile/test/test_version_bump.py,sha256=q6-3Y1roriI6YWpFwaHOMN7emEP3yL33sh_0V
578
578
  reconcile/test/test_vpc_peerings_validator.py,sha256=dFSmjc_dMN2GqMbntCFpa7PUZmyYuQ9DKffh-T5wmxM,6639
579
579
  reconcile/test/test_wrong_region.py,sha256=7KzL7OaICQ9Z3DW27zt_ykMN7_87owAFC-2CYjvGoyA,2138
580
580
  reconcile/test/saas_auto_promotions_manager/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
581
- reconcile/test/saas_auto_promotions_manager/conftest.py,sha256=oRnwwFbL00TNwY1uacjPl0cwU3o064YpDhNkeoerGzE,6085
581
+ reconcile/test/saas_auto_promotions_manager/conftest.py,sha256=m4gOYQIOO9Br9r26erJLXxO6b1IfVKO-QQPm8k2qD5Y,6094
582
582
  reconcile/test/saas_auto_promotions_manager/test_integration_test.py,sha256=S30eXJSy2Vc3YLbCP7AfLkOiFGUVoKhEvEBL5vwnbfg,1848
583
583
  reconcile/test/saas_auto_promotions_manager/merge_request_manager/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
584
584
  reconcile/test/saas_auto_promotions_manager/merge_request_manager/merge_request_manager/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -867,8 +867,8 @@ tools/test/test_qontract_cli.py,sha256=_D61RFGAN5x44CY1tYbouhlGXXABwYfxKSWSQx3Jr
867
867
  tools/test/test_saas_promotion_state.py,sha256=dy4kkSSAQ7bC0Xp2CociETGN-2aABEfL6FU5D9Jl00Y,6056
868
868
  tools/test/test_sd_app_sre_alert_report.py,sha256=v363r9zM7__0kR5K6mvJoGFcM9BvE33fWAayrqkpojA,2116
869
869
  tools/test/test_sre_checkpoints.py,sha256=SKqPPTl9ua0RFdSSofnoQX-JZE6dFLO3LRhfQzqtfh8,2607
870
- qontract_reconcile-0.10.1rc1052.dist-info/METADATA,sha256=vApqFMxNENnXiau-v2JHl7G_NMa4KoDW4q9N9CAFcvM,2213
871
- qontract_reconcile-0.10.1rc1052.dist-info/WHEEL,sha256=eOLhNAGa2EW3wWl_TU484h7q1UNgy0JXjjoqKoxAAQc,92
872
- qontract_reconcile-0.10.1rc1052.dist-info/entry_points.txt,sha256=GKQqCl2j2X1BJQ69een6rHcR26PmnxnONLNOQB-nRjY,491
873
- qontract_reconcile-0.10.1rc1052.dist-info/top_level.txt,sha256=l5ISPoXzt0SdR4jVdkfa7RPSKNc8zAHYWAnR-Dw8Ey8,24
874
- qontract_reconcile-0.10.1rc1052.dist-info/RECORD,,
870
+ qontract_reconcile-0.10.1rc1054.dist-info/METADATA,sha256=964pbKmbJyE8OC_hM19ZLF0So2_dQGW_zoQNeh6ar7U,2213
871
+ qontract_reconcile-0.10.1rc1054.dist-info/WHEEL,sha256=eOLhNAGa2EW3wWl_TU484h7q1UNgy0JXjjoqKoxAAQc,92
872
+ qontract_reconcile-0.10.1rc1054.dist-info/entry_points.txt,sha256=GKQqCl2j2X1BJQ69een6rHcR26PmnxnONLNOQB-nRjY,491
873
+ qontract_reconcile-0.10.1rc1054.dist-info/top_level.txt,sha256=l5ISPoXzt0SdR4jVdkfa7RPSKNc8zAHYWAnR-Dw8Ey8,24
874
+ qontract_reconcile-0.10.1rc1054.dist-info/RECORD,,
@@ -1,9 +1,8 @@
1
1
  import logging
2
2
  import re
3
- import sys
3
+ from collections import defaultdict
4
4
  from collections.abc import (
5
5
  Callable,
6
- Mapping,
7
6
  )
8
7
  from datetime import (
9
8
  datetime,
@@ -11,27 +10,20 @@ from datetime import (
11
10
  )
12
11
  from typing import (
13
12
  TYPE_CHECKING,
14
- Any,
15
13
  )
16
14
 
17
15
  from botocore.exceptions import ClientError
18
16
  from pydantic import (
19
17
  BaseModel,
20
- Field,
21
18
  )
22
19
 
23
- from reconcile import queries
24
- from reconcile.gql_definitions.aws_ami_cleanup.asg_namespaces import (
25
- ASGImageGitV1,
26
- ASGImageStaticV1,
27
- NamespaceTerraformProviderResourceAWSV1,
28
- NamespaceTerraformResourceASGV1,
29
- NamespaceV1,
20
+ from reconcile.gql_definitions.aws_ami_cleanup.aws_accounts import (
21
+ AWSAccountCleanupOptionAMIV1,
22
+ AWSAccountSharingOptionAMIV1,
30
23
  )
31
- from reconcile.gql_definitions.aws_ami_cleanup.asg_namespaces import (
32
- query as query_asg_namespaces,
24
+ from reconcile.gql_definitions.aws_ami_cleanup.aws_accounts import (
25
+ query as aws_accounts_query,
33
26
  )
34
- from reconcile.status import ExitCodes
35
27
  from reconcile.typed_queries.app_interface_vault_settings import (
36
28
  get_app_interface_vault_settings,
37
29
  )
@@ -40,7 +32,6 @@ from reconcile.utils.aws_api import AWSApi
40
32
  from reconcile.utils.defer import defer
41
33
  from reconcile.utils.parse_dhms_duration import dhms_to_seconds
42
34
  from reconcile.utils.secret_reader import create_secret_reader
43
- from reconcile.utils.terrascript_aws_client import TerrascriptClient as Terrascript
44
35
 
45
36
  if TYPE_CHECKING:
46
37
  from mypy_boto3_ec2 import EC2Client
@@ -51,23 +42,9 @@ QONTRACT_INTEGRATION = "aws_ami_cleanup"
51
42
  MANAGED_TAG = {"Key": "managed_by_integration", "Value": QONTRACT_INTEGRATION}
52
43
 
53
44
 
54
- class CannotCompareTagsError(Exception):
55
- pass
56
-
57
-
58
- class AmiTag(BaseModel):
59
- key: str = Field(alias="Key")
60
- value: str = Field(alias="Value")
61
-
62
- class Config:
63
- allow_population_by_field_name = True
64
- frozen = True
65
-
66
-
67
45
  class AWSAmi(BaseModel):
68
46
  name: str
69
47
  image_id: str
70
- tags: set[AmiTag]
71
48
  creation_date: datetime
72
49
  snapshot_ids: list[str]
73
50
 
@@ -75,237 +52,214 @@ class AWSAmi(BaseModel):
75
52
  frozen = True
76
53
 
77
54
 
78
- class AIAmi(BaseModel):
79
- identifier: str
80
- tags: set[AmiTag]
55
+ def get_aws_amis_from_launch_templates(ec2_client: EC2Client) -> set[str]:
56
+ amis = set()
81
57
 
82
- class Config:
83
- frozen = True
58
+ paginator = ec2_client.get_paginator("describe_launch_templates")
59
+ for page in paginator.paginate():
60
+ for launch_template in page.get("LaunchTemplates", []):
61
+ if launch_template_id := launch_template.get("LaunchTemplateId"):
62
+ launch_template_versions = ec2_client.describe_launch_template_versions(
63
+ LaunchTemplateId=launch_template_id,
64
+ Versions=[str(launch_template.get("LatestVersionNumber"))],
65
+ ).get("LaunchTemplateVersions", [])
66
+
67
+ if launch_template_versions:
68
+ if (
69
+ ami_id := launch_template_versions[0]
70
+ .get("LaunchTemplateData", {})
71
+ .get("ImageId")
72
+ ):
73
+ amis.add(ami_id)
74
+
75
+ return amis
84
76
 
85
77
 
86
78
  def get_aws_amis(
87
- aws_api: AWSApi,
88
79
  ec2_client: EC2Client,
89
80
  owner: str,
90
81
  regex: str,
91
82
  age_in_seconds: int,
92
83
  utc_now: datetime,
93
- region: str,
94
84
  ) -> list[AWSAmi]:
95
85
  """Get amis that match regex older than given age"""
96
86
 
97
- images = aws_api.paginate(
98
- ec2_client, "describe_images", "Images", {"Owners": [owner]}
99
- )
100
-
101
87
  pattern = re.compile(regex)
88
+ paginator = ec2_client.get_paginator("describe_images")
102
89
  results = []
103
- for i in images:
104
- if not re.search(pattern, i["Name"]):
105
- continue
90
+ for page in paginator.paginate(Owners=[owner]):
91
+ for image in page.get("Images", []):
92
+ if not re.search(pattern, image["Name"]):
93
+ continue
106
94
 
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)
95
+ creation_date = datetime.strptime(
96
+ image["CreationDate"], "%Y-%m-%dT%H:%M:%S.%fZ"
97
+ )
98
+ current_delta = utc_now - creation_date
99
+ delete_delta = timedelta(seconds=age_in_seconds)
110
100
 
111
- if current_delta < delete_delta:
112
- continue
101
+ if current_delta < delete_delta:
102
+ continue
113
103
 
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
104
+ snapshot_ids = []
105
+ for bdv in image["BlockDeviceMappings"]:
106
+ ebs = bdv.get("Ebs")
107
+ if not ebs:
108
+ continue
118
109
 
119
- snapshot_ids = []
120
- for bdv in i["BlockDeviceMappings"]:
121
- ebs = bdv.get("Ebs")
122
- if not ebs:
123
- continue
110
+ if sid := ebs.get("SnapshotId"):
111
+ snapshot_ids.append(sid)
124
112
 
125
- if sid := ebs.get("SnapshotId"):
126
- snapshot_ids.append(sid)
127
-
128
- tags = {AmiTag(**tag) for tag in i.get("Tags")}
129
- results.append(
130
- AWSAmi(
131
- name=i["Name"],
132
- image_id=i["ImageId"],
133
- tags=tags,
134
- creation_date=creation_date,
135
- snapshot_ids=snapshot_ids,
113
+ results.append(
114
+ AWSAmi(
115
+ name=image["Name"],
116
+ image_id=image["ImageId"],
117
+ creation_date=creation_date,
118
+ snapshot_ids=snapshot_ids,
119
+ )
136
120
  )
137
- )
138
121
 
139
122
  return results
140
123
 
141
124
 
142
125
  def get_region(
143
- cleanup: Mapping[str, Any],
144
- account: Mapping[str, Any],
126
+ config_region: str | None,
127
+ account_name: str,
128
+ resources_default_region: str,
129
+ supported_deployment_regions: list[str] | None,
145
130
  ) -> str:
146
131
  """Defines the region to search for AMIs."""
147
- region = cleanup.get("region") or account["resourcesDefaultRegion"]
148
- if region not in account["supportedDeploymentRegions"]:
149
- raise ValueError(f"region {region} is not supported in {account['name']}")
132
+ region = config_region or resources_default_region
133
+ if region not in (supported_deployment_regions or []):
134
+ raise ValueError(f"region {region} is not supported in {account_name}")
150
135
 
151
136
  return region
152
137
 
153
138
 
154
- def get_app_interface_amis(
155
- namespaces: list[NamespaceV1] | None, ts: Terrascript
156
- ) -> list[AIAmi]:
157
- """Returns all the ami referenced in ASGs in app-interface."""
158
- app_interface_amis = []
159
- for n in namespaces or []:
160
- for er in n.external_resources or []:
161
- if not isinstance(er, NamespaceTerraformProviderResourceAWSV1):
162
- continue
163
-
164
- for r in er.resources:
165
- if not isinstance(r, NamespaceTerraformResourceASGV1):
166
- continue
167
-
168
- tags = set()
169
- for i in r.image:
170
- if isinstance(i, ASGImageGitV1):
171
- tags.add(
172
- AmiTag(
173
- key=i.tag_name,
174
- value=ts.get_commit_sha(i.dict(by_alias=True)),
175
- )
176
- )
177
- elif isinstance(i, ASGImageStaticV1):
178
- tags.add(AmiTag(key=i.tag_name, value=i.value))
179
-
180
- app_interface_amis.append(AIAmi(identifier=r.identifier, tags=tags))
181
-
182
- return app_interface_amis
183
-
184
-
185
- def check_aws_ami_in_use(
186
- aws_ami: AWSAmi, app_interface_amis: list[AIAmi]
187
- ) -> str | None:
188
- """Verifies if the given AWS ami is in use in a defined app-interface ASG."""
189
- for ai_ami in app_interface_amis:
190
- # This can happen if the ASG init template has changed over the time. We don't have a way
191
- # to properly delete these automatically since we cannot assure they are not in use.
192
- # The integration will fail in this case and these amis will need to be handled manually.
193
- if len(ai_ami.tags) > len(aws_ami.tags):
194
- raise CannotCompareTagsError(
195
- f"{ai_ami.identifier} AI AMI has more tags than {aws_ami.image_id} AWS AMI"
196
- )
197
-
198
- if ai_ami.tags.issubset(aws_ami.tags):
199
- return ai_ami.identifier
200
-
201
- return None
202
-
203
-
204
139
  @defer
205
140
  def run(dry_run: bool, thread_pool_size: int, defer: Callable | None = None) -> None:
206
- exit_code = ExitCodes.SUCCESS
207
-
208
- # We still use here a non-typed query; accounts is passed to AWSApi and Terrascript classes
209
- # which contain a vast amount of magic based on keys from that dict. Since this integration
210
- # cannot still be properly monitored (see https://issues.redhat.com/browse/APPSRE-7674),
211
- # it's easy that it breaks without being noticed. Once it is properly monitored, this should
212
- # be moved to a typed query.
213
- query_data = queries.get_aws_accounts(terraform_state=True, cleanup=True)
141
+ utc_now = datetime.utcnow()
142
+ gqlapi = gql.get_api()
143
+ aws_accounts = aws_accounts_query(gqlapi.query).accounts
144
+
145
+ # Get accounts that have cleanup configured
214
146
  cleanup_accounts = []
215
- for data in query_data:
216
- cleanups = data.get("cleanup")
217
- if not cleanups:
147
+ for account in aws_accounts or []:
148
+ if not account.cleanup:
218
149
  continue
219
150
  is_ami_related = False
220
- for cleanup in cleanups:
221
- is_ami_related |= cleanup.get("provider") == "ami"
151
+ for cleanup in account.cleanup:
152
+ is_ami_related |= cleanup.provider == "ami"
222
153
  if not is_ami_related:
223
154
  continue
224
- cleanup_accounts.append(data)
155
+ cleanup_accounts.append(account)
225
156
 
226
- vault_settings = get_app_interface_vault_settings()
157
+ # Build a dict with all accounts that are used to share amis with. Together with
158
+ # the cleanup account we will look into the account's launch templates in order to
159
+ # find the AMIs that are currently being used. We will make sure that those are not
160
+ # deleted even if they have expired.
161
+ ami_accounts = defaultdict(set)
162
+ for account in cleanup_accounts:
163
+ ami_accounts[account.name].add(account.resources_default_region)
227
164
 
228
- ts = Terrascript(
229
- QONTRACT_INTEGRATION,
230
- "",
231
- thread_pool_size,
232
- cleanup_accounts,
233
- settings=vault_settings.dict(by_alias=True),
234
- )
165
+ for sharing_config in account.sharing or []:
166
+ if not isinstance(sharing_config, AWSAccountSharingOptionAMIV1):
167
+ continue
235
168
 
236
- gqlapi = gql.get_api()
237
- namespaces = query_asg_namespaces(query_func=gqlapi.query).namespaces or []
238
- app_interface_amis = get_app_interface_amis(namespaces, ts)
169
+ region = get_region(
170
+ config_region=sharing_config.region,
171
+ account_name=sharing_config.account.name,
172
+ resources_default_region=sharing_config.account.resources_default_region,
173
+ supported_deployment_regions=sharing_config.account.supported_deployment_regions,
174
+ )
239
175
 
240
- secret_reader = create_secret_reader(use_vault=vault_settings.vault)
241
- aws_api = AWSApi(1, cleanup_accounts, secret_reader=secret_reader, init_users=False)
176
+ ami_accounts[sharing_config.account.name].add(
177
+ sharing_config.account.resources_default_region
178
+ )
179
+
180
+ # Build AWSApi object. We will use all those accounts listed in ami_accounts since
181
+ # we will also need to look for used AMIs.
182
+ accounts_dicted = [
183
+ account.dict(by_alias=True)
184
+ for account in aws_accounts or []
185
+ if account.name in ami_accounts
186
+ ]
187
+ secret_reader = create_secret_reader(
188
+ use_vault=get_app_interface_vault_settings().vault
189
+ )
190
+ aws_api = AWSApi(1, accounts_dicted, secret_reader=secret_reader, init_users=False)
242
191
  if defer: # defer is provided by the method decorator; this makes just mypy happy.
243
192
  defer(aws_api.cleanup)
244
193
 
245
- utc_now = datetime.utcnow()
194
+ # Get all AMIs used
195
+ amis_used_in_launch_templates = set()
196
+ for account_name, regions in ami_accounts.items():
197
+ for region in regions:
198
+ session = aws_api.get_session(account_name)
199
+ ec2_client = aws_api.get_session_client(session, "ec2", region)
200
+ launch_template_amis = get_aws_amis_from_launch_templates(
201
+ ec2_client=ec2_client
202
+ )
203
+ if launch_template_amis:
204
+ amis_used_in_launch_templates.update(launch_template_amis)
205
+
206
+ # The action
246
207
  for account in cleanup_accounts:
247
- for cleanup in account["cleanup"]:
248
- if cleanup["provider"] != "ami":
208
+ for cleanup_config in account.cleanup or []:
209
+ if not isinstance(cleanup_config, AWSAccountCleanupOptionAMIV1):
249
210
  continue
250
211
 
251
- region = get_region(cleanup, account)
252
- regex = cleanup["regex"]
253
- age_in_seconds = dhms_to_seconds(cleanup["age"])
212
+ region = get_region(
213
+ config_region=cleanup_config.region,
214
+ account_name=account.name,
215
+ resources_default_region=account.resources_default_region,
216
+ supported_deployment_regions=account.supported_deployment_regions,
217
+ )
218
+ age_in_seconds = dhms_to_seconds(cleanup_config.age)
254
219
 
255
- session = aws_api.get_session(account["name"])
220
+ session = aws_api.get_session(account.name)
256
221
  ec2_client = aws_api.get_session_client(session, "ec2", region)
257
222
 
258
- amis = get_aws_amis(
259
- aws_api=aws_api,
223
+ aws_amis = get_aws_amis(
260
224
  ec2_client=ec2_client,
261
- owner=account["uid"],
262
- regex=regex,
225
+ owner=account.uid,
226
+ regex=cleanup_config.regex,
263
227
  age_in_seconds=age_in_seconds,
264
228
  utc_now=utc_now,
265
- region=region,
266
229
  )
267
230
 
268
- for aws_ami in amis:
269
- try:
270
- if identifier := check_aws_ami_in_use(aws_ami, app_interface_amis):
271
- logging.info(
272
- "Discarding ami %s with id %s as it is used in app-interface in %s",
273
- aws_ami.name,
274
- aws_ami.image_id,
275
- identifier,
276
- )
277
- continue
278
- except CannotCompareTagsError as e:
279
- logging.error(e)
280
- if not dry_run:
281
- exit_code = ExitCodes.ERROR
231
+ for ami in aws_amis:
232
+ if ami.image_id in amis_used_in_launch_templates:
233
+ logging.info(
234
+ "Discarding AMI %s with id %s as it is still in use.",
235
+ ami.name,
236
+ ami.image_id,
237
+ )
282
238
  continue
283
239
 
284
240
  logging.info(
285
241
  "Deregistering image %s with id %s created in %s",
286
- aws_ami.name,
287
- aws_ami.image_id,
288
- aws_ami.creation_date,
242
+ ami.name,
243
+ ami.image_id,
244
+ ami.creation_date,
289
245
  )
290
246
 
291
247
  try:
292
- ec2_client.deregister_image(
293
- ImageId=aws_ami.image_id, DryRun=dry_run
294
- )
248
+ ec2_client.deregister_image(ImageId=ami.image_id, DryRun=dry_run)
295
249
  except ClientError as e:
296
250
  if "DryRunOperation" in str(e):
297
251
  logging.info(e)
298
252
  else:
299
253
  raise
300
254
 
301
- if not aws_ami.snapshot_ids:
255
+ if not ami.snapshot_ids:
302
256
  continue
303
257
 
304
- for snapshot_id in aws_ami.snapshot_ids:
258
+ for snapshot_id in ami.snapshot_ids:
305
259
  logging.info(
306
260
  "Deleting associated snapshot %s from image %s",
307
261
  snapshot_id,
308
- aws_ami.image_id,
262
+ ami.image_id,
309
263
  )
310
264
 
311
265
  try:
@@ -317,5 +271,3 @@ def run(dry_run: bool, thread_pool_size: int, defer: Callable | None = None) ->
317
271
  logging.info(e)
318
272
  else:
319
273
  raise
320
-
321
- sys.exit(exit_code)
@@ -0,0 +1,161 @@
1
+ """
2
+ Generated by qenerate plugin=pydantic_v1. DO NOT MODIFY MANUALLY!
3
+ """
4
+ from collections.abc import Callable # noqa: F401 # pylint: disable=W0611
5
+ from datetime import datetime # noqa: F401 # pylint: disable=W0611
6
+ from enum import Enum # noqa: F401 # pylint: disable=W0611
7
+ from typing import ( # noqa: F401 # pylint: disable=W0611
8
+ Any,
9
+ Optional,
10
+ Union,
11
+ )
12
+
13
+ from pydantic import ( # noqa: F401 # pylint: disable=W0611
14
+ BaseModel,
15
+ Extra,
16
+ Field,
17
+ Json,
18
+ )
19
+
20
+ from reconcile.gql_definitions.fragments.aws_account_common import AWSAccountCommon
21
+ from reconcile.gql_definitions.fragments.terraform_state import TerraformState
22
+
23
+
24
+ DEFINITION = """
25
+ fragment AWSAccountCommon on AWSAccount_v1 {
26
+ path
27
+ name
28
+ description
29
+ uid
30
+ terraformUsername
31
+ consoleUrl
32
+ resourcesDefaultRegion
33
+ supportedDeploymentRegions
34
+ providerVersion
35
+ accountOwners {
36
+ name
37
+ email
38
+ }
39
+ automationToken {
40
+ ... VaultSecret
41
+ }
42
+ garbageCollection
43
+ enableDeletion
44
+ deletionApprovals {
45
+ type
46
+ name
47
+ expiration
48
+ }
49
+ disable {
50
+ integrations
51
+ }
52
+ deleteKeys
53
+ premiumSupport
54
+ partition
55
+ }
56
+
57
+ fragment TerraformState on TerraformStateAWS_v1 {
58
+ provider
59
+ bucket
60
+ region
61
+ integrations {
62
+ key
63
+ integration
64
+ }
65
+ }
66
+
67
+ fragment VaultSecret on VaultSecret_v1 {
68
+ path
69
+ field
70
+ version
71
+ format
72
+ }
73
+
74
+ query AWSAccountsAmiCleanup {
75
+ accounts: awsaccounts_v1 {
76
+ ...AWSAccountCommon
77
+ terraformState {
78
+ ...TerraformState
79
+ }
80
+ cleanup {
81
+ provider
82
+ ... on AWSAccountCleanupOptionAMI_v1 {
83
+ regex
84
+ age
85
+ region
86
+ }
87
+ }
88
+ sharing {
89
+ provider
90
+ account {
91
+ name
92
+ supportedDeploymentRegions
93
+ resourcesDefaultRegion
94
+ }
95
+ ... on AWSAccountSharingOptionAMI_v1 {
96
+ region
97
+ }
98
+ }
99
+ }
100
+ }
101
+ """
102
+
103
+
104
+ class ConfiguredBaseModel(BaseModel):
105
+ class Config:
106
+ smart_union=True
107
+ extra=Extra.forbid
108
+
109
+
110
+ class AWSAccountCleanupOptionV1(ConfiguredBaseModel):
111
+ provider: str = Field(..., alias="provider")
112
+
113
+
114
+ class AWSAccountCleanupOptionAMIV1(AWSAccountCleanupOptionV1):
115
+ regex: str = Field(..., alias="regex")
116
+ age: str = Field(..., alias="age")
117
+ region: Optional[str] = Field(..., alias="region")
118
+
119
+
120
+ class AWSAccountSharingOptionV1_AWSAccountV1(ConfiguredBaseModel):
121
+ name: str = Field(..., alias="name")
122
+ supported_deployment_regions: Optional[list[str]] = Field(..., alias="supportedDeploymentRegions")
123
+ resources_default_region: str = Field(..., alias="resourcesDefaultRegion")
124
+
125
+
126
+ class AWSAccountSharingOptionV1(ConfiguredBaseModel):
127
+ provider: str = Field(..., alias="provider")
128
+ account: AWSAccountSharingOptionV1_AWSAccountV1 = Field(..., alias="account")
129
+
130
+
131
+ class AWSAccountSharingOptionAMIV1(AWSAccountSharingOptionV1):
132
+ region: Optional[str] = Field(..., alias="region")
133
+
134
+
135
+ class AWSAccountV1(AWSAccountCommon):
136
+ terraform_state: Optional[TerraformState] = Field(..., alias="terraformState")
137
+ cleanup: Optional[list[Union[AWSAccountCleanupOptionAMIV1, AWSAccountCleanupOptionV1]]] = Field(..., alias="cleanup")
138
+ sharing: Optional[list[Union[AWSAccountSharingOptionAMIV1, AWSAccountSharingOptionV1]]] = Field(..., alias="sharing")
139
+
140
+
141
+ class AWSAccountsAmiCleanupQueryData(ConfiguredBaseModel):
142
+ accounts: Optional[list[AWSAccountV1]] = Field(..., alias="accounts")
143
+
144
+
145
+ def query(query_func: Callable, **kwargs: Any) -> AWSAccountsAmiCleanupQueryData:
146
+ """
147
+ This is a convenience function which queries and parses the data into
148
+ concrete types. It should be compatible with most GQL clients.
149
+ You do not have to use it to consume the generated data classes.
150
+ Alternatively, you can also mime and alternate the behavior
151
+ of this function in the caller.
152
+
153
+ Parameters:
154
+ query_func (Callable): Function which queries your GQL Server
155
+ kwargs: optional arguments that will be passed to the query function
156
+
157
+ Returns:
158
+ AWSAccountsAmiCleanupQueryData: queried data parsed into generated classes
159
+ """
160
+ raw_data: dict[Any, Any] = query_func(DEFINITION, **kwargs)
161
+ return AWSAccountsAmiCleanupQueryData(**raw_data)
@@ -265,6 +265,7 @@ query SaasFiles {
265
265
  auto
266
266
  publish
267
267
  subscribe
268
+ redeployOnPublisherConfigChange
268
269
  soakDays
269
270
  schedule
270
271
  promotion_data {
@@ -472,6 +473,7 @@ class SaasResourceTemplateTargetPromotionV1(ConfiguredBaseModel):
472
473
  auto: Optional[bool] = Field(..., alias="auto")
473
474
  publish: Optional[list[str]] = Field(..., alias="publish")
474
475
  subscribe: Optional[list[str]] = Field(..., alias="subscribe")
476
+ redeploy_on_publisher_config_change: Optional[bool] = Field(..., alias="redeployOnPublisherConfigChange")
475
477
  soak_days: Optional[int] = Field(..., alias="soakDays")
476
478
  schedule: Optional[str] = Field(..., alias="schedule")
477
479
  promotion_data: Optional[list[PromotionDataV1]] = Field(..., alias="promotion_data")
@@ -39,7 +39,7 @@ class Publisher:
39
39
  target_name: str | None,
40
40
  cluster_name: str,
41
41
  auth_code: HasSecret | None,
42
- publish_job_logs: bool | None,
42
+ redeploy_on_config_change: bool | None,
43
43
  has_subscriber: bool = True,
44
44
  ):
45
45
  self._ref = ref
@@ -56,7 +56,7 @@ class Publisher:
56
56
  self.resource_template_name = resource_template_name
57
57
  self.target_name = target_name if target_name else "None"
58
58
  self.cluster_name = cluster_name
59
- self.publish_job_logs = bool(publish_job_logs)
59
+ self.redeploy_on_config_change = bool(redeploy_on_config_change)
60
60
  self.has_subscriber = has_subscriber
61
61
 
62
62
  def fetch_commit_shas_and_deployment_info(
@@ -62,7 +62,8 @@ class S3Exporter:
62
62
  data: dict[str, dict] = {}
63
63
  for publisher in publishers:
64
64
  publisher_data = PublisherData.from_publisher(publisher)
65
- key = f"{publisher.app_name}/{publisher.saas_name}/{publisher.resource_template_name}/{publisher.target_name}/{publisher.cluster_name}/{publisher.namespace_name}/{publisher.publish_job_logs}"
65
+ # Note, redeploy_on_config_change indicates that this publisher is a test job
66
+ key = f"{publisher.app_name}/{publisher.saas_name}/{publisher.resource_template_name}/{publisher.target_name}/{publisher.cluster_name}/{publisher.namespace_name}/{publisher.redeploy_on_config_change}"
66
67
  data[key] = {
67
68
  "commit_sha": publisher_data.commit_sha,
68
69
  "deployment_state": publisher_data.deployment_state.value,
@@ -65,8 +65,8 @@ class SaasFilesInventory:
65
65
  cluster_name=target.namespace.cluster.name,
66
66
  resource_template_name=resource_template.name,
67
67
  target_name=target.name,
68
- publish_job_logs=saas_file.publish_job_logs,
69
68
  auth_code=auth_code,
69
+ redeploy_on_config_change=target.promotion.redeploy_on_publisher_config_change,
70
70
  )
71
71
 
72
72
  has_subscriber = False
@@ -122,9 +122,9 @@ class SaasFilesInventory:
122
122
  blocked_versions=blocked_versions.get(
123
123
  resource_template.url, set()
124
124
  ),
125
- # Note: this will be refactored at a later point.
126
- # https://issues.redhat.com/browse/APPSRE-7516
127
- use_target_config_hash=bool(saas_file.publish_job_logs),
125
+ use_target_config_hash=bool(
126
+ target.promotion.redeploy_on_publisher_config_change
127
+ ),
128
128
  )
129
129
  self.subscribers.append(subscriber)
130
130
  for prom_data in target.promotion.promotion_data or []:
@@ -131,7 +131,7 @@ def subscriber_builder(
131
131
  app_name="",
132
132
  resource_template_name="",
133
133
  target_name=None,
134
- publish_job_logs=True,
134
+ redeploy_on_config_change=True,
135
135
  has_subscriber=True,
136
136
  auth_code=None,
137
137
  )
@@ -11,35 +11,26 @@ from typing import (
11
11
  TYPE_CHECKING,
12
12
  Any,
13
13
  )
14
- from unittest.mock import MagicMock
15
14
 
16
15
  import boto3
17
16
  import pytest
18
17
  from moto import mock_ec2
19
- from pytest_mock import MockerFixture
20
18
 
21
19
  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
20
  get_aws_amis,
21
+ get_aws_amis_from_launch_templates,
29
22
  )
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
23
 
37
24
  if TYPE_CHECKING:
38
25
  from mypy_boto3_ec2 import EC2Client
39
- from mypy_boto3_ec2.type_defs import CreateImageResultTypeDef
26
+ from mypy_boto3_ec2.type_defs import (
27
+ CreateImageResultTypeDef,
28
+ LaunchTemplateVersionTypeDef,
29
+ )
40
30
  else:
41
31
  EC2Client = object
42
32
  CreateImageResultTypeDef = dict
33
+ LaunchTemplateVersionTypeDef = dict
43
34
 
44
35
  MOTO_DEFAULT_ACCOUNT = "123456789012"
45
36
 
@@ -57,19 +48,6 @@ def accounts() -> list[dict[str, Any]]:
57
48
  ]
58
49
 
59
50
 
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
51
  @pytest.fixture
74
52
  def ec2_client() -> Generator[EC2Client, None, None]:
75
53
  with mock_ec2():
@@ -123,46 +101,53 @@ def suse_image(ec2_client: EC2Client) -> CreateImageResultTypeDef:
123
101
 
124
102
 
125
103
  @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
- ]
104
+ def ubuntu_launch_template(ec2_client: EC2Client) -> LaunchTemplateVersionTypeDef:
105
+ # Ubuntu AMI from moto/ec2/resources/amis.json
106
+ response = ec2_client.create_launch_template(
107
+ LaunchTemplateName="Ubuntu",
108
+ LaunchTemplateData={
109
+ "ImageId": "ami-1e749f67", # Canonical, Ubuntu, 14.04 LTS
110
+ "InstanceType": "t3.micro",
111
+ "SecurityGroupIds": ["sg-12345678"],
112
+ },
113
+ )
114
+
115
+ return ec2_client.describe_launch_template_versions(
116
+ LaunchTemplateId=response["LaunchTemplate"]["LaunchTemplateId"],
117
+ Versions=["1"],
118
+ )["LaunchTemplateVersions"][0]
119
+
120
+
121
+ @pytest.fixture
122
+ def suse_launch_template(ec2_client: EC2Client) -> LaunchTemplateVersionTypeDef:
123
+ # SUSE AMI from moto/ec2/resources/amis.json
124
+ response = ec2_client.create_launch_template(
125
+ LaunchTemplateName="SUSE",
126
+ LaunchTemplateData={
127
+ "ImageId": "ami-35e92e4c", # SUSE Linux Enterprise Server 12 SP3
128
+ "InstanceType": "t3.micro",
129
+ "SecurityGroupIds": ["sg-12345678"],
130
+ },
131
+ )
132
+
133
+ return ec2_client.describe_launch_template_versions(
134
+ LaunchTemplateId=response["LaunchTemplate"]["LaunchTemplateId"],
135
+ Versions=["1"],
136
+ )["LaunchTemplateVersions"][0]
149
137
 
150
138
 
151
139
  def test_get_aws_amis_success(
152
140
  ec2_client: EC2Client,
153
- aws_api: AWSApi,
154
141
  rhel_image: CreateImageResultTypeDef,
155
142
  suse_image: CreateImageResultTypeDef,
156
143
  ) -> None:
157
144
  utc_now = datetime.utcnow() + timedelta(seconds=60)
158
145
  amis = get_aws_amis(
159
- aws_api=aws_api,
160
146
  ec2_client=ec2_client,
161
147
  owner=MOTO_DEFAULT_ACCOUNT,
162
148
  regex="ci-int-jenkins-worker-rhel7.*",
163
149
  age_in_seconds=30,
164
150
  utc_now=utc_now,
165
- region="us-east-1",
166
151
  )
167
152
 
168
153
  assert len(amis) == 1
@@ -171,19 +156,16 @@ def test_get_aws_amis_success(
171
156
 
172
157
  def test_get_aws_amis_unmatched_regex(
173
158
  ec2_client: EC2Client,
174
- aws_api: AWSApi,
175
159
  rhel_image: CreateImageResultTypeDef,
176
160
  suse_image: CreateImageResultTypeDef,
177
161
  ) -> None:
178
162
  utc_now = datetime.utcnow() + timedelta(seconds=60)
179
163
  amis = get_aws_amis(
180
- aws_api=aws_api,
181
164
  ec2_client=ec2_client,
182
165
  owner=MOTO_DEFAULT_ACCOUNT,
183
166
  regex="ci-int-jenkins-worker-centos7.*",
184
167
  age_in_seconds=30,
185
168
  utc_now=utc_now,
186
- region="us-east-1",
187
169
  )
188
170
 
189
171
  assert len(amis) == 0
@@ -191,19 +173,16 @@ def test_get_aws_amis_unmatched_regex(
191
173
 
192
174
  def test_get_aws_amis_different_account(
193
175
  ec2_client: EC2Client,
194
- aws_api: AWSApi,
195
176
  rhel_image: CreateImageResultTypeDef,
196
177
  suse_image: CreateImageResultTypeDef,
197
178
  ) -> None:
198
179
  utc_now = datetime.utcnow() + timedelta(seconds=60)
199
180
  amis = get_aws_amis(
200
- aws_api=aws_api,
201
181
  ec2_client=ec2_client,
202
182
  owner="789123456789",
203
183
  regex="ci-int-jenkins-worker-rhel7.*",
204
184
  age_in_seconds=30,
205
185
  utc_now=utc_now,
206
- region="us-east-1",
207
186
  )
208
187
 
209
188
  assert len(amis) == 0
@@ -211,90 +190,41 @@ def test_get_aws_amis_different_account(
211
190
 
212
191
  def test_get_aws_amis_too_young(
213
192
  ec2_client: EC2Client,
214
- aws_api: AWSApi,
215
193
  rhel_image: CreateImageResultTypeDef,
216
194
  suse_image: CreateImageResultTypeDef,
217
195
  ) -> None:
218
196
  utc_now = datetime.utcnow() + timedelta(seconds=60)
219
197
  amis = get_aws_amis(
220
- aws_api=aws_api,
221
198
  ec2_client=ec2_client,
222
199
  owner=MOTO_DEFAULT_ACCOUNT,
223
200
  regex="ci-int-jenkins-worker-rhel7.*",
224
201
  age_in_seconds=90,
225
202
  utc_now=utc_now,
226
- region="us-east-1",
227
203
  )
228
204
 
229
205
  assert len(amis) == 0
230
206
 
231
207
 
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
- snapshot_ids=[],
256
- )
257
- assert check_aws_ami_in_use(aws_ami, ai_amis_fxt) == "ci-int-jenkins-worker-app-sre"
258
-
259
- aws_ami = AWSAmi(
260
- name="ci-int-jenkins-worker-app-interface-sha-4567",
261
- image_id="ami-823445",
262
- creation_date=utc_now,
263
- tags={
264
- AmiTag(key="type", value="ci-int-jenkins-worker-app-interface"),
265
- AmiTag(key="infra_commit", value="sha-4567"),
266
- },
267
- snapshot_ids=[],
268
- )
269
- assert (
270
- check_aws_ami_in_use(aws_ami, ai_amis_fxt)
271
- == "ci-int-jenkins-worker-app-interface"
272
- )
273
-
274
- aws_ami = AWSAmi(
275
- name="ci-int-jenkins-worker-app-interface-a-different-sha",
276
- image_id="ami-823445",
277
- creation_date=utc_now,
278
- tags={
279
- AmiTag(key="type", value="ci-int-jenkins-worker-app-interface"),
280
- AmiTag(key="infra_commit", value="a-different-sha"),
281
- },
282
- snapshot_ids=[],
283
- )
284
-
285
- assert not check_aws_ami_in_use(aws_ami, ai_amis_fxt)
208
+ def test_get_aws_amis_from_launch_templates(
209
+ ec2_client: EC2Client,
210
+ ubuntu_launch_template: LaunchTemplateVersionTypeDef,
211
+ suse_launch_template: LaunchTemplateVersionTypeDef,
212
+ ) -> None:
213
+ amis = get_aws_amis_from_launch_templates(ec2_client)
214
+ assert amis == {
215
+ ubuntu_launch_template["LaunchTemplateData"]["ImageId"],
216
+ suse_launch_template["LaunchTemplateData"]["ImageId"],
217
+ }
286
218
 
287
- aws_ami = AWSAmi(
288
- name="ci-int-jenkins-worker-app-interface-a-weird-one",
289
- image_id="ami-823445",
290
- creation_date=utc_now,
291
- tags={
292
- AmiTag(key="type", value="ci-int-jenkins-worker-app-interface"),
293
- },
294
- snapshot_ids=[],
219
+ # create a new ubuntu version
220
+ new_ami_id = "ami-785db401" # "Canonical, Ubuntu, 16.04 LTS
221
+ ec2_client.create_launch_template_version(
222
+ LaunchTemplateId=ubuntu_launch_template["LaunchTemplateId"],
223
+ LaunchTemplateData={"ImageId": new_ami_id},
295
224
  )
296
225
 
297
- with pytest.raises(CannotCompareTagsError) as excinfo:
298
- check_aws_ami_in_use(aws_ami, ai_amis_fxt)
299
-
300
- assert "AI AMI has more tags than" in str(excinfo.value)
226
+ amis = get_aws_amis_from_launch_templates(ec2_client)
227
+ assert amis == {
228
+ new_ami_id,
229
+ suse_launch_template["LaunchTemplateData"]["ImageId"],
230
+ }
@@ -164,6 +164,7 @@ class TestSaasFileValid(TestCase):
164
164
  auto=True,
165
165
  publish=None,
166
166
  subscribe=None,
167
+ redeployOnPublisherConfigChange=None,
167
168
  promotion_data=None,
168
169
  soakDays=0,
169
170
  schedule="* * * * *",
@@ -190,6 +191,7 @@ class TestSaasFileValid(TestCase):
190
191
  publish=None,
191
192
  subscribe=None,
192
193
  promotion_data=None,
194
+ redeployOnPublisherConfigChange=None,
193
195
  soakDays=0,
194
196
  schedule="* * * * *",
195
197
  )
@@ -1,124 +0,0 @@
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(..., alias="image")
93
-
94
-
95
- class NamespaceTerraformProviderResourceAWSV1(NamespaceExternalResourceV1):
96
- resources: list[Union[NamespaceTerraformResourceASGV1, NamespaceTerraformResourceAWSV1]] = Field(..., alias="resources")
97
-
98
-
99
- class NamespaceV1(ConfiguredBaseModel):
100
- name: str = Field(..., alias="name")
101
- external_resources: Optional[list[Union[NamespaceTerraformProviderResourceAWSV1, NamespaceExternalResourceV1]]] = Field(..., alias="externalResources")
102
-
103
-
104
- class ASGNamespacesQueryData(ConfiguredBaseModel):
105
- namespaces: Optional[list[NamespaceV1]] = Field(..., alias="namespaces")
106
-
107
-
108
- def query(query_func: Callable, **kwargs: Any) -> ASGNamespacesQueryData:
109
- """
110
- This is a convenience function which queries and parses the data into
111
- concrete types. It should be compatible with most GQL clients.
112
- You do not have to use it to consume the generated data classes.
113
- Alternatively, you can also mime and alternate the behavior
114
- of this function in the caller.
115
-
116
- Parameters:
117
- query_func (Callable): Function which queries your GQL Server
118
- kwargs: optional arguments that will be passed to the query function
119
-
120
- Returns:
121
- ASGNamespacesQueryData: queried data parsed into generated classes
122
- """
123
- raw_data: dict[Any, Any] = query_func(DEFINITION, **kwargs)
124
- return ASGNamespacesQueryData(**raw_data)