qontract-reconcile 0.10.2.dev345__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.
- {qontract_reconcile-0.10.2.dev345.dist-info → qontract_reconcile-0.10.2.dev408.dist-info}/METADATA +11 -10
- {qontract_reconcile-0.10.2.dev345.dist-info → qontract_reconcile-0.10.2.dev408.dist-info}/RECORD +126 -120
- reconcile/aus/base.py +17 -14
- reconcile/automated_actions/config/integration.py +12 -0
- reconcile/aws_account_manager/integration.py +2 -2
- reconcile/aws_ami_cleanup/integration.py +6 -7
- reconcile/aws_ami_share.py +69 -62
- reconcile/aws_cloudwatch_log_retention/integration.py +155 -126
- reconcile/aws_ecr_image_pull_secrets.py +2 -2
- reconcile/aws_iam_keys.py +1 -0
- reconcile/aws_saml_idp/integration.py +7 -1
- reconcile/aws_saml_roles/integration.py +9 -3
- reconcile/change_owners/change_owners.py +1 -1
- reconcile/change_owners/diff.py +2 -4
- reconcile/checkpoint.py +11 -3
- reconcile/cli.py +33 -8
- reconcile/dashdotdb_dora.py +4 -11
- reconcile/database_access_manager.py +118 -111
- reconcile/endpoints_discovery/integration.py +4 -1
- reconcile/endpoints_discovery/merge_request_manager.py +9 -11
- reconcile/external_resources/factories.py +5 -12
- reconcile/external_resources/integration.py +1 -1
- reconcile/external_resources/manager.py +5 -3
- reconcile/external_resources/meta.py +0 -1
- reconcile/external_resources/model.py +10 -10
- reconcile/external_resources/reconciler.py +5 -2
- reconcile/external_resources/secrets_sync.py +4 -6
- reconcile/external_resources/state.py +5 -4
- reconcile/gabi_authorized_users.py +8 -5
- reconcile/gitlab_housekeeping.py +13 -15
- reconcile/gitlab_mr_sqs_consumer.py +2 -2
- reconcile/gitlab_owners.py +15 -11
- reconcile/gql_definitions/automated_actions/instance.py +41 -2
- reconcile/gql_definitions/aws_ami_cleanup/aws_accounts.py +10 -0
- reconcile/gql_definitions/aws_cloudwatch_log_retention/aws_accounts.py +22 -61
- reconcile/gql_definitions/aws_saml_idp/aws_accounts.py +10 -0
- reconcile/gql_definitions/aws_saml_roles/aws_accounts.py +10 -0
- reconcile/gql_definitions/common/aws_vpc_requests.py +10 -0
- reconcile/gql_definitions/common/clusters.py +2 -0
- reconcile/gql_definitions/external_resources/external_resources_namespaces.py +84 -1
- reconcile/gql_definitions/external_resources/external_resources_settings.py +2 -0
- reconcile/gql_definitions/fragments/aws_account_common.py +2 -0
- reconcile/gql_definitions/fragments/aws_organization.py +33 -0
- reconcile/gql_definitions/fragments/aws_vpc_request.py +2 -0
- reconcile/gql_definitions/introspection.json +3474 -1986
- reconcile/gql_definitions/jira_permissions_validator/jira_boards_for_permissions_validator.py +4 -0
- reconcile/gql_definitions/terraform_init/aws_accounts.py +14 -0
- reconcile/gql_definitions/terraform_resources/terraform_resources_namespaces.py +33 -1
- reconcile/gql_definitions/terraform_tgw_attachments/aws_accounts.py +10 -0
- reconcile/jenkins_worker_fleets.py +1 -0
- reconcile/jira_permissions_validator.py +236 -121
- reconcile/ocm/types.py +6 -0
- reconcile/openshift_base.py +47 -1
- reconcile/openshift_cluster_bots.py +2 -1
- reconcile/openshift_resources_base.py +6 -2
- reconcile/openshift_saas_deploy.py +2 -2
- reconcile/openshift_saas_deploy_trigger_cleaner.py +3 -5
- reconcile/openshift_upgrade_watcher.py +3 -3
- reconcile/queries.py +131 -0
- reconcile/saas_auto_promotions_manager/subscriber.py +4 -3
- reconcile/slack_usergroups.py +4 -3
- reconcile/sql_query.py +1 -0
- reconcile/statuspage/integrations/maintenances.py +4 -3
- reconcile/statuspage/status.py +5 -8
- reconcile/templates/rosa-classic-cluster-creation.sh.j2 +4 -0
- reconcile/templates/rosa-hcp-cluster-creation.sh.j2 +3 -0
- reconcile/templating/renderer.py +2 -1
- reconcile/terraform_aws_route53.py +7 -1
- reconcile/terraform_init/integration.py +185 -21
- reconcile/terraform_resources.py +11 -1
- reconcile/terraform_tgw_attachments.py +7 -1
- reconcile/terraform_users.py +7 -0
- reconcile/terraform_vpc_peerings.py +14 -3
- reconcile/terraform_vpc_resources/integration.py +7 -0
- reconcile/typed_queries/aws_account_tags.py +41 -0
- reconcile/typed_queries/saas_files.py +2 -2
- reconcile/utils/aggregated_list.py +4 -3
- reconcile/utils/aws_api.py +51 -20
- reconcile/utils/aws_api_typed/api.py +38 -9
- reconcile/utils/aws_api_typed/cloudformation.py +149 -0
- reconcile/utils/aws_api_typed/logs.py +73 -0
- reconcile/utils/datetime_util.py +67 -0
- reconcile/utils/differ.py +2 -3
- reconcile/utils/early_exit_cache.py +3 -2
- reconcile/utils/expiration.py +7 -3
- reconcile/utils/external_resource_spec.py +24 -1
- reconcile/utils/filtering.py +1 -1
- reconcile/utils/helm.py +2 -1
- reconcile/utils/helpers.py +1 -1
- reconcile/utils/jinja2/utils.py +4 -96
- reconcile/utils/jira_client.py +82 -63
- reconcile/utils/jjb_client.py +9 -12
- reconcile/utils/jobcontroller/controller.py +1 -1
- reconcile/utils/jobcontroller/models.py +17 -1
- reconcile/utils/json.py +32 -0
- reconcile/utils/merge_request_manager/merge_request_manager.py +3 -3
- reconcile/utils/merge_request_manager/parser.py +2 -2
- reconcile/utils/mr/app_interface_reporter.py +2 -2
- reconcile/utils/mr/base.py +2 -2
- reconcile/utils/mr/notificator.py +2 -2
- reconcile/utils/mr/update_access_report_base.py +3 -4
- reconcile/utils/oc.py +113 -95
- reconcile/utils/oc_filters.py +3 -3
- reconcile/utils/ocm/products.py +6 -0
- reconcile/utils/ocm/search_filters.py +3 -6
- reconcile/utils/ocm/service_log.py +3 -5
- reconcile/utils/openshift_resource.py +10 -5
- reconcile/utils/output.py +3 -2
- reconcile/utils/pagerduty_api.py +5 -5
- reconcile/utils/runtime/integration.py +1 -2
- reconcile/utils/runtime/runner.py +2 -2
- reconcile/utils/saasherder/models.py +2 -1
- reconcile/utils/saasherder/saasherder.py +9 -7
- reconcile/utils/slack_api.py +24 -2
- reconcile/utils/sloth.py +171 -2
- reconcile/utils/sqs_gateway.py +2 -1
- reconcile/utils/state.py +2 -1
- reconcile/utils/terraform_client.py +4 -3
- reconcile/utils/terrascript_aws_client.py +165 -111
- reconcile/utils/vault.py +1 -1
- reconcile/vault_replication.py +107 -42
- tools/app_interface_reporter.py +4 -4
- tools/cli_commands/systems_and_tools.py +5 -1
- tools/qontract_cli.py +25 -13
- {qontract_reconcile-0.10.2.dev345.dist-info → qontract_reconcile-0.10.2.dev408.dist-info}/WHEEL +0 -0
- {qontract_reconcile-0.10.2.dev345.dist-info → qontract_reconcile-0.10.2.dev408.dist-info}/entry_points.txt +0 -0
reconcile/utils/oc.py
CHANGED
|
@@ -12,7 +12,6 @@ import threading
|
|
|
12
12
|
import time
|
|
13
13
|
from contextlib import suppress
|
|
14
14
|
from dataclasses import dataclass
|
|
15
|
-
from datetime import datetime
|
|
16
15
|
from functools import cache, wraps
|
|
17
16
|
from subprocess import Popen
|
|
18
17
|
from threading import Lock
|
|
@@ -47,6 +46,8 @@ from sretoolbox.utils import (
|
|
|
47
46
|
)
|
|
48
47
|
|
|
49
48
|
from reconcile.status import RunningState
|
|
49
|
+
from reconcile.utils.datetime_util import utc_now
|
|
50
|
+
from reconcile.utils.json import json_dumps
|
|
50
51
|
from reconcile.utils.jump_host import (
|
|
51
52
|
JumphostParameters,
|
|
52
53
|
JumpHostSSH,
|
|
@@ -67,7 +68,8 @@ if TYPE_CHECKING:
|
|
|
67
68
|
urllib3.disable_warnings()
|
|
68
69
|
|
|
69
70
|
GET_REPLICASET_MAX_ATTEMPTS = 20
|
|
70
|
-
|
|
71
|
+
DEFAULT_GROUP = ""
|
|
72
|
+
PROJECT_KIND = "Project.project.openshift.io"
|
|
71
73
|
|
|
72
74
|
oc_run_execution_counter = Counter(
|
|
73
75
|
name="oc_run_execution_counter",
|
|
@@ -144,6 +146,14 @@ class RequestEntityTooLargeError(Exception):
|
|
|
144
146
|
pass
|
|
145
147
|
|
|
146
148
|
|
|
149
|
+
class KindNotFoundError(Exception):
|
|
150
|
+
pass
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class AmbiguousResourceTypeError(Exception):
|
|
154
|
+
pass
|
|
155
|
+
|
|
156
|
+
|
|
147
157
|
class OCDecorators:
|
|
148
158
|
@classmethod
|
|
149
159
|
def process_reconcile_time(cls, function: Callable) -> Callable:
|
|
@@ -379,10 +389,7 @@ class OCCli:
|
|
|
379
389
|
|
|
380
390
|
self.init_projects = init_projects
|
|
381
391
|
if self.init_projects:
|
|
382
|
-
if self.is_kind_supported("
|
|
383
|
-
kind = "Project.project.openshift.io"
|
|
384
|
-
else:
|
|
385
|
-
kind = "Namespace"
|
|
392
|
+
kind = PROJECT_KIND if self.is_kind_supported(PROJECT_KIND) else "Namespace"
|
|
386
393
|
self.projects = {p["metadata"]["name"] for p in self.get_all(kind)["items"]}
|
|
387
394
|
|
|
388
395
|
self.slow_oc_reconcile_threshold = float(
|
|
@@ -452,10 +459,7 @@ class OCCli:
|
|
|
452
459
|
|
|
453
460
|
self.init_projects = init_projects
|
|
454
461
|
if self.init_projects:
|
|
455
|
-
if self.is_kind_supported("
|
|
456
|
-
kind = "Project.project.openshift.io"
|
|
457
|
-
else:
|
|
458
|
-
kind = "Namespace"
|
|
462
|
+
kind = PROJECT_KIND if self.is_kind_supported(PROJECT_KIND) else "Namespace"
|
|
459
463
|
self.projects = {p["metadata"]["name"] for p in self.get_all(kind)["items"]}
|
|
460
464
|
|
|
461
465
|
self.slow_oc_reconcile_threshold = float(
|
|
@@ -563,7 +567,7 @@ class OCCli:
|
|
|
563
567
|
"-f",
|
|
564
568
|
"-",
|
|
565
569
|
] + parameters_to_process
|
|
566
|
-
result = self._run(cmd, stdin=
|
|
570
|
+
result = self._run(cmd, stdin=json_dumps(template))
|
|
567
571
|
return json.loads(result)["items"]
|
|
568
572
|
|
|
569
573
|
@OCDecorators.process_reconcile_time
|
|
@@ -592,7 +596,7 @@ class OCCli:
|
|
|
592
596
|
def patch(
|
|
593
597
|
self, namespace: str, kind: str, name: str, patch: Mapping[str, Any]
|
|
594
598
|
) -> OCProcessReconcileTimeDecoratorMsg:
|
|
595
|
-
cmd = ["patch", "-n", namespace, kind, name, "-p",
|
|
599
|
+
cmd = ["patch", "-n", namespace, kind, name, "-p", json_dumps(patch)]
|
|
596
600
|
self._run(cmd)
|
|
597
601
|
resource = OR({"kind": kind, "metadata": {"name": name}}, "", "")
|
|
598
602
|
return self._msg_to_process_reconcile_time(namespace, resource)
|
|
@@ -636,11 +640,9 @@ class OCCli:
|
|
|
636
640
|
def project_exists(self, name: str) -> bool:
|
|
637
641
|
if name in self.projects:
|
|
638
642
|
return True
|
|
643
|
+
kind = PROJECT_KIND if self.is_kind_supported(PROJECT_KIND) else "Namespace"
|
|
639
644
|
try:
|
|
640
|
-
|
|
641
|
-
self.get(None, "Project.project.openshift.io", name)
|
|
642
|
-
else:
|
|
643
|
-
self.get(None, "Namespace", name)
|
|
645
|
+
self.get(None, kind, name)
|
|
644
646
|
except StatusCodeError as e:
|
|
645
647
|
if "NotFound" in str(e):
|
|
646
648
|
return False
|
|
@@ -649,7 +651,7 @@ class OCCli:
|
|
|
649
651
|
|
|
650
652
|
@OCDecorators.process_reconcile_time
|
|
651
653
|
def new_project(self, namespace: str) -> OCProcessReconcileTimeDecoratorMsg:
|
|
652
|
-
if self.is_kind_supported(
|
|
654
|
+
if self.is_kind_supported(PROJECT_KIND):
|
|
653
655
|
cmd = ["new-project", namespace]
|
|
654
656
|
else:
|
|
655
657
|
cmd = ["create", "namespace", namespace]
|
|
@@ -665,7 +667,7 @@ class OCCli:
|
|
|
665
667
|
|
|
666
668
|
@OCDecorators.process_reconcile_time
|
|
667
669
|
def delete_project(self, namespace: str) -> OCProcessReconcileTimeDecoratorMsg:
|
|
668
|
-
if self.is_kind_supported(
|
|
670
|
+
if self.is_kind_supported(PROJECT_KIND):
|
|
669
671
|
cmd = ["delete", "project", namespace]
|
|
670
672
|
else:
|
|
671
673
|
cmd = ["delete", "namespace", namespace]
|
|
@@ -716,7 +718,7 @@ class OCCli:
|
|
|
716
718
|
cmd = ["sa", "-n", namespace, "get-token", name]
|
|
717
719
|
return self._run(cmd)
|
|
718
720
|
|
|
719
|
-
def get_api_resources(self) -> dict[str,
|
|
721
|
+
def get_api_resources(self) -> dict[str, list[OCCliApiResource]]:
|
|
720
722
|
with self.api_resources_lock:
|
|
721
723
|
if not self.api_resources:
|
|
722
724
|
cmd = ["api-resources", "--no-headers"]
|
|
@@ -1009,7 +1011,7 @@ class OCCli:
|
|
|
1009
1011
|
name = obj["metadata"]["name"]
|
|
1010
1012
|
logging.info([f"recycle_{kind.lower()}", self.cluster_name, namespace, name])
|
|
1011
1013
|
if not dry_run:
|
|
1012
|
-
now =
|
|
1014
|
+
now = utc_now()
|
|
1013
1015
|
recycle_time = now.strftime("%d/%m/%Y %H:%M:%S")
|
|
1014
1016
|
|
|
1015
1017
|
# get the object in case it was modified
|
|
@@ -1020,7 +1022,7 @@ class OCCli:
|
|
|
1020
1022
|
a["recycle.time"] = recycle_time
|
|
1021
1023
|
obj["spec"]["template"]["metadata"]["annotations"] = a
|
|
1022
1024
|
cmd = ["apply", "-n", namespace, "-f", "-"]
|
|
1023
|
-
stdin =
|
|
1025
|
+
stdin = json_dumps(obj)
|
|
1024
1026
|
self._run(cmd, stdin=stdin, apply=True)
|
|
1025
1027
|
|
|
1026
1028
|
def get_obj_root_owner(
|
|
@@ -1195,76 +1197,90 @@ class OCCli:
|
|
|
1195
1197
|
|
|
1196
1198
|
return out_json
|
|
1197
1199
|
|
|
1198
|
-
def
|
|
1199
|
-
|
|
1200
|
-
# the api resources initialization.
|
|
1201
|
-
if not self.api_resources:
|
|
1202
|
-
self.get_api_resources()
|
|
1200
|
+
def parse_kind(self, kind: str) -> tuple[str, str, str]:
|
|
1201
|
+
"""Parse a Kubernetes kind string into its components.
|
|
1203
1202
|
|
|
1204
|
-
|
|
1205
|
-
kind
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
else:
|
|
1209
|
-
raise StatusCodeError(f"{self.server}: {kind} does not exist")
|
|
1210
|
-
|
|
1211
|
-
# if a kind_group has more than 1 entry than the kind_name is in
|
|
1212
|
-
# the format kind.apigroup. Find the apigroup/version that matches
|
|
1213
|
-
# the apigroup passed with the kind_name
|
|
1214
|
-
if len(kind_group) > 1:
|
|
1215
|
-
apigroup_override = kind_group[1]
|
|
1216
|
-
find = False
|
|
1217
|
-
for gv in self.api_resources[kind]:
|
|
1218
|
-
if apigroup_override == gv.group:
|
|
1219
|
-
if not gv.group:
|
|
1220
|
-
group_version = gv.api_version
|
|
1221
|
-
else:
|
|
1222
|
-
group_version = f"{gv.group}/{gv.api_version}"
|
|
1223
|
-
find = True
|
|
1224
|
-
break
|
|
1203
|
+
Supports three formats:
|
|
1204
|
+
- kind
|
|
1205
|
+
- kind.group.whatever
|
|
1206
|
+
- kind.group.whatever/version
|
|
1225
1207
|
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1208
|
+
Args:
|
|
1209
|
+
kind: A Kubernetes kind string in one of the supported formats
|
|
1210
|
+
|
|
1211
|
+
Returns:
|
|
1212
|
+
Tuple of (kind, group, version) where missing parts are empty strings
|
|
1213
|
+
|
|
1214
|
+
Raises:
|
|
1215
|
+
ValueError: If the kind string format is invalid
|
|
1216
|
+
|
|
1217
|
+
Examples:
|
|
1218
|
+
>>> parse_kind_string("Deployment")
|
|
1219
|
+
('Deployment', '', '')
|
|
1220
|
+
>>> parse_kind_string("ClusterRoleBinding.rbac.authorization.k8s.io")
|
|
1221
|
+
('ClusterRoleBinding', 'rbac.authorization.k8s.io', '')
|
|
1222
|
+
>>> parse_kind_string("CustomResource.mygroup.example.com/v1")
|
|
1223
|
+
('CustomResource', 'mygroup.example.com', 'v1')
|
|
1224
|
+
"""
|
|
1225
|
+
pattern = r"^(?P<kind>[^./]+)(?:\.(?P<group>[^/]+))?(?:/(?P<version>.+))?$"
|
|
1226
|
+
match = re.match(pattern, kind)
|
|
1227
|
+
if not match:
|
|
1228
|
+
raise ValueError(f"Invalid kind string: {kind}")
|
|
1229
|
+
|
|
1230
|
+
kind = match.group("kind") or ""
|
|
1231
|
+
group = match.group("group") or DEFAULT_GROUP
|
|
1232
|
+
version = match.group("version") or ""
|
|
1233
|
+
|
|
1234
|
+
return kind, group, version
|
|
1231
1235
|
|
|
1232
1236
|
def is_kind_supported(self, kind: str) -> bool:
|
|
1233
|
-
|
|
1234
|
-
# the api resources initialization.
|
|
1235
|
-
if not self.api_resources:
|
|
1236
|
-
self.get_api_resources()
|
|
1237
|
+
"""Returns True if the given kind is supported by the cluster, False otherwise.
|
|
1237
1238
|
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
else:
|
|
1245
|
-
return kind in self.api_resources
|
|
1239
|
+
Kind can be either kind, kind.group or kind.group/version."""
|
|
1240
|
+
try:
|
|
1241
|
+
self.get_api_resource(kind)
|
|
1242
|
+
return True
|
|
1243
|
+
except KindNotFoundError:
|
|
1244
|
+
return False
|
|
1246
1245
|
|
|
1247
1246
|
def is_kind_namespaced(self, kind: str) -> bool:
|
|
1248
|
-
|
|
1249
|
-
|
|
1247
|
+
"""Returns True if the given kind is namespaced, False if it's cluster scoped.
|
|
1248
|
+
|
|
1249
|
+
Kind can be either kind, kind.group or kind.group/version."""
|
|
1250
|
+
return self.get_api_resource(kind).namespaced
|
|
1251
|
+
|
|
1252
|
+
def get_api_resource(self, kind: str) -> OCCliApiResource:
|
|
1253
|
+
"""Return the OCCliApiResource for the given resource type.
|
|
1254
|
+
|
|
1255
|
+
Resource type can be either kind, kind.group or kind.group/version.
|
|
1256
|
+
If kind is not unique, group must be specified."""
|
|
1257
|
+
|
|
1250
1258
|
if not self.api_resources:
|
|
1251
|
-
|
|
1259
|
+
raise RuntimeError("API resources not initialized")
|
|
1252
1260
|
|
|
1253
|
-
|
|
1254
|
-
kind = kg[0]
|
|
1261
|
+
kind, group, _ = self.parse_kind(kind)
|
|
1255
1262
|
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
raise StatusCodeError(f"Kind {kind} does not exist in the ApiServer")
|
|
1263
|
+
if not (resources := self.api_resources.get(kind)):
|
|
1264
|
+
# the kind not found at all
|
|
1265
|
+
raise KindNotFoundError(f"Unsupported resource type: {kind}")
|
|
1260
1266
|
|
|
1261
|
-
if len(
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1267
|
+
if len(resources) == 1 and group == DEFAULT_GROUP:
|
|
1268
|
+
return resources[0]
|
|
1269
|
+
|
|
1270
|
+
# get the resource with the specified group
|
|
1271
|
+
if resource := next((r for r in resources if r.group == group), None):
|
|
1272
|
+
return resource
|
|
1273
|
+
|
|
1274
|
+
# no resource with the specified group found
|
|
1275
|
+
if group == DEFAULT_GROUP:
|
|
1276
|
+
message = (
|
|
1277
|
+
f"Ambiguous resource type: {kind}. "
|
|
1278
|
+
"Please fully qualify it with its API group. E.g., ClusterRoleBinding -> ClusterRoleBinding.rbac.authorization.k8s.io"
|
|
1279
|
+
)
|
|
1280
|
+
raise AmbiguousResourceTypeError(message)
|
|
1281
|
+
|
|
1282
|
+
# group was specified but no matching resource found
|
|
1283
|
+
raise KindNotFoundError(f"Unsupported resource type: {kind}")
|
|
1268
1284
|
|
|
1269
1285
|
|
|
1270
1286
|
REQUEST_TIMEOUT = 60
|
|
@@ -1304,20 +1320,16 @@ class OCNative(OCCli):
|
|
|
1304
1320
|
|
|
1305
1321
|
server = connection_parameters.server_url
|
|
1306
1322
|
|
|
1307
|
-
if server:
|
|
1308
|
-
|
|
1309
|
-
self.api_resources = self.get_api_resources()
|
|
1323
|
+
if not server:
|
|
1324
|
+
raise Exception("Server name is required!")
|
|
1310
1325
|
|
|
1311
|
-
|
|
1312
|
-
|
|
1326
|
+
self.client = self._get_client(server, token)
|
|
1327
|
+
self.api_resources = self.get_api_resources()
|
|
1313
1328
|
|
|
1314
1329
|
self.projects = set()
|
|
1315
1330
|
self.init_projects = init_projects
|
|
1316
1331
|
if self.init_projects:
|
|
1317
|
-
if self.is_kind_supported("
|
|
1318
|
-
kind = "Project.project.openshift.io"
|
|
1319
|
-
else:
|
|
1320
|
-
kind = "Namespace"
|
|
1332
|
+
kind = PROJECT_KIND if self.is_kind_supported(PROJECT_KIND) else "Namespace"
|
|
1321
1333
|
self.projects = {p["metadata"]["name"] for p in self.get_all(kind)["items"]}
|
|
1322
1334
|
|
|
1323
1335
|
def __enter__(self) -> OCNative:
|
|
@@ -1367,8 +1379,10 @@ class OCNative(OCCli):
|
|
|
1367
1379
|
|
|
1368
1380
|
@retry(max_attempts=5, exceptions=(ServerTimeoutError))
|
|
1369
1381
|
def get_items(self, kind: str, **kwargs: Any) -> list[dict[str, Any]]:
|
|
1370
|
-
|
|
1371
|
-
obj_client = self._get_obj_client(
|
|
1382
|
+
resource = self.get_api_resource(kind)
|
|
1383
|
+
obj_client = self._get_obj_client(
|
|
1384
|
+
group_version=resource.group_version, kind=resource.kind
|
|
1385
|
+
)
|
|
1372
1386
|
|
|
1373
1387
|
namespace = ""
|
|
1374
1388
|
if "namespace" in kwargs:
|
|
@@ -1420,8 +1434,10 @@ class OCNative(OCCli):
|
|
|
1420
1434
|
name: str | None = None,
|
|
1421
1435
|
allow_not_found: bool = False,
|
|
1422
1436
|
) -> dict[str, Any]:
|
|
1423
|
-
|
|
1424
|
-
obj_client = self._get_obj_client(
|
|
1437
|
+
resource = self.get_api_resource(kind)
|
|
1438
|
+
obj_client = self._get_obj_client(
|
|
1439
|
+
group_version=resource.group_version, kind=resource.kind
|
|
1440
|
+
)
|
|
1425
1441
|
try:
|
|
1426
1442
|
obj = obj_client.get(
|
|
1427
1443
|
name=name,
|
|
@@ -1435,8 +1451,10 @@ class OCNative(OCCli):
|
|
|
1435
1451
|
raise StatusCodeError(f"[{self.server}]: {e}") from None
|
|
1436
1452
|
|
|
1437
1453
|
def get_all(self, kind: str, all_namespaces: bool = False) -> dict[str, Any]:
|
|
1438
|
-
|
|
1439
|
-
obj_client = self._get_obj_client(
|
|
1454
|
+
resource = self.get_api_resource(kind)
|
|
1455
|
+
obj_client = self._get_obj_client(
|
|
1456
|
+
group_version=resource.group_version, kind=resource.kind
|
|
1457
|
+
)
|
|
1440
1458
|
try:
|
|
1441
1459
|
return obj_client.get(_request_timeout=REQUEST_TIMEOUT).to_dict()
|
|
1442
1460
|
except NotFoundError as e:
|
reconcile/utils/oc_filters.py
CHANGED
|
@@ -19,19 +19,19 @@ class Namespace(Protocol):
|
|
|
19
19
|
NS = TypeVar("NS", bound=Namespace)
|
|
20
20
|
|
|
21
21
|
|
|
22
|
-
def filter_namespaces_by_cluster(
|
|
22
|
+
def filter_namespaces_by_cluster[NS: Namespace](
|
|
23
23
|
namespaces: Iterable[NS], cluster_names: Iterable[str]
|
|
24
24
|
) -> list[NS]:
|
|
25
25
|
return [n for n in namespaces if n.cluster.name in cluster_names]
|
|
26
26
|
|
|
27
27
|
|
|
28
|
-
def filter_namespaces_by_name(
|
|
28
|
+
def filter_namespaces_by_name[NS: Namespace](
|
|
29
29
|
namespaces: Iterable[NS], namespace_names: Iterable[str]
|
|
30
30
|
) -> list[NS]:
|
|
31
31
|
return [n for n in namespaces if n.name in namespace_names]
|
|
32
32
|
|
|
33
33
|
|
|
34
|
-
def filter_namespaces_by_cluster_and_namespace(
|
|
34
|
+
def filter_namespaces_by_cluster_and_namespace[NS: Namespace](
|
|
35
35
|
namespaces: Iterable[NS],
|
|
36
36
|
cluster_names: Iterable[str] | None,
|
|
37
37
|
namespace_names: Iterable[str] | None,
|
reconcile/utils/ocm/products.py
CHANGED
|
@@ -47,6 +47,7 @@ SPEC_ATTR_MULTI_AZ = "multi_az"
|
|
|
47
47
|
SPEC_ATTR_HYPERSHIFT = "hypershift"
|
|
48
48
|
SPEC_ATTR_SUBNET_IDS = "subnet_ids"
|
|
49
49
|
SPEC_ATTR_AVAILABILITY_ZONES = "availability_zones"
|
|
50
|
+
SPEC_ATTR_FIPS = "fips"
|
|
50
51
|
|
|
51
52
|
SPEC_ATTR_NETWORK = "network"
|
|
52
53
|
IGNORE_NETWORK_TYPE_ATTR = "type"
|
|
@@ -177,6 +178,7 @@ class OCMProductOsd(OCMProduct):
|
|
|
177
178
|
],
|
|
178
179
|
provision_shard_id=provision_shard_id,
|
|
179
180
|
hypershift=cluster["hypershift"]["enabled"],
|
|
181
|
+
fips=cluster.get("fips") or False,
|
|
180
182
|
)
|
|
181
183
|
|
|
182
184
|
if not cluster["ccs"]["enabled"]:
|
|
@@ -257,6 +259,7 @@ class OCMProductOsd(OCMProduct):
|
|
|
257
259
|
if (duwm := cluster.spec.disable_user_workload_monitoring) is not None
|
|
258
260
|
else True
|
|
259
261
|
),
|
|
262
|
+
"fips": cluster.spec.fips,
|
|
260
263
|
}
|
|
261
264
|
|
|
262
265
|
# Workaround to enable type checks.
|
|
@@ -426,6 +429,7 @@ class OCMProductRosa(OCMProduct):
|
|
|
426
429
|
subnet_ids=cluster["aws"].get("subnet_ids"),
|
|
427
430
|
availability_zones=cluster["nodes"].get("availability_zones"),
|
|
428
431
|
oidc_endpoint_url=oidc_endpoint_url,
|
|
432
|
+
fips=cluster.get("fips") or False,
|
|
429
433
|
)
|
|
430
434
|
|
|
431
435
|
machine_pools = [
|
|
@@ -513,6 +517,7 @@ class OCMProductRosa(OCMProduct):
|
|
|
513
517
|
if (duwm := cluster.spec.disable_user_workload_monitoring) is not None
|
|
514
518
|
else True
|
|
515
519
|
),
|
|
520
|
+
"fips": cluster.spec.fips,
|
|
516
521
|
}
|
|
517
522
|
|
|
518
523
|
provision_shard_id = cluster.spec.provision_shard_id
|
|
@@ -701,6 +706,7 @@ class OCMProductHypershift(OCMProduct):
|
|
|
701
706
|
availability_zones=cluster["nodes"].get("availability_zones"),
|
|
702
707
|
hypershift=cluster["hypershift"]["enabled"],
|
|
703
708
|
oidc_endpoint_url=oidc_endpoint_url,
|
|
709
|
+
fips=cluster.get("fips") or False,
|
|
704
710
|
)
|
|
705
711
|
|
|
706
712
|
network = OCMClusterNetwork(
|
|
@@ -5,7 +5,6 @@ from abc import (
|
|
|
5
5
|
from collections.abc import Iterable
|
|
6
6
|
from dataclasses import dataclass
|
|
7
7
|
from datetime import (
|
|
8
|
-
UTC,
|
|
9
8
|
datetime,
|
|
10
9
|
)
|
|
11
10
|
from enum import Enum
|
|
@@ -16,6 +15,8 @@ from typing import (
|
|
|
16
15
|
|
|
17
16
|
import dateparser
|
|
18
17
|
|
|
18
|
+
from reconcile.utils.datetime_util import utc_now
|
|
19
|
+
|
|
19
20
|
|
|
20
21
|
@dataclass
|
|
21
22
|
class FilterCondition:
|
|
@@ -166,17 +167,13 @@ class DateRangeCondition(FilterCondition):
|
|
|
166
167
|
return date
|
|
167
168
|
parsed = dateparser.parse(
|
|
168
169
|
date,
|
|
169
|
-
settings={"RELATIVE_BASE":
|
|
170
|
+
settings={"RELATIVE_BASE": utc_now()},
|
|
170
171
|
)
|
|
171
172
|
if parsed is None:
|
|
172
173
|
raise InvalidFilterError(f"Invalid relative date: {date}")
|
|
173
174
|
|
|
174
175
|
return parsed
|
|
175
176
|
|
|
176
|
-
@staticmethod
|
|
177
|
-
def now() -> datetime:
|
|
178
|
-
return datetime.now(tz=UTC)
|
|
179
|
-
|
|
180
177
|
|
|
181
178
|
class InvalidFilterError(Exception):
|
|
182
179
|
pass
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
from collections.abc import Generator
|
|
2
|
-
from datetime import
|
|
3
|
-
datetime,
|
|
4
|
-
timedelta,
|
|
5
|
-
)
|
|
2
|
+
from datetime import timedelta
|
|
6
3
|
|
|
4
|
+
from reconcile.utils.datetime_util import utc_now
|
|
7
5
|
from reconcile.utils.ocm.base import (
|
|
8
6
|
OCMClusterServiceLog,
|
|
9
7
|
OCMClusterServiceLogCreateModel,
|
|
@@ -47,7 +45,7 @@ def create_service_log(
|
|
|
47
45
|
.eq("severity", service_log.severity.value)
|
|
48
46
|
.eq("summary", service_log.summary)
|
|
49
47
|
.eq("description", service_log.description)
|
|
50
|
-
.after("created_at",
|
|
48
|
+
.after("created_at", utc_now() - dedup_interval),
|
|
51
49
|
),
|
|
52
50
|
None,
|
|
53
51
|
)
|
|
@@ -4,9 +4,8 @@ from __future__ import annotations
|
|
|
4
4
|
import base64
|
|
5
5
|
import contextlib
|
|
6
6
|
import copy
|
|
7
|
-
import datetime
|
|
8
7
|
import hashlib
|
|
9
|
-
import
|
|
8
|
+
import logging
|
|
10
9
|
import re
|
|
11
10
|
from threading import Lock
|
|
12
11
|
from typing import TYPE_CHECKING, Any
|
|
@@ -15,6 +14,8 @@ import semver
|
|
|
15
14
|
from pydantic import BaseModel
|
|
16
15
|
|
|
17
16
|
from reconcile.external_resources.meta import SECRET_UPDATED_AT
|
|
17
|
+
from reconcile.utils.datetime_util import to_utc_seconds_iso_format, utc_now
|
|
18
|
+
from reconcile.utils.json import json_dumps
|
|
18
19
|
from reconcile.utils.metrics import GaugeMetric
|
|
19
20
|
|
|
20
21
|
if TYPE_CHECKING:
|
|
@@ -368,8 +369,8 @@ class OpenshiftResource:
|
|
|
368
369
|
annotations[QONTRACT_ANNOTATION_INTEGRATION] = self.integration
|
|
369
370
|
annotations[QONTRACT_ANNOTATION_INTEGRATION_VERSION] = self.integration_version
|
|
370
371
|
annotations[QONTRACT_ANNOTATION_SHA256SUM] = sha256sum
|
|
371
|
-
now =
|
|
372
|
-
annotations[QONTRACT_ANNOTATION_UPDATE] = now
|
|
372
|
+
now = utc_now()
|
|
373
|
+
annotations[QONTRACT_ANNOTATION_UPDATE] = to_utc_seconds_iso_format(now)
|
|
373
374
|
if self.caller_name:
|
|
374
375
|
annotations[QONTRACT_ANNOTATION_CALLER_NAME] = self.caller_name
|
|
375
376
|
|
|
@@ -530,7 +531,7 @@ class OpenshiftResource:
|
|
|
530
531
|
|
|
531
532
|
@staticmethod
|
|
532
533
|
def serialize(body: dict[str, Any]) -> str:
|
|
533
|
-
return
|
|
534
|
+
return json_dumps(body)
|
|
534
535
|
|
|
535
536
|
@staticmethod
|
|
536
537
|
def calculate_sha256sum(body: str) -> str:
|
|
@@ -601,6 +602,10 @@ class ResourceInventory:
|
|
|
601
602
|
resource: OpenshiftResource,
|
|
602
603
|
privileged: bool = False,
|
|
603
604
|
) -> None:
|
|
605
|
+
if cluster not in self._clusters:
|
|
606
|
+
logging.error(f"Cluster {cluster} not initialized in ResourceInventory")
|
|
607
|
+
return
|
|
608
|
+
|
|
604
609
|
if resource.kind_and_group in self._clusters[cluster][namespace]:
|
|
605
610
|
kind = resource.kind_and_group
|
|
606
611
|
else:
|
reconcile/utils/output.py
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import json
|
|
2
1
|
import re
|
|
3
2
|
from collections.abc import Iterable, Mapping
|
|
4
3
|
|
|
5
4
|
import yaml
|
|
6
5
|
from tabulate import tabulate
|
|
7
6
|
|
|
7
|
+
from reconcile.utils.json import json_dumps
|
|
8
|
+
|
|
8
9
|
|
|
9
10
|
def print_output(
|
|
10
11
|
options: Mapping[str, str | bool],
|
|
@@ -30,7 +31,7 @@ def print_output(
|
|
|
30
31
|
)
|
|
31
32
|
print(formatted_content)
|
|
32
33
|
elif output == "json":
|
|
33
|
-
formatted_content =
|
|
34
|
+
formatted_content = json_dumps(content)
|
|
34
35
|
print(formatted_content)
|
|
35
36
|
elif output == "yaml":
|
|
36
37
|
formatted_content = yaml.dump(content)
|
reconcile/utils/pagerduty_api.py
CHANGED
|
@@ -3,8 +3,7 @@ from collections.abc import (
|
|
|
3
3
|
Callable,
|
|
4
4
|
Iterable,
|
|
5
5
|
)
|
|
6
|
-
from datetime import datetime
|
|
7
|
-
from datetime import timedelta
|
|
6
|
+
from datetime import datetime, timedelta
|
|
8
7
|
from typing import (
|
|
9
8
|
Protocol,
|
|
10
9
|
)
|
|
@@ -14,6 +13,7 @@ import requests
|
|
|
14
13
|
from pydantic import BaseModel
|
|
15
14
|
from sretoolbox.utils import retry
|
|
16
15
|
|
|
16
|
+
from reconcile.utils.datetime_util import utc_now
|
|
17
17
|
from reconcile.utils.secret_reader import (
|
|
18
18
|
HasSecret,
|
|
19
19
|
SecretReader,
|
|
@@ -80,7 +80,7 @@ class PagerDutyApi:
|
|
|
80
80
|
def get_pagerduty_users(
|
|
81
81
|
self, resource_type: str, resource_id: str
|
|
82
82
|
) -> list[pypd.User]:
|
|
83
|
-
now =
|
|
83
|
+
now = utc_now()
|
|
84
84
|
|
|
85
85
|
try:
|
|
86
86
|
if resource_type == "schedule":
|
|
@@ -103,7 +103,7 @@ class PagerDutyApi:
|
|
|
103
103
|
self.users.append(user)
|
|
104
104
|
return user.email.split("@")[0]
|
|
105
105
|
|
|
106
|
-
def get_schedule_users(self, schedule_id: str, now:
|
|
106
|
+
def get_schedule_users(self, schedule_id: str, now: datetime) -> list[pypd.User]:
|
|
107
107
|
until = now + timedelta(seconds=60)
|
|
108
108
|
s = pypd.Schedule.fetch(id=schedule_id, since=now, until=until, time_zone="UTC")
|
|
109
109
|
entries = s["final_schedule"]["rendered_schedule_entries"]
|
|
@@ -115,7 +115,7 @@ class PagerDutyApi:
|
|
|
115
115
|
]
|
|
116
116
|
|
|
117
117
|
def get_escalation_policy_users(
|
|
118
|
-
self, escalation_policy_id: str, now:
|
|
118
|
+
self, escalation_policy_id: str, now: datetime
|
|
119
119
|
) -> list[pypd.User]:
|
|
120
120
|
ep = pypd.EscalationPolicy.fetch(
|
|
121
121
|
id=escalation_policy_id, since=now, until=now, time_zone="UTC"
|
|
@@ -7,7 +7,6 @@ from dataclasses import dataclass
|
|
|
7
7
|
from types import ModuleType
|
|
8
8
|
from typing import (
|
|
9
9
|
Any,
|
|
10
|
-
Generic,
|
|
11
10
|
Optional,
|
|
12
11
|
TypeVar,
|
|
13
12
|
)
|
|
@@ -144,7 +143,7 @@ IntegrationClassTypeVar = TypeVar(
|
|
|
144
143
|
)
|
|
145
144
|
|
|
146
145
|
|
|
147
|
-
class QontractReconcileIntegration
|
|
146
|
+
class QontractReconcileIntegration[RunParamsTypeVar: RunParams](ABC):
|
|
148
147
|
"""
|
|
149
148
|
The base class for all integrations. It defines the basic interface to interact
|
|
150
149
|
with an integration and offers hook methods that allow the integration to opt
|
|
@@ -156,7 +156,7 @@ def run_integration_cfg(run_cfg: IntegrationRunConfiguration) -> None:
|
|
|
156
156
|
_integration_wet_run(run_cfg.integration)
|
|
157
157
|
|
|
158
158
|
|
|
159
|
-
def _integration_wet_run(
|
|
159
|
+
def _integration_wet_run[RunParamsTypeVar: RunParams](
|
|
160
160
|
integration: QontractReconcileIntegration[RunParamsTypeVar],
|
|
161
161
|
) -> None:
|
|
162
162
|
"""
|
|
@@ -165,7 +165,7 @@ def _integration_wet_run(
|
|
|
165
165
|
integration.run(False)
|
|
166
166
|
|
|
167
167
|
|
|
168
|
-
def _integration_dry_run(
|
|
168
|
+
def _integration_dry_run[RunParamsTypeVar: RunParams](
|
|
169
169
|
integration: QontractReconcileIntegration[RunParamsTypeVar],
|
|
170
170
|
desired_state_diff: DesiredStateDiff | None,
|
|
171
171
|
) -> None:
|
|
@@ -16,6 +16,7 @@ from reconcile.gql_definitions.fragments.saas_slo_document import (
|
|
|
16
16
|
SLODocument,
|
|
17
17
|
)
|
|
18
18
|
from reconcile.utils.jenkins_api import JobBuildState
|
|
19
|
+
from reconcile.utils.json import json_dumps
|
|
19
20
|
from reconcile.utils.oc_connection_parameters import Cluster
|
|
20
21
|
from reconcile.utils.saasherder.interfaces import (
|
|
21
22
|
HasParameters,
|
|
@@ -422,7 +423,7 @@ class TargetSpec:
|
|
|
422
423
|
elif v is False:
|
|
423
424
|
parameters[k] = "false"
|
|
424
425
|
elif any(isinstance(v, t) for t in [dict, list, tuple]):
|
|
425
|
-
parameters[k] =
|
|
426
|
+
parameters[k] = json_dumps(v)
|
|
426
427
|
return parameters
|
|
427
428
|
|
|
428
429
|
def _collect_secret_parameters(
|