qontract-reconcile 0.10.1rc967__py3-none-any.whl → 0.10.1rc968__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.1rc967
3
+ Version: 0.10.1rc968
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
@@ -61,7 +61,7 @@ reconcile/ocm_groups.py,sha256=-rTPMewkdyo1De6gs4u-294p3z34oUbGfuNi8ov56Sk,3424
61
61
  reconcile/ocm_machine_pools.py,sha256=poGfITOCJEMwYAJpiuL8SytgTcBmGIKEZPgNGld80TY,16563
62
62
  reconcile/ocm_update_recommended_version.py,sha256=IYkfLXIprOW1jguZeELcGP1iBPuj-b53R-FTqKulMl8,4204
63
63
  reconcile/ocm_upgrade_scheduler_org_updater.py,sha256=aLgyInt9oIWAg0XtCiwJRUSwdPx3masKV8kHzkyEEOQ,4282
64
- reconcile/openshift_base.py,sha256=rbM6B9A4oRsklFHl0Me8XnMVW8qUME66pfdIyqM_gEg,49709
64
+ reconcile/openshift_base.py,sha256=IS5J8nccqRELRCheuz2wiLMHpaDypA9y3aKTMAjmMuE,52389
65
65
  reconcile/openshift_cluster_bots.py,sha256=eRPYZqWMKFNxLlSN0QG97V5t1iIESQ0BbGaiaQP5VB0,10940
66
66
  reconcile/openshift_clusterrolebindings.py,sha256=QfSy1Ik8eEY5XObc1Q4xyhqyErZenJmbPv_u9wcDNNo,5864
67
67
  reconcile/openshift_groups.py,sha256=sK2wLWwNupztbfyFPl32VH42s_s8Mu3g-URdlisnwJc,9382
@@ -83,7 +83,7 @@ reconcile/openshift_saas_deploy_trigger_configs.py,sha256=eUejMGWuaQabZTLuvPLLvR
83
83
  reconcile/openshift_saas_deploy_trigger_images.py,sha256=iUsiBGJf-CyFw7tSLWo59rXmSvsVnN6TTaAObbsVpNg,936
84
84
  reconcile/openshift_saas_deploy_trigger_moving_commits.py,sha256=fpanSH-EGH15C9me--0VSpcpaw9BY4RTb8_mPtsSZGc,942
85
85
  reconcile/openshift_saas_deploy_trigger_upstream_jobs.py,sha256=0CjfeVQE0QrRrOVuTxkXvBUdKNtYLYuX4mZRB48PQ9g,940
86
- reconcile/openshift_serviceaccount_tokens.py,sha256=kc8o6tYDpzYTuX7BGIuZjGR1rC4tAB1tfN-ijCS0AgA,6005
86
+ reconcile/openshift_serviceaccount_tokens.py,sha256=9zNSMEHwcaGWy4H-OLkk0zLw80CYTBnLVEZKB_xq6Ew,7331
87
87
  reconcile/openshift_tekton_resources.py,sha256=jKH5nw84aeYkgikxjQnjSOSF3m2kk3lp2BaPd3FfTew,16220
88
88
  reconcile/openshift_upgrade_watcher.py,sha256=9IB321hlRZZhzdaR9G3zoWAhVv0-KzNiEqx73p3-wmk,6539
89
89
  reconcile/openshift_users.py,sha256=63mar-swgidz8f10TCPJcofbMN9FETq-HuVFpi8dUL4,5293
@@ -94,7 +94,7 @@ reconcile/quay_mirror.py,sha256=sDLJUTWp8o9Swr2stQJl6PNR_5j-aUYyTtlKv7yznwQ,1482
94
94
  reconcile/quay_mirror_org.py,sha256=utrJpJaKCs7U6WX6DODdfCeB0EmX-lUC8Y5fkmpgFSs,10764
95
95
  reconcile/quay_permissions.py,sha256=9KOutS1w4RFQqkvMSy54VtsKNx56-phzP6yI_rEW-B8,4244
96
96
  reconcile/quay_repos.py,sha256=cuEYG0HUe0ut5yvLdEwOF5-CmccpXQHRb_wDazvDrvQ,6895
97
- reconcile/queries.py,sha256=2DL0yzk44m-AfgvkByTeEPpt4xQcXsCOz_Wv0ZUpfbA,51295
97
+ reconcile/queries.py,sha256=uUc_5ni7NEUCV4UwG96xxwp_lg-8_MaLRx69i9H--wQ,50164
98
98
  reconcile/query_validator.py,sha256=BAjGrU8_VhzTOv5k0-uz0hY9ziZyconv8VAhgre1Auc,1497
99
99
  reconcile/requests_sender.py,sha256=914iluuF4UVgG3VyxxtnHOu4yf6YKS2fIy6PViSsFTQ,3875
100
100
  reconcile/resource_scraper.py,sha256=znXCHrU7YwPfKuxGBiUrV7T1tYtn4vlz9qmZlfy6Flg,2307
@@ -302,6 +302,7 @@ reconcile/gql_definitions/fragments/resource_limits_requirements.py,sha256=ucskQ
302
302
  reconcile/gql_definitions/fragments/resource_requests_requirements.py,sha256=TFKO4YALFPanSvZvIJFz0dCioBU7i73Q6hkDtGMvs9I,736
303
303
  reconcile/gql_definitions/fragments/resource_values.py,sha256=-N2lNRhWp8PgocmIeX3U9f3l90Q97N2lXoq1pXdb_LE,742
304
304
  reconcile/gql_definitions/fragments/saas_target_namespace.py,sha256=8kMXeD7u2bmdjn10zHmMJ80ScOhUp6KqSfWfjWZW-40,4001
305
+ reconcile/gql_definitions/fragments/serviceaccount_token.py,sha256=2pG4rxAjvT-YsFBnm4zl301i7DCYznp99HOEGA-216I,1117
305
306
  reconcile/gql_definitions/fragments/terraform_state.py,sha256=S5QuTR9YlvUObiU7hevS9ybxZEssWoRGqCR9YtGwePs,1024
306
307
  reconcile/gql_definitions/fragments/upgrade_policy.py,sha256=cVza8zfra1E3yBsHiS-hKbys17fvv572GFnKshJjluE,1246
307
308
  reconcile/gql_definitions/fragments/user.py,sha256=TZyFEs1fBg5PkvWdyCxFDZ_3aRhcQzusfhObXFiOU_0,1025
@@ -341,6 +342,8 @@ reconcile/gql_definitions/openshift_cluster_bots/clusters.py,sha256=QshhCQeFRu_o
341
342
  reconcile/gql_definitions/openshift_groups/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
342
343
  reconcile/gql_definitions/openshift_groups/managed_groups.py,sha256=mBWZX9xxeW3eB1ylnAI5x_7UBacRqJf_H6um-fB_nKc,2013
343
344
  reconcile/gql_definitions/openshift_groups/managed_roles.py,sha256=rsHMgDWwnh0RRJpS_-TxD22KQPC6_7rtRfauTOtxH5I,2627
345
+ reconcile/gql_definitions/openshift_serviceaccount_tokens/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
346
+ reconcile/gql_definitions/openshift_serviceaccount_tokens/tokens.py,sha256=FeraeQThHl2UbhTuCPDwm3ltczF_jfZn5E3Squ8-znw,3313
344
347
  reconcile/gql_definitions/quay_membership/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
345
348
  reconcile/gql_definitions/quay_membership/quay_membership.py,sha256=H2xHvdNr3K0QzB2dituwStUIWCqePt35dkgeUZycECM,2824
346
349
  reconcile/gql_definitions/rhidp/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -518,7 +521,7 @@ reconcile/test/test_ocm_clusters_manifest_updates.py,sha256=jFRVfc5jby1kI2x_gT6w
518
521
  reconcile/test/test_ocm_machine_pools.py,sha256=_1tG-nF8edU29ONDUGXtR3J2-E8FWv-PI0Y9gb8HQfY,29697
519
522
  reconcile/test/test_ocm_update_recommended_version.py,sha256=iA4BVirTGVXlwcOyeR52IuNO81X_8NR6ZNd7ZFE7igs,4328
520
523
  reconcile/test/test_ocm_upgrade_scheduler_org_updater.py,sha256=V91g2XQMa2nvKwkLVWkiPwNL6pMQE16s4jO0oXJ6wdk,4330
521
- reconcile/test/test_openshift_base.py,sha256=gaz_C0gxoyGtn_Jw43z0JZO9-ImFuw16oGWtQ_wmXGU,33865
524
+ reconcile/test/test_openshift_base.py,sha256=SX8URKF8Kn21nA-qcmICSUaZKHuc5YTgPzfcb2rHvk8,36385
522
525
  reconcile/test/test_openshift_cluster_bots.py,sha256=sSGLgxnXO82xcfTFVNJzsrDuNfObwAR_-AkDe4B_4WE,7983
523
526
  reconcile/test/test_openshift_namespace_labels.py,sha256=i4S5QJFxMRjLkwi3iO6A-uhjgZ1QZb4jYXwB696m82Y,12070
524
527
  reconcile/test/test_openshift_namespaces.py,sha256=HmRnCE5EnFt3MYceVEFHmk8wWRtCrxu2AFGFkY9pdyA,9214
@@ -527,6 +530,7 @@ reconcile/test/test_openshift_resources_base.py,sha256=LtlR9x3o7KkSEw0JN0fZhinFe
527
530
  reconcile/test/test_openshift_saas_deploy.py,sha256=3QXMrN9dXIiR0JktVDNQ7yJSexMTjZLb1tbRrB3-7uU,5991
528
531
  reconcile/test/test_openshift_saas_deploy_change_tester.py,sha256=1yVe54Hx9YdVjn6qdnKge5Sa_s732c-8uZqCnuT1gGI,12871
529
532
  reconcile/test/test_openshift_saas_deploy_trigger_cleaner.py,sha256=UQx1iJ21rsMa2whG-rtUIuTXbUzc0Ngr7jRLKXZCCCI,2838
533
+ reconcile/test/test_openshift_serviceaccount_tokens.py,sha256=btaI8Au5XdZXXHtQfFPA8bGSTrXReC8ywJKUjaIQc-A,7341
530
534
  reconcile/test/test_openshift_tekton_resources.py,sha256=RtRWsdm51S13OSkENC9nY_rOH0QELSCaO5tjF0XqIDI,11222
531
535
  reconcile/test/test_openshift_upgrade_watcher.py,sha256=0GDQ_YFHIX8DbkbDYSuLv9uZeeg4NwP1vlOqvSaZvN4,7183
532
536
  reconcile/test/test_prometheus_rules_tester.py,sha256=cgVkPM3KcAw69bOkJ6iR2Lfog_WgblyoqVRtXv4ly7o,5685
@@ -847,8 +851,8 @@ tools/test/test_qontract_cli.py,sha256=_D61RFGAN5x44CY1tYbouhlGXXABwYfxKSWSQx3Jr
847
851
  tools/test/test_saas_promotion_state.py,sha256=dy4kkSSAQ7bC0Xp2CociETGN-2aABEfL6FU5D9Jl00Y,6056
848
852
  tools/test/test_sd_app_sre_alert_report.py,sha256=v363r9zM7__0kR5K6mvJoGFcM9BvE33fWAayrqkpojA,2116
849
853
  tools/test/test_sre_checkpoints.py,sha256=SKqPPTl9ua0RFdSSofnoQX-JZE6dFLO3LRhfQzqtfh8,2607
850
- qontract_reconcile-0.10.1rc967.dist-info/METADATA,sha256=U1KHlHKS0EVLd-fnOLrjHk2OZaH3Y5t1BZW8ci9Np9s,2262
851
- qontract_reconcile-0.10.1rc967.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
852
- qontract_reconcile-0.10.1rc967.dist-info/entry_points.txt,sha256=GKQqCl2j2X1BJQ69een6rHcR26PmnxnONLNOQB-nRjY,491
853
- qontract_reconcile-0.10.1rc967.dist-info/top_level.txt,sha256=l5ISPoXzt0SdR4jVdkfa7RPSKNc8zAHYWAnR-Dw8Ey8,24
854
- qontract_reconcile-0.10.1rc967.dist-info/RECORD,,
854
+ qontract_reconcile-0.10.1rc968.dist-info/METADATA,sha256=9_cctR46UDeQ8KvizoJx12uh1u7_W-xYEprAgQpq_DQ,2262
855
+ qontract_reconcile-0.10.1rc968.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
856
+ qontract_reconcile-0.10.1rc968.dist-info/entry_points.txt,sha256=GKQqCl2j2X1BJQ69een6rHcR26PmnxnONLNOQB-nRjY,491
857
+ qontract_reconcile-0.10.1rc968.dist-info/top_level.txt,sha256=l5ISPoXzt0SdR4jVdkfa7RPSKNc8zAHYWAnR-Dw8Ey8,24
858
+ qontract_reconcile-0.10.1rc968.dist-info/RECORD,,
@@ -0,0 +1,38 @@
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.oc_connection_cluster import OcConnectionCluster
21
+
22
+
23
+ class ConfiguredBaseModel(BaseModel):
24
+ class Config:
25
+ smart_union=True
26
+ extra=Extra.forbid
27
+
28
+
29
+ class NamespaceV1(ConfiguredBaseModel):
30
+ name: str = Field(..., alias="name")
31
+ delete: Optional[bool] = Field(..., alias="delete")
32
+ cluster: OcConnectionCluster = Field(..., alias="cluster")
33
+
34
+
35
+ class ServiceAccountToken(ConfiguredBaseModel):
36
+ name: Optional[str] = Field(..., alias="name")
37
+ service_account_name: str = Field(..., alias="serviceAccountName")
38
+ namespace: NamespaceV1 = Field(..., alias="namespace")
@@ -0,0 +1,132 @@
1
+ """
2
+ Generated by qenerate plugin=pydantic_v1. DO NOT MODIFY MANUALLY!
3
+ """
4
+ from collections.abc import Callable # noqa: F401 # pylint: disable=W0611
5
+ from datetime import datetime # noqa: F401 # pylint: disable=W0611
6
+ from enum import Enum # noqa: F401 # pylint: disable=W0611
7
+ from typing import ( # noqa: F401 # pylint: disable=W0611
8
+ Any,
9
+ Optional,
10
+ Union,
11
+ )
12
+
13
+ from pydantic import ( # noqa: F401 # pylint: disable=W0611
14
+ BaseModel,
15
+ Extra,
16
+ Field,
17
+ Json,
18
+ )
19
+
20
+ from reconcile.gql_definitions.fragments.oc_connection_cluster import OcConnectionCluster
21
+ from reconcile.gql_definitions.fragments.serviceaccount_token import ServiceAccountToken
22
+
23
+
24
+ DEFINITION = """
25
+ fragment CommonJumphostFields on ClusterJumpHost_v1 {
26
+ hostname
27
+ knownHosts
28
+ user
29
+ port
30
+ remotePort
31
+ identity {
32
+ ... VaultSecret
33
+ }
34
+ }
35
+
36
+ fragment OcConnectionCluster on Cluster_v1 {
37
+ name
38
+ serverUrl
39
+ internal
40
+ insecureSkipTLSVerify
41
+ jumpHost {
42
+ ...CommonJumphostFields
43
+ }
44
+ automationToken {
45
+ ...VaultSecret
46
+ }
47
+ clusterAdminAutomationToken {
48
+ ...VaultSecret
49
+ }
50
+ disable {
51
+ integrations
52
+ }
53
+ }
54
+
55
+ fragment ServiceAccountToken on ServiceAccountTokenSpec_v1 {
56
+ name
57
+ serviceAccountName
58
+ namespace {
59
+ name
60
+ delete
61
+ cluster {
62
+ ...OcConnectionCluster
63
+ }
64
+ }
65
+ }
66
+
67
+ fragment VaultSecret on VaultSecret_v1 {
68
+ path
69
+ field
70
+ version
71
+ format
72
+ }
73
+
74
+ query ServiceAccountTokens {
75
+ namespaces: namespaces_v1 {
76
+ name
77
+ delete
78
+ cluster {
79
+ ...OcConnectionCluster
80
+ }
81
+ sharedResources {
82
+ openshiftServiceAccountTokens {
83
+ ...ServiceAccountToken
84
+ }
85
+ }
86
+ openshiftServiceAccountTokens {
87
+ ...ServiceAccountToken
88
+ }
89
+ }
90
+ }
91
+ """
92
+
93
+
94
+ class ConfiguredBaseModel(BaseModel):
95
+ class Config:
96
+ smart_union=True
97
+ extra=Extra.forbid
98
+
99
+
100
+ class SharedResourcesV1(ConfiguredBaseModel):
101
+ openshift_service_account_tokens: Optional[list[ServiceAccountToken]] = Field(..., alias="openshiftServiceAccountTokens")
102
+
103
+
104
+ class NamespaceV1(ConfiguredBaseModel):
105
+ name: str = Field(..., alias="name")
106
+ delete: Optional[bool] = Field(..., alias="delete")
107
+ cluster: OcConnectionCluster = Field(..., alias="cluster")
108
+ shared_resources: Optional[list[SharedResourcesV1]] = Field(..., alias="sharedResources")
109
+ openshift_service_account_tokens: Optional[list[ServiceAccountToken]] = Field(..., alias="openshiftServiceAccountTokens")
110
+
111
+
112
+ class ServiceAccountTokensQueryData(ConfiguredBaseModel):
113
+ namespaces: Optional[list[NamespaceV1]] = Field(..., alias="namespaces")
114
+
115
+
116
+ def query(query_func: Callable, **kwargs: Any) -> ServiceAccountTokensQueryData:
117
+ """
118
+ This is a convenience function which queries and parses the data into
119
+ concrete types. It should be compatible with most GQL clients.
120
+ You do not have to use it to consume the generated data classes.
121
+ Alternatively, you can also mime and alternate the behavior
122
+ of this function in the caller.
123
+
124
+ Parameters:
125
+ query_func (Callable): Function which queries your GQL Server
126
+ kwargs: optional arguments that will be passed to the query function
127
+
128
+ Returns:
129
+ ServiceAccountTokensQueryData: queried data parsed into generated classes
130
+ """
131
+ raw_data: dict[Any, Any] = query_func(DEFINITION, **kwargs)
132
+ return ServiceAccountTokensQueryData(**raw_data)
@@ -1306,6 +1306,82 @@ def aggregate_shared_resources(namespace_info, shared_resources_type):
1306
1306
  namespace_info[shared_resources_type] = namespace_type_resources
1307
1307
 
1308
1308
 
1309
+ @runtime_checkable
1310
+ class HasOpenShiftResources(Protocol):
1311
+ openshift_resources: list | None
1312
+
1313
+
1314
+ @runtime_checkable
1315
+ class HasOpenshiftServiceAccountTokens(Protocol):
1316
+ openshift_service_account_tokens: list | None
1317
+
1318
+
1319
+ @runtime_checkable
1320
+ class HasSharedResourcesOpenShiftResources(Protocol):
1321
+ @property
1322
+ def shared_resources(self) -> Sequence[HasOpenShiftResources] | None:
1323
+ pass
1324
+
1325
+
1326
+ @runtime_checkable
1327
+ class HasSharedResourcesOpenshiftServiceAccountTokens(Protocol):
1328
+ @property
1329
+ def shared_resources(self) -> Sequence[HasOpenshiftServiceAccountTokens] | None:
1330
+ pass
1331
+
1332
+
1333
+ @runtime_checkable
1334
+ class HasOpenshiftServiceAccountTokensAndSharedResources(
1335
+ HasOpenshiftServiceAccountTokens,
1336
+ HasSharedResourcesOpenshiftServiceAccountTokens,
1337
+ Protocol,
1338
+ ): ...
1339
+
1340
+
1341
+ @runtime_checkable
1342
+ class HasOpenShiftResourcesAndSharedResources(
1343
+ HasOpenShiftResources, HasSharedResourcesOpenShiftResources, Protocol
1344
+ ): ...
1345
+
1346
+
1347
+ def aggregate_shared_resources_typed(
1348
+ namespace: HasOpenshiftServiceAccountTokensAndSharedResources
1349
+ | HasOpenShiftResourcesAndSharedResources,
1350
+ ) -> None:
1351
+ """This function aggregates the shared resources to the appropriate namespace section.
1352
+
1353
+ Attention: It updates the namespace object in place and isn't indempotent!
1354
+ """
1355
+ if not namespace.shared_resources:
1356
+ return
1357
+
1358
+ match namespace:
1359
+ case HasOpenshiftServiceAccountTokensAndSharedResources():
1360
+ shared_type_resources_items = []
1361
+ for ost_shared_resources_item in namespace.shared_resources:
1362
+ if shared_type_resources := getattr(
1363
+ ost_shared_resources_item, "openshift_service_account_tokens"
1364
+ ):
1365
+ shared_type_resources_items.extend(shared_type_resources)
1366
+ if namespace.openshift_service_account_tokens:
1367
+ namespace.openshift_service_account_tokens.extend(
1368
+ shared_type_resources_items
1369
+ )
1370
+ else:
1371
+ namespace.openshift_service_account_tokens = shared_type_resources_items
1372
+ case HasOpenShiftResourcesAndSharedResources():
1373
+ shared_type_resources_items = []
1374
+ for or_shared_resources_item in namespace.shared_resources:
1375
+ if shared_type_resources := getattr(
1376
+ or_shared_resources_item, "openshift_resources"
1377
+ ):
1378
+ shared_type_resources_items.extend(shared_type_resources)
1379
+ if namespace.openshift_resources:
1380
+ namespace.openshift_resources.extend(shared_type_resources_items)
1381
+ else:
1382
+ namespace.openshift_resources = shared_type_resources_items
1383
+
1384
+
1309
1385
  def determine_user_keys_for_access(
1310
1386
  cluster_name: str,
1311
1387
  auth_list: Sequence[dict[str, str] | HasService],
@@ -1,9 +1,15 @@
1
1
  import logging
2
2
  import sys
3
+ from collections.abc import Callable, Iterable
3
4
 
4
5
  import reconcile.openshift_base as ob
5
- from reconcile import queries
6
+ from reconcile.gql_definitions.openshift_serviceaccount_tokens.tokens import NamespaceV1
7
+ from reconcile.gql_definitions.openshift_serviceaccount_tokens.tokens import (
8
+ query as serviceaccount_tokens_query,
9
+ )
10
+ from reconcile.utils import gql
6
11
  from reconcile.utils.defer import defer
12
+ from reconcile.utils.disabled_integrations import integration_is_enabled
7
13
  from reconcile.utils.oc import OC_Map
8
14
  from reconcile.utils.openshift_resource import OpenshiftResource as OR
9
15
  from reconcile.utils.openshift_resource import ResourceInventory
@@ -14,7 +20,7 @@ QONTRACT_INTEGRATION = "openshift-serviceaccount-tokens"
14
20
  QONTRACT_INTEGRATION_VERSION = make_semver(0, 1, 0)
15
21
 
16
22
 
17
- def construct_sa_token_oc_resource(name, sa_token):
23
+ def construct_sa_token_oc_resource(name: str, sa_token: str) -> OR:
18
24
  body = {
19
25
  "apiVersion": "v1",
20
26
  "kind": "Secret",
@@ -47,104 +53,133 @@ def get_tokens_for_service_account(
47
53
  return result
48
54
 
49
55
 
50
- def fetch_desired_state(namespaces: list[dict], ri: ResourceInventory, oc_map: OC_Map):
51
- for namespace_info in namespaces:
52
- if not namespace_info.get("openshiftServiceAccountTokens"):
56
+ def fetch_desired_state(
57
+ namespaces: list[NamespaceV1], ri: ResourceInventory, oc_map: OC_Map
58
+ ) -> None:
59
+ for namespace in namespaces:
60
+ if not namespace.openshift_service_account_tokens:
53
61
  continue
54
- namespace_name = namespace_info["name"]
55
- cluster_name = namespace_info["cluster"]["name"]
56
- oc = oc_map.get(cluster_name)
57
- if not oc:
62
+
63
+ if not (oc := oc_map.get(namespace.cluster.name)):
58
64
  logging.log(level=oc.log_level, msg=oc.message)
59
65
  continue
60
- for sat in namespace_info["openshiftServiceAccountTokens"]:
61
- sa_name = sat["serviceAccountName"]
62
- sa_namespace_info = sat["namespace"]
63
- sa_namespace_name = sa_namespace_info["name"]
64
- sa_cluster_name = sa_namespace_info["cluster"]["name"]
65
- oc = oc_map.get(sa_cluster_name)
66
+
67
+ for sat in namespace.openshift_service_account_tokens:
68
+ oc = oc_map.get(sat.namespace.cluster.name)
66
69
  if not oc:
67
70
  if oc.log_level >= logging.ERROR:
68
71
  ri.register_error()
69
72
  logging.log(level=oc.log_level, msg=oc.message)
70
73
  continue
71
74
 
72
- all_tokens = oc.get_items(kind="Secret", namespace=sa_namespace_name)
73
- oc_resource_name = (
74
- sat.get("name") or f"{sa_cluster_name}-{sa_namespace_name}-{sa_name}"
75
+ namespace_secrets = oc.get_items(
76
+ kind="Secret", namespace=sat.namespace.name
75
77
  )
76
78
  try:
77
- sa_token_list = get_tokens_for_service_account(sa_name, all_tokens)
78
- sa_token_list.sort(key=lambda t: t["metadata"]["name"])
79
- sa_token = sa_token_list[0]["data"]["token"]
79
+ sa_tokens = get_tokens_for_service_account(
80
+ sat.service_account_name, namespace_secrets
81
+ )
82
+ sa_tokens.sort(key=lambda t: t["metadata"]["name"])
83
+ # take the first token found
84
+ sa_token = sa_tokens[0]["data"]["token"]
80
85
  except KeyError:
81
- logging.log(
82
- level=logging.ERROR,
83
- msg=f"[{sa_cluster_name}/{sa_namespace_name}] Token not found for service account: {sa_name}",
86
+ logging.error(
87
+ f"[{sat.namespace.cluster.name}/{sat.namespace.name}] Token not found for service account: {sat.service_account_name}"
84
88
  )
85
89
  raise
86
90
  except IndexError:
87
- logging.log(
88
- level=logging.ERROR,
89
- msg=f"[{sa_cluster_name}/{sa_namespace_name}] 0 Secret found for service account: {sa_name}",
91
+ logging.error(
92
+ f"[{sat.namespace.cluster.name}/{sat.namespace.name}] 0 Secret found for service account: {sat.service_account_name}"
90
93
  )
91
94
  raise
92
95
 
93
- oc_resource = construct_sa_token_oc_resource(oc_resource_name, sa_token)
94
- ri.add_desired(
95
- cluster_name, namespace_name, "Secret", oc_resource_name, oc_resource
96
+ oc_resource = construct_sa_token_oc_resource(
97
+ name=(
98
+ sat.name
99
+ or f"{sat.namespace.cluster.name}-{sat.namespace.name}-{sat.service_account_name}"
100
+ ),
101
+ sa_token=sa_token,
102
+ )
103
+ ri.add_desired_resource(
104
+ namespace.cluster.name,
105
+ namespace.name,
106
+ oc_resource,
96
107
  )
97
108
 
98
109
 
99
- def write_outputs_to_vault(vault_path, ri):
110
+ def write_outputs_to_vault(
111
+ vault_client: VaultClient, vault_path: str, ri: ResourceInventory
112
+ ) -> None:
100
113
  integration_name = QONTRACT_INTEGRATION.replace("_", "-")
101
- vault_client = VaultClient()
102
114
  for cluster, namespace, _, data in ri:
103
115
  for name, d_item in data["desired"].items():
104
116
  body_data = d_item.body["data"]
105
117
  # write secret to per-namespace location
106
118
  secret_path = (
107
- f"{vault_path}/{integration_name}/" + f"{cluster}/{namespace}/{name}"
119
+ f"{vault_path}/{integration_name}/{cluster}/{namespace}/{name}"
108
120
  )
109
121
  secret = {"path": secret_path, "data": body_data}
110
- vault_client.write(secret)
122
+ vault_client.write(secret) # type: ignore
111
123
  # write secret to shared-resources location
112
- secret_path = (
113
- f"{vault_path}/{integration_name}/" + f"shared-resources/{name}"
114
- )
124
+ secret_path = f"{vault_path}/{integration_name}/shared-resources/{name}"
115
125
  secret = {"path": secret_path, "data": body_data}
116
- vault_client.write(secret)
126
+ vault_client.write(secret) # type: ignore
117
127
 
118
128
 
119
- def canonicalize_namespaces(namespaces):
120
- canonicalized_namespaces = []
121
- for namespace_info in namespaces:
122
- if ob.is_namespace_deleted(namespace_info):
123
- continue
124
- ob.aggregate_shared_resources(namespace_info, "openshiftServiceAccountTokens")
125
- openshift_serviceaccount_tokens = namespace_info.get(
126
- "openshiftServiceAccountTokens"
129
+ def canonicalize_namespaces(namespaces: Iterable[NamespaceV1]) -> list[NamespaceV1]:
130
+ canonicalized_namespaces: dict[str, NamespaceV1] = {}
131
+ for namespace in namespaces:
132
+ ob.aggregate_shared_resources_typed(namespace)
133
+ if namespace.openshift_service_account_tokens:
134
+ canonicalized_namespaces[f"{namespace.cluster.name}/{namespace.name}"] = (
135
+ namespace
136
+ )
137
+ for namespace in list(canonicalized_namespaces.values()):
138
+ for sat in namespace.openshift_service_account_tokens or []:
139
+ key = f"{sat.namespace.cluster.name}/{sat.namespace.name}"
140
+ if key not in canonicalized_namespaces:
141
+ canonicalized_namespaces[key] = NamespaceV1(
142
+ **sat.namespace.dict(by_alias=True),
143
+ sharedResources=None,
144
+ openshiftServiceAccountTokens=None,
145
+ )
146
+ return list(canonicalized_namespaces.values())
147
+
148
+
149
+ def get_namespaces_with_serviceaccount_tokens(
150
+ query_func: Callable,
151
+ ) -> list[NamespaceV1]:
152
+ return [
153
+ namespace
154
+ for namespace in serviceaccount_tokens_query(query_func=query_func).namespaces
155
+ or []
156
+ if integration_is_enabled(QONTRACT_INTEGRATION, namespace.cluster)
157
+ and not bool(namespace.delete)
158
+ and (
159
+ namespace.openshift_service_account_tokens
160
+ or any(
161
+ sr.openshift_service_account_tokens
162
+ for sr in namespace.shared_resources or []
163
+ )
127
164
  )
128
- if openshift_serviceaccount_tokens:
129
- canonicalized_namespaces.append(namespace_info)
130
- for sat in openshift_serviceaccount_tokens:
131
- canonicalized_namespaces.append(sat["namespace"])
132
-
133
- return canonicalized_namespaces
165
+ ]
134
166
 
135
167
 
136
168
  @defer
137
169
  def run(
138
- dry_run,
139
- thread_pool_size=10,
140
- internal=None,
141
- use_jump_host=True,
142
- vault_output_path="",
143
- defer=None,
144
- ):
145
- namespaces = canonicalize_namespaces(queries.get_serviceaccount_tokens())
170
+ dry_run: bool,
171
+ thread_pool_size: int = 10,
172
+ internal: bool | None = None,
173
+ use_jump_host: bool = True,
174
+ vault_output_path: str = "",
175
+ defer: Callable | None = None,
176
+ ) -> None:
177
+ gql_api = gql.get_api()
178
+ namespaces = canonicalize_namespaces(
179
+ get_namespaces_with_serviceaccount_tokens(gql_api.query)
180
+ )
146
181
  ri, oc_map = ob.fetch_current_state(
147
- namespaces=namespaces,
182
+ namespaces=[ns.dict(by_alias=True) for ns in namespaces],
148
183
  thread_pool_size=thread_pool_size,
149
184
  integration=QONTRACT_INTEGRATION,
150
185
  integration_version=QONTRACT_INTEGRATION_VERSION,
@@ -152,12 +187,13 @@ def run(
152
187
  internal=internal,
153
188
  use_jump_host=use_jump_host,
154
189
  )
155
- defer(oc_map.cleanup)
190
+ if defer:
191
+ defer(oc_map.cleanup)
156
192
  fetch_desired_state(namespaces, ri, oc_map)
157
193
  ob.publish_metrics(ri, QONTRACT_INTEGRATION)
158
194
  ob.realize_data(dry_run, oc_map, ri, thread_pool_size)
159
195
  if not dry_run and vault_output_path:
160
- write_outputs_to_vault(vault_output_path, ri)
196
+ write_outputs_to_vault(VaultClient(), vault_output_path, ri)
161
197
 
162
198
  if ri.has_error_registered():
163
199
  sys.exit(1)
reconcile/queries.py CHANGED
@@ -1553,79 +1553,6 @@ def get_namespaces(minimal=False):
1553
1553
  return gqlapi.query(NAMESPACES_QUERY)["namespaces"]
1554
1554
 
1555
1555
 
1556
- SA_TOKEN = """
1557
- name
1558
- namespace {
1559
- name
1560
- cluster {
1561
- name
1562
- serverUrl
1563
- insecureSkipTLSVerify
1564
- jumpHost {
1565
- %s
1566
- }
1567
- automationToken {
1568
- path
1569
- field
1570
- version
1571
- format
1572
- }
1573
- internal
1574
- disable {
1575
- integrations
1576
- }
1577
- }
1578
- }
1579
- serviceAccountName
1580
- """ % (indent(JUMPHOST_FIELDS, 6 * " "),)
1581
-
1582
-
1583
- SERVICEACCOUNT_TOKENS_QUERY = """
1584
- {
1585
- namespaces: namespaces_v1 {
1586
- name
1587
- delete
1588
- cluster {
1589
- name
1590
- serverUrl
1591
- insecureSkipTLSVerify
1592
- jumpHost {
1593
- %s
1594
- }
1595
- automationToken {
1596
- path
1597
- field
1598
- version
1599
- format
1600
- }
1601
- internal
1602
- disable {
1603
- integrations
1604
- }
1605
- }
1606
- sharedResources {
1607
- openshiftServiceAccountTokens {
1608
- %s
1609
- }
1610
- }
1611
- openshiftServiceAccountTokens {
1612
- %s
1613
- }
1614
- }
1615
- }
1616
- """ % (
1617
- indent(JUMPHOST_FIELDS, 8 * " "),
1618
- indent(SA_TOKEN, 8 * " "),
1619
- indent(SA_TOKEN, 6 * " "),
1620
- )
1621
-
1622
-
1623
- def get_serviceaccount_tokens():
1624
- """Returns all namespaces with ServiceAccount tokens information"""
1625
- gqlapi = gql.get_api()
1626
- return gqlapi.query(SERVICEACCOUNT_TOKENS_QUERY)["namespaces"]
1627
-
1628
-
1629
1556
  PRODUCTS_QUERY = """
1630
1557
  {
1631
1558
  products: products_v1 {
@@ -1205,3 +1205,65 @@ def test_get_state_count_combinations():
1205
1205
  ]
1206
1206
  expected = {"c1": 2, "c2": 2, "c3": 1}
1207
1207
  assert expected == sut.get_state_count_combinations(state)
1208
+
1209
+
1210
+ def test_aggregate_shared_resources_typed_openshift_service_resources() -> None:
1211
+ class OpenShiftResourcesStub(BaseModel):
1212
+ openshift_resources: list | None
1213
+
1214
+ class OpenShiftResourcesAndSharedResourcesStub(OpenShiftResourcesStub, BaseModel):
1215
+ shared_resources: list[OpenShiftResourcesStub] | None
1216
+
1217
+ namespace = OpenShiftResourcesAndSharedResourcesStub(
1218
+ openshift_resources=[1], shared_resources=None
1219
+ )
1220
+ sut.aggregate_shared_resources_typed(namespace=namespace)
1221
+ assert namespace.openshift_resources == [1]
1222
+
1223
+ namespace = OpenShiftResourcesAndSharedResourcesStub(
1224
+ openshift_resources=None,
1225
+ shared_resources=[OpenShiftResourcesStub(openshift_resources=[2])],
1226
+ )
1227
+ sut.aggregate_shared_resources_typed(namespace=namespace)
1228
+ assert namespace.openshift_resources == [2]
1229
+
1230
+ namespace = OpenShiftResourcesAndSharedResourcesStub(
1231
+ openshift_resources=[1],
1232
+ shared_resources=[OpenShiftResourcesStub(openshift_resources=[2])],
1233
+ )
1234
+ sut.aggregate_shared_resources_typed(namespace=namespace)
1235
+ assert namespace.openshift_resources == [1, 2]
1236
+
1237
+
1238
+ def test_aggregate_shared_resources_typed_openshift_service_account_token() -> None:
1239
+ class OpenshiftServiceAccountTokensStub(BaseModel):
1240
+ openshift_service_account_tokens: list | None
1241
+
1242
+ class OpenshiftServiceAccountTokensAndSharedResourcesStub(
1243
+ OpenshiftServiceAccountTokensStub, BaseModel
1244
+ ):
1245
+ shared_resources: list[OpenshiftServiceAccountTokensStub] | None
1246
+
1247
+ namespace = OpenshiftServiceAccountTokensAndSharedResourcesStub(
1248
+ openshift_service_account_tokens=[1], shared_resources=None
1249
+ )
1250
+ sut.aggregate_shared_resources_typed(namespace=namespace)
1251
+ assert namespace.openshift_service_account_tokens == [1]
1252
+
1253
+ namespace = OpenshiftServiceAccountTokensAndSharedResourcesStub(
1254
+ openshift_service_account_tokens=None,
1255
+ shared_resources=[
1256
+ OpenshiftServiceAccountTokensStub(openshift_service_account_tokens=[2])
1257
+ ],
1258
+ )
1259
+ sut.aggregate_shared_resources_typed(namespace=namespace)
1260
+ assert namespace.openshift_service_account_tokens == [2]
1261
+
1262
+ namespace = OpenshiftServiceAccountTokensAndSharedResourcesStub(
1263
+ openshift_service_account_tokens=[1],
1264
+ shared_resources=[
1265
+ OpenshiftServiceAccountTokensStub(openshift_service_account_tokens=[2])
1266
+ ],
1267
+ )
1268
+ sut.aggregate_shared_resources_typed(namespace=namespace)
1269
+ assert namespace.openshift_service_account_tokens == [1, 2]
@@ -0,0 +1,228 @@
1
+ from collections.abc import Callable, Mapping
2
+ from typing import Any
3
+ from unittest.mock import call
4
+
5
+ import pytest
6
+ from pytest_mock import MockerFixture
7
+
8
+ from reconcile.gql_definitions.openshift_serviceaccount_tokens.tokens import NamespaceV1
9
+ from reconcile.openshift_serviceaccount_tokens import (
10
+ canonicalize_namespaces,
11
+ construct_sa_token_oc_resource,
12
+ fetch_desired_state,
13
+ get_namespaces_with_serviceaccount_tokens,
14
+ get_tokens_for_service_account,
15
+ write_outputs_to_vault,
16
+ )
17
+ from reconcile.test.fixtures import Fixtures
18
+ from reconcile.utils.oc import OC_Map, OCCli
19
+ from reconcile.utils.openshift_resource import ResourceInventory
20
+ from reconcile.utils.vault import _VaultClient
21
+
22
+
23
+ @pytest.fixture
24
+ def fx() -> Fixtures:
25
+ return Fixtures("openshift_serviceaccount_tokens")
26
+
27
+
28
+ @pytest.fixture
29
+ def query_func(
30
+ data_factory: Callable[[type[NamespaceV1], Mapping[str, Any]], Mapping[str, Any]],
31
+ fx: Fixtures,
32
+ ) -> Callable:
33
+ def q(*args: Any, **kwargs: Any) -> dict:
34
+ return {
35
+ "namespaces": [
36
+ data_factory(NamespaceV1, item)
37
+ for item in fx.get_anymarkup("namespaces.yml")["namespaces"]
38
+ ]
39
+ }
40
+
41
+ return q
42
+
43
+
44
+ @pytest.fixture
45
+ def namespaces(query_func: Callable) -> list[NamespaceV1]:
46
+ return get_namespaces_with_serviceaccount_tokens(query_func)
47
+
48
+
49
+ @pytest.fixture
50
+ def ri(namespaces: list[NamespaceV1]) -> ResourceInventory:
51
+ _ri = ResourceInventory()
52
+ _ri.initialize_resource_type(
53
+ cluster="cluster", namespace="namespace", resource_type="Secret"
54
+ )
55
+ for ns in namespaces:
56
+ _ri.initialize_resource_type(
57
+ cluster=ns.cluster.name, namespace=ns.name, resource_type="Secret"
58
+ )
59
+ return _ri
60
+
61
+
62
+ def test_openshift_serviceaccount_tokens__construct_sa_token_oc_resource() -> None:
63
+ qr = construct_sa_token_oc_resource("foobar", "token")
64
+ assert qr.body == {
65
+ "apiVersion": "v1",
66
+ "data": {"token": "token"},
67
+ "kind": "Secret",
68
+ "metadata": {"name": "foobar"},
69
+ "type": "Opaque",
70
+ }
71
+
72
+
73
+ def test_openshift_serviceaccount_tokens__get_tokens_for_service_account() -> None:
74
+ foobar_secret = {
75
+ "metadata": {
76
+ "annotations": {"kubernetes.io/service-account.name": "foobar-account"}
77
+ },
78
+ "type": "kubernetes.io/service-account-token",
79
+ }
80
+ assert get_tokens_for_service_account(
81
+ service_account="foobar-account",
82
+ tokens=[
83
+ foobar_secret,
84
+ {
85
+ "metadata": {
86
+ "annotations": {
87
+ "kubernetes.io/service-account.name": "just-another-account"
88
+ }
89
+ },
90
+ "type": "kubernetes.io/service-account-token",
91
+ },
92
+ foobar_secret,
93
+ {
94
+ "metadata": {"annotations": {"whatever": "foobar-account"}},
95
+ "type": "not-a-service-account-token",
96
+ },
97
+ ],
98
+ ) == [foobar_secret, foobar_secret]
99
+
100
+
101
+ def test_openshift_serviceaccount_tokens__write_outputs_to_vault(
102
+ mocker: MockerFixture, ri: ResourceInventory
103
+ ) -> None:
104
+ vault_client = mocker.create_autospec(_VaultClient)
105
+
106
+ ri.add_desired(
107
+ cluster="cluster",
108
+ namespace="namespace",
109
+ resource_type="Secret",
110
+ name="name",
111
+ value=construct_sa_token_oc_resource("name", "token"),
112
+ )
113
+ write_outputs_to_vault(vault_client, "path/to/secrets", ri)
114
+ vault_client.write.call_count == 2
115
+ vault_client.write.assert_has_calls([
116
+ call({
117
+ "path": "path/to/secrets/openshift-serviceaccount-tokens/cluster/namespace/name",
118
+ "data": {"token": "token"},
119
+ }),
120
+ call({
121
+ "path": "path/to/secrets/openshift-serviceaccount-tokens/shared-resources/name",
122
+ "data": {"token": "token"},
123
+ }),
124
+ ])
125
+
126
+
127
+ def test_openshift_serviceaccount_tokens__get_namespaces_with_serviceaccount_tokens(
128
+ namespaces: list[NamespaceV1],
129
+ ) -> None:
130
+ assert len(namespaces) == 3
131
+ assert namespaces[0].name == "with-openshift-serviceaccount-tokens"
132
+ assert namespaces[1].name == "with-shared-resources"
133
+ assert (
134
+ namespaces[2].name
135
+ == "with-openshift-serviceaccount-tokens-and-shared-resources"
136
+ )
137
+
138
+
139
+ def test_openshift_serviceaccount_tokens__canonicalize_namespaces(
140
+ namespaces: list[NamespaceV1],
141
+ ) -> None:
142
+ nss = canonicalize_namespaces(namespaces)
143
+ # sort by number of tokens and namespace name
144
+ nss.sort(key=lambda n: f"{len(n.openshift_service_account_tokens or [])}-{n.name}")
145
+ assert len(nss) == 6
146
+
147
+ # added via remote service account token
148
+ assert nss[0].name == "observability"
149
+ assert nss[0].openshift_service_account_tokens is None
150
+ assert nss[1].name == "platform-changelog-stage"
151
+ assert nss[1].openshift_service_account_tokens is None
152
+ assert nss[2].name == "with-openshift-serviceaccount-tokens"
153
+ assert nss[2].openshift_service_account_tokens is None
154
+
155
+ # namespace with tokens or shared resources defined
156
+ nss[3].name == "with-shared-resources"
157
+ nss[3].cluster.name == "cluster"
158
+ assert len(nss[3].openshift_service_account_tokens or []) == 1
159
+
160
+ nss[4].name == "with-openshift-serviceaccount-tokens"
161
+ assert len(nss[4].openshift_service_account_tokens or []) == 2
162
+
163
+ nss[5].name == "with-openshift-serviceaccount-tokens-and-shared-resources"
164
+ assert len(nss[5].openshift_service_account_tokens or []) == 2
165
+
166
+
167
+ def test_openshift_serviceaccount_tokens__fetch_desired_state(
168
+ mocker: MockerFixture, namespaces: list[NamespaceV1], ri: ResourceInventory
169
+ ) -> None:
170
+ grafana_secret = {
171
+ "metadata": {
172
+ "name": "grafana-secret",
173
+ "annotations": {"kubernetes.io/service-account.name": "grafana"},
174
+ },
175
+ "type": "kubernetes.io/service-account-token",
176
+ "data": {"token": "super-secret-token"},
177
+ }
178
+
179
+ oc_map = mocker.create_autospec(OC_Map)
180
+ oc = mocker.create_autospec(OCCli)
181
+ oc_map.get.return_value = oc
182
+ oc.get_items.return_value = [
183
+ grafana_secret,
184
+ {
185
+ "metadata": {
186
+ "name": "just-another-account-secret",
187
+ "annotations": {
188
+ "kubernetes.io/service-account.name": "just-another-account"
189
+ },
190
+ },
191
+ "type": "kubernetes.io/service-account-token",
192
+ },
193
+ grafana_secret,
194
+ {
195
+ "metadata": {
196
+ "name": "just-something-different",
197
+ "annotations": {"whatever": "grafana"},
198
+ },
199
+ "type": "not-a-service-account-token",
200
+ },
201
+ ]
202
+ fetch_desired_state(
203
+ namespaces=namespaces,
204
+ ri=ri,
205
+ oc_map=oc_map,
206
+ )
207
+ assert (
208
+ len(
209
+ ri._clusters["cluster"]["with-openshift-serviceaccount-tokens"]["Secret"][
210
+ "desired"
211
+ ].keys()
212
+ )
213
+ == 2
214
+ )
215
+ assert (
216
+ len(
217
+ ri._clusters["cluster"][
218
+ "with-openshift-serviceaccount-tokens-and-shared-resources"
219
+ ]["Secret"]["desired"].keys()
220
+ )
221
+ == 1
222
+ )
223
+ assert (
224
+ "another-cluster-with-openshift-serviceaccount-tokens-grafana"
225
+ in ri._clusters["cluster"]["with-openshift-serviceaccount-tokens"]["Secret"][
226
+ "desired"
227
+ ]
228
+ )