qontract-reconcile 0.10.2.dev395__py3-none-any.whl → 0.10.2.dev408__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.
@@ -4937,6 +4937,11 @@
4937
4937
  "name": "AutomatedActionNoOp_v1",
4938
4938
  "ofType": null
4939
4939
  },
4940
+ {
4941
+ "kind": "OBJECT",
4942
+ "name": "AutomatedActionOpenshiftTriggerCronjob_v1",
4943
+ "ofType": null
4944
+ },
4940
4945
  {
4941
4946
  "kind": "OBJECT",
4942
4947
  "name": "AutomatedActionOpenshiftWorkloadDelete_v1",
@@ -27712,6 +27717,11 @@
27712
27717
  "name": "AutomatedActionNoOp_v1",
27713
27718
  "ofType": null
27714
27719
  },
27720
+ {
27721
+ "kind": "OBJECT",
27722
+ "name": "AutomatedActionOpenshiftTriggerCronjob_v1",
27723
+ "ofType": null
27724
+ },
27715
27725
  {
27716
27726
  "kind": "OBJECT",
27717
27727
  "name": "AutomatedActionOpenshiftWorkloadDelete_v1",
@@ -55332,6 +55342,215 @@
55332
55342
  "enumValues": null,
55333
55343
  "possibleTypes": null
55334
55344
  },
55345
+ {
55346
+ "kind": "OBJECT",
55347
+ "name": "AutomatedActionOpenshiftTriggerCronjob_v1",
55348
+ "description": null,
55349
+ "fields": [
55350
+ {
55351
+ "name": "schema",
55352
+ "description": null,
55353
+ "args": [],
55354
+ "type": {
55355
+ "kind": "NON_NULL",
55356
+ "name": null,
55357
+ "ofType": {
55358
+ "kind": "SCALAR",
55359
+ "name": "String",
55360
+ "ofType": null
55361
+ }
55362
+ },
55363
+ "isDeprecated": false,
55364
+ "deprecationReason": null
55365
+ },
55366
+ {
55367
+ "name": "path",
55368
+ "description": null,
55369
+ "args": [],
55370
+ "type": {
55371
+ "kind": "NON_NULL",
55372
+ "name": null,
55373
+ "ofType": {
55374
+ "kind": "SCALAR",
55375
+ "name": "String",
55376
+ "ofType": null
55377
+ }
55378
+ },
55379
+ "isDeprecated": false,
55380
+ "deprecationReason": null
55381
+ },
55382
+ {
55383
+ "name": "type",
55384
+ "description": null,
55385
+ "args": [],
55386
+ "type": {
55387
+ "kind": "NON_NULL",
55388
+ "name": null,
55389
+ "ofType": {
55390
+ "kind": "SCALAR",
55391
+ "name": "String",
55392
+ "ofType": null
55393
+ }
55394
+ },
55395
+ "isDeprecated": false,
55396
+ "deprecationReason": null
55397
+ },
55398
+ {
55399
+ "name": "description",
55400
+ "description": null,
55401
+ "args": [],
55402
+ "type": {
55403
+ "kind": "SCALAR",
55404
+ "name": "String",
55405
+ "ofType": null
55406
+ },
55407
+ "isDeprecated": false,
55408
+ "deprecationReason": null
55409
+ },
55410
+ {
55411
+ "name": "maxOps",
55412
+ "description": null,
55413
+ "args": [],
55414
+ "type": {
55415
+ "kind": "NON_NULL",
55416
+ "name": null,
55417
+ "ofType": {
55418
+ "kind": "SCALAR",
55419
+ "name": "Int",
55420
+ "ofType": null
55421
+ }
55422
+ },
55423
+ "isDeprecated": false,
55424
+ "deprecationReason": null
55425
+ },
55426
+ {
55427
+ "name": "instances",
55428
+ "description": null,
55429
+ "args": [],
55430
+ "type": {
55431
+ "kind": "NON_NULL",
55432
+ "name": null,
55433
+ "ofType": {
55434
+ "kind": "LIST",
55435
+ "name": null,
55436
+ "ofType": {
55437
+ "kind": "NON_NULL",
55438
+ "name": null,
55439
+ "ofType": {
55440
+ "kind": "OBJECT",
55441
+ "name": "AutomatedActionsInstance_v1",
55442
+ "ofType": null
55443
+ }
55444
+ }
55445
+ }
55446
+ },
55447
+ "isDeprecated": false,
55448
+ "deprecationReason": null
55449
+ },
55450
+ {
55451
+ "name": "permissions",
55452
+ "description": null,
55453
+ "args": [],
55454
+ "type": {
55455
+ "kind": "LIST",
55456
+ "name": null,
55457
+ "ofType": {
55458
+ "kind": "NON_NULL",
55459
+ "name": null,
55460
+ "ofType": {
55461
+ "kind": "OBJECT",
55462
+ "name": "PermissionAutomatedActions_v1",
55463
+ "ofType": null
55464
+ }
55465
+ }
55466
+ },
55467
+ "isDeprecated": false,
55468
+ "deprecationReason": null
55469
+ },
55470
+ {
55471
+ "name": "arguments",
55472
+ "description": null,
55473
+ "args": [],
55474
+ "type": {
55475
+ "kind": "NON_NULL",
55476
+ "name": null,
55477
+ "ofType": {
55478
+ "kind": "LIST",
55479
+ "name": null,
55480
+ "ofType": {
55481
+ "kind": "NON_NULL",
55482
+ "name": null,
55483
+ "ofType": {
55484
+ "kind": "OBJECT",
55485
+ "name": "AutomatedActionOpenshiftTriggerCronjobArgument_v1",
55486
+ "ofType": null
55487
+ }
55488
+ }
55489
+ }
55490
+ },
55491
+ "isDeprecated": false,
55492
+ "deprecationReason": null
55493
+ }
55494
+ ],
55495
+ "inputFields": null,
55496
+ "interfaces": [
55497
+ {
55498
+ "kind": "INTERFACE",
55499
+ "name": "AutomatedAction_v1",
55500
+ "ofType": null
55501
+ },
55502
+ {
55503
+ "kind": "INTERFACE",
55504
+ "name": "DatafileObject_v1",
55505
+ "ofType": null
55506
+ }
55507
+ ],
55508
+ "enumValues": null,
55509
+ "possibleTypes": null
55510
+ },
55511
+ {
55512
+ "kind": "OBJECT",
55513
+ "name": "AutomatedActionOpenshiftTriggerCronjobArgument_v1",
55514
+ "description": null,
55515
+ "fields": [
55516
+ {
55517
+ "name": "namespace",
55518
+ "description": null,
55519
+ "args": [],
55520
+ "type": {
55521
+ "kind": "NON_NULL",
55522
+ "name": null,
55523
+ "ofType": {
55524
+ "kind": "OBJECT",
55525
+ "name": "Namespace_v1",
55526
+ "ofType": null
55527
+ }
55528
+ },
55529
+ "isDeprecated": false,
55530
+ "deprecationReason": null
55531
+ },
55532
+ {
55533
+ "name": "cronjob",
55534
+ "description": null,
55535
+ "args": [],
55536
+ "type": {
55537
+ "kind": "NON_NULL",
55538
+ "name": null,
55539
+ "ofType": {
55540
+ "kind": "SCALAR",
55541
+ "name": "String",
55542
+ "ofType": null
55543
+ }
55544
+ },
55545
+ "isDeprecated": false,
55546
+ "deprecationReason": null
55547
+ }
55548
+ ],
55549
+ "inputFields": null,
55550
+ "interfaces": [],
55551
+ "enumValues": null,
55552
+ "possibleTypes": null
55553
+ },
55335
55554
  {
55336
55555
  "kind": "OBJECT",
55337
55556
  "name": "AutomatedActionOpenshiftWorkloadDelete_v1",
reconcile/ocm/types.py CHANGED
@@ -4,6 +4,7 @@ from pydantic import (
4
4
  BaseModel,
5
5
  Extra,
6
6
  Field,
7
+ validator,
7
8
  )
8
9
 
9
10
 
@@ -36,7 +37,11 @@ class OCMClusterSpec(BaseModel):
36
37
  initial_version: str | None
37
38
  version: str
38
39
  hypershift: bool | None
39
- fips: bool | None
40
+ fips: bool = False
41
+
42
+ @validator("fips", pre=True)
43
+ def set_fips_default(cls, v: bool | None) -> bool:
44
+ return v or False
40
45
 
41
46
  class Config:
42
47
  extra = Extra.forbid
@@ -30,9 +30,11 @@ from reconcile.utils import (
30
30
  )
31
31
  from reconcile.utils.constants import DEFAULT_THREAD_POOL_SIZE
32
32
  from reconcile.utils.oc import (
33
+ AmbiguousResourceTypeError,
33
34
  DeploymentFieldIsImmutableError,
34
35
  FieldIsImmutableError,
35
36
  InvalidValueApplyError,
37
+ KindNotFoundError,
36
38
  MayNotChangeOnceSetError,
37
39
  MetaDataAnnotationsTooLongApplyError,
38
40
  OC_Map,
@@ -128,6 +130,29 @@ class ClusterMap(Protocol):
128
130
  ) -> list[str]: ...
129
131
 
130
132
 
133
+ def validate_managed_resource_types(
134
+ oc: OCCli,
135
+ managed_resource_types: Iterable[str],
136
+ managed_resource_names: Iterable[Mapping[str, Any]],
137
+ cluster_scope_resource_validation: bool,
138
+ ) -> None:
139
+ """Validate the managed resource types."""
140
+ managed_resources = [
141
+ managed_resource_name["resource"]
142
+ for managed_resource_name in managed_resource_names
143
+ ]
144
+ for managed_resource_type in managed_resource_types:
145
+ # The k8s kind must be supported by the cluster
146
+ resource = oc.get_api_resource(managed_resource_type)
147
+
148
+ if cluster_scope_resource_validation and not resource.namespaced:
149
+ # cluster-scoped resources must be use managedResourceNames!
150
+ if managed_resource_type not in managed_resources:
151
+ raise ValidationError(
152
+ f"Cluster-scoped resource {managed_resource_type} must be managed by name only. Please use 'managedResourceNames' field to specify the names of the resources to manage."
153
+ )
154
+
155
+
131
156
  def init_specs_to_fetch(
132
157
  ri: ResourceInventory,
133
158
  oc_map: ClusterMap,
@@ -136,6 +161,7 @@ def init_specs_to_fetch(
136
161
  override_managed_types: Iterable[str] | None = None,
137
162
  managed_types_key: str = "managedResourceTypes",
138
163
  cluster_admin: bool = False,
164
+ cluster_scope_resource_validation: bool = False,
139
165
  ) -> list[StateSpec]:
140
166
  state_specs: list[StateSpec] = []
141
167
 
@@ -163,9 +189,27 @@ def init_specs_to_fetch(
163
189
  logging.log(level=ex.log_level, msg=ex.message)
164
190
  continue
165
191
 
192
+ managed_resource_names = namespace_info.get("managedResourceNames") or []
193
+ try:
194
+ validate_managed_resource_types(
195
+ oc,
196
+ managed_types,
197
+ managed_resource_names,
198
+ cluster_scope_resource_validation=cluster_scope_resource_validation,
199
+ )
200
+ except KindNotFoundError:
201
+ # We must allow kinds that are not supported by the cluster because:
202
+ # 1. We install CRD with an operator in the same MR
203
+ # 2. SAAS files initialize the namespace objects with managedResourceTypes from the SAAS file
204
+ # and we can't expect that all of those are valid for all clusters
205
+ pass
206
+ except (AmbiguousResourceTypeError, ValidationError) as e:
207
+ ri.register_error()
208
+ logging.error(f"[{cluster}/{namespace_info['name']}] {e}")
209
+ continue
210
+
166
211
  namespace = namespace_info["name"]
167
212
  # These may exit but have a value of None
168
- managed_resource_names = namespace_info.get("managedResourceNames") or []
169
213
  managed_resource_type_overrides = (
170
214
  namespace_info.get("managedResourceTypeOverrides") or []
171
215
  )
@@ -340,6 +384,7 @@ def fetch_current_state(
340
384
  cluster_admin: bool = False,
341
385
  caller: str | None = None,
342
386
  init_projects: bool = False,
387
+ cluster_scope_resource_validation: bool = False,
343
388
  ) -> tuple[ResourceInventory, OC_Map]:
344
389
  ri = ResourceInventory()
345
390
  settings = queries.get_app_interface_settings()
@@ -362,6 +407,7 @@ def fetch_current_state(
362
407
  clusters=clusters,
363
408
  override_managed_types=override_managed_types,
364
409
  cluster_admin=cluster_admin,
410
+ cluster_scope_resource_validation=cluster_scope_resource_validation,
365
411
  )
366
412
  threaded.run(
367
413
  populate_current_state,
@@ -806,7 +806,11 @@ def fetch_data(
806
806
  init_api_resources=init_api_resources,
807
807
  )
808
808
  state_specs = ob.init_specs_to_fetch(
809
- ri, oc_map, namespaces=namespaces, override_managed_types=overrides
809
+ ri,
810
+ oc_map,
811
+ namespaces=namespaces,
812
+ override_managed_types=overrides,
813
+ cluster_scope_resource_validation=True,
810
814
  )
811
815
  threaded.run(fetch_states, state_specs, thread_pool_size, ri=ri, settings=settings)
812
816
 
@@ -861,7 +865,7 @@ def canonicalize_namespaces(
861
865
  elif providers[0] == "route":
862
866
  override = ["Route"]
863
867
  elif providers[0] == "prometheus-rule":
864
- override = ["PrometheusRule"]
868
+ override = ["PrometheusRule.monitoring.coreos.com"]
865
869
 
866
870
  namespace_info["openshiftResources"] = ors
867
871
  canonicalized_namespaces.append(namespace_info)
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
@@ -100,7 +100,7 @@ class K8sJobController:
100
100
  """
101
101
  new_cache = {}
102
102
  for item in self.oc.get_items(
103
- kind="Job",
103
+ kind="Job.batch",
104
104
  namespace=self.namespace,
105
105
  ):
106
106
  openshift_resource = OpenshiftResource(
@@ -38,6 +38,8 @@ class JobValidationError(Exception):
38
38
 
39
39
 
40
40
  JOB_GENERATION_ANNOTATION = "qontract-reconcile/job.generation"
41
+ MAX_JOB_NAME_LENGTH = 63
42
+ UNIT_OF_WORK_DIGEST_LENGTH = 10
41
43
 
42
44
 
43
45
  class K8sJob(ABC):
@@ -72,7 +74,21 @@ class K8sJob(ABC):
72
74
  """
73
75
 
74
76
  def name(self) -> str:
75
- return f"{self.name_prefix()}-{self.unit_of_work_digest()}"
77
+ """
78
+ Generate the full job name by combining the name prefix with a digest.
79
+
80
+ The name is constructed from the name_prefix (truncated to ensure total
81
+ length compliance) and the unit_of_work_digest. The total length is
82
+ limited to MAX_JOB_NAME_LENGTH (63 characters) to comply with Kubernetes
83
+ naming constraints.
84
+
85
+ Returns:
86
+ A unique job name in the format: {name_prefix}-{digest}
87
+ """
88
+ prefix = self.name_prefix()[
89
+ : MAX_JOB_NAME_LENGTH - UNIT_OF_WORK_DIGEST_LENGTH - 1
90
+ ]
91
+ return f"{prefix}-{self.unit_of_work_digest(UNIT_OF_WORK_DIGEST_LENGTH)}"
76
92
 
77
93
  @abstractmethod
78
94
  def name_prefix(self) -> str: