qontract-reconcile 0.10.2.dev403__py3-none-any.whl → 0.10.2.dev405__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.

Potentially problematic release.


This version of qontract-reconcile might be problematic. Click here for more details.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: qontract-reconcile
3
- Version: 0.10.2.dev403
3
+ Version: 0.10.2.dev405
4
4
  Summary: Collection of tools to reconcile services with their desired state as defined in the app-interface DB.
5
5
  Project-URL: homepage, https://github.com/app-sre/qontract-reconcile
6
6
  Project-URL: repository, https://github.com/app-sre/qontract-reconcile
@@ -1,7 +1,7 @@
1
1
  reconcile/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
2
  reconcile/acs_policies.py,sha256=1LKnRRPa6h3QewsDhv0epINi9RJtQmT88mPgcU2LhDM,8667
3
3
  reconcile/acs_rbac.py,sha256=15vNfNzdG_DeXaJ-f5m8DSaJh__LUK766_xAECqyTsg,22657
4
- reconcile/aws_ami_share.py,sha256=M_gT7y3cSAyT_Pm90PBCNDSmbZtqREqe2jNETh0i9Qs,3808
4
+ reconcile/aws_ami_share.py,sha256=JqevGzjLKlktRDeL01ukBko5ope5Xhl_0NvdjfGdENA,3503
5
5
  reconcile/aws_ecr_image_pull_secrets.py,sha256=r9Dy01w4kNPWh3LO2tSGH_x4rQg3B2FJc5sVjPJxZdI,2622
6
6
  reconcile/aws_iam_keys.py,sha256=YHp-K8K8Dqm7vVKzry0RyhM6Egmpp7eCWMNdKk0vbME,4118
7
7
  reconcile/aws_iam_password_reset.py,sha256=5oajSspJVAvpGd445hKsxtEGYb75dM4l1_PJTzrfHk0,3253
@@ -88,7 +88,7 @@ reconcile/quay_mirror.py,sha256=pA1_OujRduwQ6dYljoWXU_VJgAwlv7DzThk26ymKmGs,1432
88
88
  reconcile/quay_mirror_org.py,sha256=ltPbHuWUI8Wnl8gV4aeYmvoYFA1uXLWqlXqEPpw7Hi0,11065
89
89
  reconcile/quay_permissions.py,sha256=BF539lRxjpgwm88WzazklzgaCF_ipRALwbO2AdpqUqE,4388
90
90
  reconcile/quay_repos.py,sha256=fBleLzMtfDmTidpzbrTt8kGCy-Bk3J06EO4hhyghGnQ,7570
91
- reconcile/queries.py,sha256=_XCgyGyZib2YM90W3qCgqVPoOkuiwjFjNTTAhKM2tLo,56477
91
+ reconcile/queries.py,sha256=L0NbIr6-M12P7xqU2s_2-tgi5BOC5QEo6Otu33WG5tI,56598
92
92
  reconcile/query_validator.py,sha256=csOSkKxcf6ZlpchJu4ck2jLYKUN6y1l-UmSQUFHgssY,1618
93
93
  reconcile/requests_sender.py,sha256=g-tlrudvIqhneQPDMrfYF0Xsq7BSW2QcBPirl7hFM6I,4058
94
94
  reconcile/resource_scraper.py,sha256=TcMhXga7konX9x97NhpoijnDGWA-ZjdpiiXjm5qCmPk,2249
@@ -162,7 +162,7 @@ reconcile/change_owners/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG
162
162
  reconcile/change_owners/approver.py,sha256=Z3_11vnK2WNOxjEEXVDh0224-_-qbt9d6mBeVE-7fsc,2259
163
163
  reconcile/change_owners/bundle.py,sha256=h30fU-JmLH5a-rCAovpzTeTkkkgZztsZ5A2raee0YuU,5355
164
164
  reconcile/change_owners/change_log_tracking.py,sha256=lRHdC6-sqzUClmoVNq8v5rygAzeQJRxbKbJNDB3YI8E,9499
165
- reconcile/change_owners/change_owners.py,sha256=U0KmAkDlzxmj4odk2sRrGotfEOq1hC_mb85iCwAzVD0,17105
165
+ reconcile/change_owners/change_owners.py,sha256=e9ujTibYL5-2Pbf1UuewMFw9Vettfe_WLqiHl4Ad9Y0,17133
166
166
  reconcile/change_owners/change_types.py,sha256=5eSvS2_npUriq9RN4LdAWdYUiNzF91K1pDUEVYDIQ4k,32023
167
167
  reconcile/change_owners/changes.py,sha256=YTqwUYutQ6JVSSYmC2Ph5ROCiVix42Vnzy47-i57z4Q,18119
168
168
  reconcile/change_owners/decision.py,sha256=755rHmrnhfM_xVKnCPlLPOVm_TCJVb3lSkkUvxFM61Q,7491
@@ -584,7 +584,7 @@ reconcile/unleash_feature_toggles/integration.py,sha256=Etp6D-UPkGy16IGHMsWBJscg
584
584
  reconcile/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
585
585
  reconcile/utils/aggregated_list.py,sha256=IxQoMpKV2HHuZOxRmVIew2WQ7SutuLJ_VxlewieK3nY,4036
586
586
  reconcile/utils/amtool.py,sha256=Ng5VVNCiPYEK67eDjIwfuuTLs5JsfltCwt6w5UfXbcY,2289
587
- reconcile/utils/aws_api.py,sha256=fEf9Gtv0fz6NchYxJogjMQnMRfTLykmGSpYm7V52Ub4,61187
587
+ reconcile/utils/aws_api.py,sha256=M2EQfDg2Aa8gKDh5aZDL8-Zd0vBXD7FQSySv-ivmYE4,62002
588
588
  reconcile/utils/aws_helper.py,sha256=9u4m94bnc3chFIDDEXwYXRXj5b--D6CTRy6g234jhNY,2972
589
589
  reconcile/utils/batches.py,sha256=TtEm64a8lWhFuNbUVpFEmXVdU2Q0sTBrP_I0Cjbgh7g,320
590
590
  reconcile/utils/binary.py,sha256=lSIevhilMeoGMePPHD7A-pxe45LVpBT0LksecYbM-EA,2477
@@ -800,7 +800,7 @@ tools/saas_promotion_state/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJ
800
800
  tools/saas_promotion_state/saas_promotion_state.py,sha256=uQv2QJAmUXP1g2GPIH30WTlvL9soY6m9lefpZEVDM5w,3965
801
801
  tools/sre_checkpoints/__init__.py,sha256=CDaDaywJnmRCLyl_NCcvxi-Zc0hTi_3OdwKiFOyS39I,145
802
802
  tools/sre_checkpoints/util.py,sha256=KcYVfa3UmJHVP_ocgrKe8NkrO5IDB9aWEDydSokPcRk,975
803
- qontract_reconcile-0.10.2.dev403.dist-info/METADATA,sha256=J9syq10cvCdSVu54yWmY5g4xeigFcWBapp8Qlc61bsk,24946
804
- qontract_reconcile-0.10.2.dev403.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
805
- qontract_reconcile-0.10.2.dev403.dist-info/entry_points.txt,sha256=5i9l54La3vQrDLAdwDKQWC0iG4sV9RRfOb1BpvzOWLc,698
806
- qontract_reconcile-0.10.2.dev403.dist-info/RECORD,,
803
+ qontract_reconcile-0.10.2.dev405.dist-info/METADATA,sha256=eH-WJsCyUi-2jX12Didk4LiLJaaQ7VKVZOOoFN_b4n4,24946
804
+ qontract_reconcile-0.10.2.dev405.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
805
+ qontract_reconcile-0.10.2.dev405.dist-info/entry_points.txt,sha256=5i9l54La3vQrDLAdwDKQWC0iG4sV9RRfOb1BpvzOWLc,698
806
+ qontract_reconcile-0.10.2.dev405.dist-info/RECORD,,
@@ -1,17 +1,19 @@
1
1
  import logging
2
+ import re
2
3
  from collections.abc import (
3
- Callable,
4
4
  Iterable,
5
5
  Mapping,
6
6
  )
7
7
  from typing import Any
8
8
 
9
9
  from reconcile import queries
10
+ from reconcile.typed_queries.aws_account_tags import get_aws_account_tags
11
+ from reconcile.typed_queries.external_resources import get_settings
10
12
  from reconcile.utils.aws_api import AWSApi
11
- from reconcile.utils.defer import defer
12
13
 
13
14
  QONTRACT_INTEGRATION = "aws-ami-share"
14
- MANAGED_TAG = {"Key": "managed_by_integration", "Value": QONTRACT_INTEGRATION}
15
+
16
+ MANAGED_TAG = {"managed_by_integration": QONTRACT_INTEGRATION}
15
17
 
16
18
 
17
19
  def filter_accounts(accounts: Iterable[dict[str, Any]]) -> list[dict[str, Any]]:
@@ -37,65 +39,70 @@ def get_region(
37
39
  return region
38
40
 
39
41
 
40
- @defer
41
- def run(dry_run: bool, defer: Callable | None = None) -> None:
42
+ def share_ami(
43
+ dry_run: bool,
44
+ src_account: Mapping[str, Any],
45
+ share: Mapping[str, Any],
46
+ default_tags: dict[str, str],
47
+ aws_api: AWSApi,
48
+ ) -> None:
49
+ dst_account = share["account"]
50
+ regex = re.compile(share["regex"])
51
+ region = get_region(share, src_account, dst_account)
52
+ src_amis = aws_api.get_amis_details(src_account, src_account, regex, region)
53
+ dst_amis = aws_api.get_amis_details(dst_account, src_account, regex, region)
54
+
55
+ for ami_id, src_ami_tags in src_amis.items():
56
+ dst_ami_tags = dst_amis.get(ami_id)
57
+ if dst_ami_tags is None:
58
+ logging.info([
59
+ "share_ami",
60
+ src_account["name"],
61
+ dst_account["name"],
62
+ ami_id,
63
+ ])
64
+ if not dry_run:
65
+ aws_api.share_ami(src_account, dst_account["uid"], ami_id, region)
66
+ dst_account_tags = default_tags | get_aws_account_tags(
67
+ dst_account.get("organization", None)
68
+ )
69
+ desired_tags = src_ami_tags | dst_account_tags | MANAGED_TAG
70
+ current_tags = dst_ami_tags or {}
71
+
72
+ if desired_tags != current_tags:
73
+ logging.info([
74
+ "tag_shared_ami",
75
+ dst_account["name"],
76
+ ami_id,
77
+ desired_tags,
78
+ ])
79
+ if not dry_run:
80
+ aws_api.create_tags(dst_account, ami_id, desired_tags)
81
+
82
+
83
+ def run(dry_run: bool) -> None:
42
84
  accounts = queries.get_aws_accounts(sharing=True)
43
85
  sharing_accounts = filter_accounts(accounts)
44
86
  settings = queries.get_app_interface_settings()
45
- aws_api = AWSApi(1, sharing_accounts, settings=settings, init_users=False)
46
- if defer:
47
- defer(aws_api.cleanup)
48
-
49
- for src_account in sharing_accounts:
50
- sharing = src_account.get("sharing")
51
- if not sharing:
52
- continue
53
- for share in sharing:
54
- if share["provider"] != "ami":
55
- continue
56
- dst_account = share["account"]
57
- regex = share["regex"]
58
- region = get_region(share, src_account, dst_account)
59
- src_amis = aws_api.get_amis_details(src_account, src_account, regex, region)
60
- dst_amis = aws_api.get_amis_details(dst_account, src_account, regex, region)
61
-
62
- for src_ami in src_amis:
63
- src_ami_id = src_ami["image_id"]
64
- found_dst_amis = [d for d in dst_amis if d["image_id"] == src_ami_id]
65
- if not found_dst_amis:
66
- logging.info([
67
- "share_ami",
68
- src_account["name"],
69
- dst_account["name"],
70
- src_ami_id,
71
- ])
72
- if not dry_run:
73
- aws_api.share_ami(
74
- src_account, dst_account["uid"], src_ami_id, region
75
- )
76
- # we assume an unshared ami does not have tags
77
- found_dst_amis = [{"image_id": src_ami_id, "tags": []}]
78
-
79
- dst_ami = found_dst_amis[0]
80
- dst_ami_id = dst_ami["image_id"]
81
- dst_ami_tags = dst_ami["tags"]
82
- if MANAGED_TAG not in dst_ami_tags:
83
- logging.info([
84
- "tag_shared_ami",
85
- dst_account["name"],
86
- dst_ami_id,
87
- MANAGED_TAG,
88
- ])
89
- if not dry_run:
90
- aws_api.create_tag(dst_account, dst_ami_id, MANAGED_TAG)
91
- src_ami_tags = src_ami["tags"]
92
- for src_tag in src_ami_tags:
93
- if src_tag not in dst_ami_tags:
94
- logging.info([
95
- "tag_shared_ami",
96
- dst_account["name"],
97
- dst_ami_id,
98
- src_tag,
99
- ])
100
- if not dry_run:
101
- aws_api.create_tag(dst_account, dst_ami_id, src_tag)
87
+ try:
88
+ default_tags = get_settings().default_tags
89
+ except ValueError:
90
+ # no external resources settings found
91
+ default_tags = {}
92
+
93
+ with AWSApi(
94
+ 1,
95
+ sharing_accounts,
96
+ settings=settings,
97
+ init_users=False,
98
+ ) as aws_api:
99
+ for src_account in sharing_accounts:
100
+ for share in src_account.get("sharing") or []:
101
+ if share["provider"] == "ami":
102
+ share_ami(
103
+ dry_run=dry_run,
104
+ src_account=src_account,
105
+ share=share,
106
+ default_tags=default_tags,
107
+ aws_api=aws_api,
108
+ )
@@ -140,7 +140,7 @@ def write_coverage_report_to_mr(
140
140
  approver_reachability = set()
141
141
  for d in change_decisions:
142
142
  approvers = [
143
- f"{cr.context} - {' '.join([f'@{a.org_username}' if a.tag_on_merge_requests else a.org_username for a in cr.approvers])}"
143
+ f"{cr.context} - {' '.join([f'@{a.org_username}' if (a.tag_on_merge_requests or len(cr.approvers) == 1) else a.org_username for a in cr.approvers])}"
144
144
  for cr in d.change_responsibles
145
145
  ]
146
146
  if d.coverable_by_fragment_decisions:
reconcile/queries.py CHANGED
@@ -505,6 +505,12 @@ AWS_ACCOUNTS_QUERY = """
505
505
  name
506
506
  uid
507
507
  supportedDeploymentRegions
508
+ organization {
509
+ payerAccount {
510
+ organizationAccountTags
511
+ }
512
+ tags
513
+ }
508
514
  }
509
515
  ... on AWSAccountSharingOptionAMI_v1 {
510
516
  regex
@@ -3,7 +3,6 @@ from __future__ import annotations
3
3
  import logging
4
4
  import operator
5
5
  import os
6
- import re
7
6
  from functools import lru_cache
8
7
  from threading import Lock
9
8
  from typing import (
@@ -25,6 +24,7 @@ import reconcile.utils.lean_terraform_client as terraform
25
24
  from reconcile.utils.secret_reader import SecretReader, SecretReaderBase
26
25
 
27
26
  if TYPE_CHECKING:
27
+ import re
28
28
  from collections.abc import (
29
29
  Iterable,
30
30
  Iterator,
@@ -1074,28 +1074,40 @@ class AWSApi:
1074
1074
  return [rt["RouteTableId"] for rt in vpc_route_tables]
1075
1075
 
1076
1076
  @staticmethod
1077
- def _filter_amis(
1078
- images: Iterable[ImageTypeDef], regex: str
1079
- ) -> list[dict[str, Any]]:
1080
- results = []
1081
- pattern = re.compile(regex)
1082
- for i in images:
1083
- if not re.search(pattern, i["Name"]):
1084
- continue
1085
- if i["State"] != "available":
1086
- continue
1087
- item = {"image_id": i["ImageId"], "tags": i.get("Tags", [])}
1088
- results.append(item)
1077
+ def normalize_tags(tags: Iterable[TagTypeDef]) -> dict[str, str]:
1078
+ return {tag["Key"]: tag["Value"] for tag in tags}
1089
1079
 
1090
- return results
1080
+ @staticmethod
1081
+ def _filter_amis(
1082
+ images: Iterable[ImageTypeDef],
1083
+ regex: re.Pattern,
1084
+ ) -> dict[str, dict[str, str]]:
1085
+ return {
1086
+ image["ImageId"]: AWSApi.normalize_tags(image.get("Tags", []))
1087
+ for image in images
1088
+ if regex.search(image["Name"]) and image["State"] == "available"
1089
+ }
1091
1090
 
1092
1091
  def get_amis_details(
1093
1092
  self,
1094
1093
  account: Mapping[str, Any],
1095
1094
  owner_account: Mapping[str, Any],
1096
- regex: str,
1095
+ regex: re.Pattern,
1097
1096
  region: str | None = None,
1098
- ) -> list[dict[str, Any]]:
1097
+ ) -> dict[str, dict[str, str]]:
1098
+ """
1099
+ Get AMI details for an account, find AMI name matches regex and state is available.
1100
+ Return ImageId and normalized tags.
1101
+
1102
+ Args:
1103
+ account: AWS account
1104
+ owner_account: AMI owner AWS account uid
1105
+ regex: regex to filter AMI name
1106
+ region: AWS account region
1107
+
1108
+ Returns:
1109
+ dict[str, dict[str, str]]: Key is AMI ImageId, value is AMI normalized tags.
1110
+ """
1099
1111
  ec2 = self._account_ec2_client(account["name"], region_name=region)
1100
1112
  images = self.get_account_amis(ec2, owner=owner_account["uid"])
1101
1113
  return self._filter_amis(images, regex)
@@ -1175,12 +1187,31 @@ class AWSApi:
1175
1187
  client = self._account_cloudwatch_client(account_name, region_name=region_name)
1176
1188
  client.delete_log_group(logGroupName=group_name)
1177
1189
 
1178
- def create_tag(
1179
- self, account: Mapping[str, Any], resource_id: str, tag: Mapping[str, str]
1190
+ def create_tags(
1191
+ self,
1192
+ account: Mapping[str, Any],
1193
+ resource_id: str,
1194
+ tags: Mapping[str, str],
1180
1195
  ) -> None:
1196
+ """
1197
+ Create tags on EC2 resources (AMI)
1198
+
1199
+ Args:
1200
+ account: AWS account
1201
+ resource_id: AWS resource id
1202
+ tags: tags to update
1203
+
1204
+ Returns:
1205
+ None
1206
+ """
1181
1207
  ec2 = self._account_ec2_client(account["name"])
1182
- tag_type_def: TagTypeDef = {"Key": tag["Key"], "Value": tag["Value"]}
1183
- ec2.create_tags(Resources=[resource_id], Tags=[tag_type_def])
1208
+ formatted_tags: list[TagTypeDef] = [
1209
+ {"Key": k, "Value": v} for k, v in tags.items()
1210
+ ]
1211
+ ec2.create_tags(
1212
+ Resources=[resource_id],
1213
+ Tags=formatted_tags,
1214
+ )
1184
1215
 
1185
1216
  def get_alb_network_interface_ips(
1186
1217
  self, account: awsh.Account, service_name: str