qontract-reconcile 0.10.2.dev481__py3-none-any.whl → 0.10.2.dev484__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.
@@ -0,0 +1,135 @@
1
+ """OpenShift RoleBindings integration.
2
+
3
+ Manages namespace-scoped RoleBindings within OpenShift namespaces.
4
+ """
5
+
6
+ import sys
7
+ from collections.abc import Callable
8
+ from typing import TYPE_CHECKING
9
+
10
+ import reconcile.openshift_base as ob
11
+ from reconcile.openshift_bindings.constants import (
12
+ OPENSHIFT_ROLEBINDINGS_INTEGRATION_NAME,
13
+ )
14
+ from reconcile.openshift_bindings.models import RoleBindingSpec
15
+ from reconcile.openshift_bindings.utils import (
16
+ is_valid_namespace,
17
+ )
18
+ from reconcile.typed_queries.app_interface_roles import get_app_interface_roles
19
+ from reconcile.typed_queries.namespaces import get_namespaces
20
+ from reconcile.utils import expiration
21
+ from reconcile.utils.constants import DEFAULT_THREAD_POOL_SIZE
22
+ from reconcile.utils.defer import defer
23
+ from reconcile.utils.oc import OC_Map
24
+ from reconcile.utils.openshift_resource import ResourceInventory
25
+ from reconcile.utils.runtime.integration import (
26
+ PydanticRunParams,
27
+ QontractReconcileIntegration,
28
+ )
29
+ from reconcile.utils.semver_helper import make_semver
30
+
31
+ if TYPE_CHECKING:
32
+ from reconcile.gql_definitions.common.app_interface_roles import RoleV1
33
+
34
+ QONTRACT_INTEGRATION_VERSION = make_semver(0, 3, 0)
35
+ QONTRACT_INTEGRATION_MANAGED_TYPE = "RoleBinding.rbac.authorization.k8s.io"
36
+
37
+
38
+ class OpenShiftRoleBindingsIntegrationParams(PydanticRunParams):
39
+ support_role_ref: bool = False
40
+ enforced_user_keys: list[str] | None = None
41
+ thread_pool_size: int = DEFAULT_THREAD_POOL_SIZE
42
+ internal: bool | None = None
43
+ use_jump_host: bool = True
44
+
45
+
46
+ class OpenShiftRoleBindingsIntegration(
47
+ QontractReconcileIntegration[OpenShiftRoleBindingsIntegrationParams],
48
+ ):
49
+ """Manages RoleBindings within OpenShift namespaces."""
50
+
51
+ @defer
52
+ def run(self, dry_run: bool, defer: Callable | None = None) -> None:
53
+ ri, oc_map = self.fetch_current_state()
54
+ if defer:
55
+ defer(oc_map.cleanup)
56
+ self.fetch_desired_state(
57
+ ri,
58
+ support_role_ref=self.params.support_role_ref,
59
+ enforced_user_keys=self.params.enforced_user_keys,
60
+ allowed_clusters=set(oc_map.clusters()),
61
+ )
62
+ ob.publish_metrics(ri, self.name)
63
+ ob.realize_data(dry_run, oc_map, ri, self.params.thread_pool_size)
64
+ if ri.has_error_registered():
65
+ sys.exit(1)
66
+
67
+ @property
68
+ def integration_version(self) -> str:
69
+ return QONTRACT_INTEGRATION_VERSION
70
+
71
+ @property
72
+ def name(self) -> str:
73
+ return OPENSHIFT_ROLEBINDINGS_INTEGRATION_NAME
74
+
75
+ def fetch_current_state(self) -> tuple[ResourceInventory, OC_Map]:
76
+ """Fetch current RoleBindings state from namespaces."""
77
+ namespaces = [
78
+ namespace.model_dump(by_alias=True, exclude={"openshift_resources"})
79
+ for namespace in get_namespaces()
80
+ if is_valid_namespace(namespace)
81
+ ]
82
+ return ob.fetch_current_state(
83
+ namespaces=namespaces,
84
+ thread_pool_size=self.params.thread_pool_size,
85
+ integration=self.name,
86
+ integration_version=self.integration_version,
87
+ override_managed_types=[QONTRACT_INTEGRATION_MANAGED_TYPE],
88
+ internal=self.params.internal,
89
+ use_jump_host=self.params.use_jump_host,
90
+ )
91
+
92
+ def fetch_desired_state(
93
+ self,
94
+ ri: ResourceInventory | None,
95
+ support_role_ref: bool = False,
96
+ enforced_user_keys: list[str] | None = None,
97
+ allowed_clusters: set[str] | None = None,
98
+ ) -> None:
99
+ if allowed_clusters is not None and not allowed_clusters:
100
+ return
101
+ if ri is None:
102
+ return
103
+ roles: list[RoleV1] = expiration.filter(get_app_interface_roles())
104
+ for role in roles:
105
+ rolebindings: list[RoleBindingSpec] = (
106
+ RoleBindingSpec.create_rb_specs_from_role(
107
+ role, enforced_user_keys, support_role_ref
108
+ )
109
+ )
110
+ if allowed_clusters is not None:
111
+ rolebindings = [
112
+ rolebinding
113
+ for rolebinding in rolebindings
114
+ if rolebinding.cluster.name in allowed_clusters
115
+ ]
116
+ for rolebinding in rolebindings:
117
+ for oc_resource in rolebinding.get_openshift_resources(
118
+ self.name,
119
+ self.integration_version,
120
+ privileged=rolebinding.privileged,
121
+ ):
122
+ if not ri.get_desired(
123
+ rolebinding.cluster.name,
124
+ rolebinding.namespace.name,
125
+ QONTRACT_INTEGRATION_MANAGED_TYPE,
126
+ oc_resource.resource_name,
127
+ ):
128
+ ri.add_desired(
129
+ cluster=rolebinding.cluster.name,
130
+ namespace=rolebinding.namespace.name,
131
+ resource_type=QONTRACT_INTEGRATION_MANAGED_TYPE,
132
+ name=oc_resource.resource_name,
133
+ value=oc_resource.resource,
134
+ privileged=oc_resource.privileged,
135
+ )
@@ -0,0 +1,14 @@
1
+ import reconcile.openshift_base as ob
2
+ from reconcile.gql_definitions.common.app_interface_roles import NamespaceV1
3
+ from reconcile.gql_definitions.common.namespaces import NamespaceV1 as CommonNamespaceV1
4
+ from reconcile.utils.sharding import is_in_shard
5
+
6
+
7
+ def is_valid_namespace(
8
+ namespace: NamespaceV1 | CommonNamespaceV1,
9
+ ) -> bool:
10
+ return (
11
+ bool(namespace.managed_roles)
12
+ and is_in_shard(f"{namespace.cluster.name}/{namespace.name}")
13
+ and not ob.is_namespace_deleted(namespace.model_dump(by_alias=True))
14
+ )
@@ -5,23 +5,28 @@ from collections.abc import (
5
5
  Iterable,
6
6
  Mapping,
7
7
  )
8
- from typing import Any
8
+ from typing import TYPE_CHECKING, Any
9
9
 
10
10
  from sretoolbox.utils import threaded
11
11
 
12
12
  from reconcile import (
13
13
  openshift_groups,
14
- openshift_rolebindings,
15
14
  )
15
+
16
+ if TYPE_CHECKING:
17
+ from reconcile.gql_definitions.common.app_interface_roles import RoleV1
16
18
  from reconcile.gql_definitions.common.clusters_minimal import (
17
19
  ClusterAuthOIDCV1,
18
20
  ClusterAuthRHIDPV1,
19
21
  ClusterV1,
20
22
  )
23
+ from reconcile.openshift_bindings.models import RoleBindingSpec
24
+ from reconcile.typed_queries.app_interface_roles import get_app_interface_roles
21
25
  from reconcile.typed_queries.app_interface_vault_settings import (
22
26
  get_app_interface_vault_settings,
23
27
  )
24
28
  from reconcile.typed_queries.clusters_minimal import get_clusters_minimal
29
+ from reconcile.utils import expiration
25
30
  from reconcile.utils.constants import DEFAULT_THREAD_POOL_SIZE
26
31
  from reconcile.utils.defer import defer
27
32
  from reconcile.utils.oc_map import (
@@ -97,15 +102,55 @@ def fetch_current_state(
97
102
  return oc_map, current_state
98
103
 
99
104
 
105
+ def fetch_rolebindings_desired_state(
106
+ allowed_clusters: set[str] | None = None,
107
+ enforced_user_keys: list[str] | None = None,
108
+ ) -> list[dict[str, str]]:
109
+ if allowed_clusters is not None and not allowed_clusters:
110
+ return []
111
+ roles: list[RoleV1] = expiration.filter(get_app_interface_roles())
112
+
113
+ users_desired_state: list[dict[str, str]] = [
114
+ user
115
+ for role in roles
116
+ for rolebinding in filter_rolebindings(
117
+ RoleBindingSpec.create_rb_specs_from_role(role, enforced_user_keys),
118
+ allowed_clusters,
119
+ )
120
+ for user in get_users_from_rolebinding_desired_state(rolebinding)
121
+ ]
122
+ return users_desired_state
123
+
124
+
125
+ def get_users_from_rolebinding_desired_state(
126
+ role_binding: RoleBindingSpec,
127
+ ) -> list[dict[str, str]]:
128
+ return [
129
+ {"cluster": role_binding.cluster.name, "user": user}
130
+ for user in role_binding.usernames
131
+ ]
132
+
133
+
134
+ def filter_rolebindings(
135
+ rolebindings: list[RoleBindingSpec], allowed_clusters: set[str] | None = None
136
+ ) -> list[RoleBindingSpec]:
137
+ if allowed_clusters is not None:
138
+ return [
139
+ rolebinding
140
+ for rolebinding in rolebindings
141
+ if rolebinding.cluster.name in allowed_clusters
142
+ ]
143
+ return rolebindings
144
+
145
+
100
146
  def fetch_desired_state(
101
147
  oc_map: OCMap | None, enforced_user_keys: Any = None
102
148
  ) -> list[Any]:
103
149
  desired_state = []
104
150
  filtered_clusters = oc_map.clusters() if oc_map else None
105
- flat_rolebindings_desired_state = openshift_rolebindings.fetch_desired_state(
106
- ri=None,
107
- enforced_user_keys=enforced_user_keys,
151
+ flat_rolebindings_desired_state = fetch_rolebindings_desired_state(
108
152
  allowed_clusters=set(filtered_clusters) if filtered_clusters else None,
153
+ enforced_user_keys=enforced_user_keys,
109
154
  )
110
155
  desired_state.extend(flat_rolebindings_desired_state)
111
156
 
@@ -0,0 +1,10 @@
1
+ from reconcile.gql_definitions.common.app_interface_clusterrole import (
2
+ RoleV1,
3
+ query,
4
+ )
5
+ from reconcile.utils import gql
6
+
7
+
8
+ def get_app_interface_clusterroles() -> list[RoleV1]:
9
+ data = query(gql.get_api().query)
10
+ return list(data.cluster_roles or [])
@@ -1,192 +0,0 @@
1
- import contextlib
2
- import sys
3
- from collections.abc import Callable
4
-
5
- import reconcile.openshift_base as ob
6
- from reconcile import queries
7
- from reconcile.utils import (
8
- expiration,
9
- gql,
10
- )
11
- from reconcile.utils.constants import DEFAULT_THREAD_POOL_SIZE
12
- from reconcile.utils.defer import defer
13
- from reconcile.utils.openshift_resource import OpenshiftResource as OR
14
- from reconcile.utils.openshift_resource import (
15
- ResourceInventory,
16
- ResourceKeyExistsError,
17
- )
18
- from reconcile.utils.semver_helper import make_semver
19
-
20
- ROLES_QUERY = """
21
- {
22
- roles: roles_v1 {
23
- name
24
- users {
25
- org_username
26
- github_username
27
- }
28
- bots {
29
- openshift_serviceaccount
30
- }
31
- access {
32
- cluster {
33
- name
34
- auth {
35
- service
36
- }
37
- }
38
- clusterRole
39
- }
40
- expirationDate
41
- }
42
- }
43
- """
44
-
45
-
46
- QONTRACT_INTEGRATION = "openshift-clusterrolebindings"
47
- QONTRACT_INTEGRATION_VERSION = make_semver(0, 1, 0)
48
-
49
-
50
- def construct_user_oc_resource(role: str, user: str) -> tuple[OR, str]:
51
- name = f"{role}-{user}"
52
- # Note: In OpenShift 4.x this resource is in rbac.authorization.k8s.io/v1
53
- body = {
54
- "apiVersion": "rbac.authorization.k8s.io/v1",
55
- "kind": "ClusterRoleBinding",
56
- "metadata": {"name": name},
57
- "roleRef": {"name": role, "kind": "ClusterRole"},
58
- "subjects": [{"kind": "User", "name": user}],
59
- }
60
- return (
61
- OR(
62
- body, QONTRACT_INTEGRATION, QONTRACT_INTEGRATION_VERSION, error_details=name
63
- ),
64
- name,
65
- )
66
-
67
-
68
- def construct_sa_oc_resource(role: str, namespace: str, sa_name: str) -> tuple[OR, str]:
69
- name = f"{role}-{namespace}-{sa_name}"
70
- # Note: In OpenShift 4.x this resource is in rbac.authorization.k8s.io/v1
71
- body = {
72
- "apiVersion": "rbac.authorization.k8s.io/v1",
73
- "kind": "ClusterRoleBinding",
74
- "metadata": {"name": name},
75
- "roleRef": {"name": role, "kind": "ClusterRole"},
76
- "subjects": [
77
- {"kind": "ServiceAccount", "name": sa_name, "namespace": namespace}
78
- ],
79
- }
80
- return (
81
- OR(
82
- body, QONTRACT_INTEGRATION, QONTRACT_INTEGRATION_VERSION, error_details=name
83
- ),
84
- name,
85
- )
86
-
87
-
88
- def fetch_desired_state(
89
- ri: ResourceInventory | None, oc_map: ob.ClusterMap
90
- ) -> list[dict[str, str]]:
91
- gqlapi = gql.get_api()
92
- roles: list[dict] = expiration.filter(gqlapi.query(ROLES_QUERY)["roles"])
93
- users_desired_state = []
94
- # set namespace to something indicative
95
- namespace_cluster_scope = "cluster"
96
- for role in roles:
97
- permissions = [
98
- {"cluster": a["cluster"], "cluster_role": a["clusterRole"]}
99
- for a in role["access"] or []
100
- if a["cluster"] and a["clusterRole"]
101
- ]
102
- if not permissions:
103
- continue
104
-
105
- service_accounts = [
106
- bot["openshift_serviceaccount"]
107
- for bot in role["bots"]
108
- if bot.get("openshift_serviceaccount")
109
- ]
110
-
111
- for permission in permissions:
112
- cluster_info = permission["cluster"]
113
- cluster = cluster_info["name"]
114
- if not oc_map.get(cluster):
115
- continue
116
-
117
- # get username keys based on used IDPs
118
- user_keys = ob.determine_user_keys_for_access(cluster, cluster_info["auth"])
119
- for user in role["users"]:
120
- for username in {user[user_key] for user_key in user_keys}:
121
- # used by openshift-users and github integrations
122
- # this is just to simplify things a bit on the their side
123
- users_desired_state.append({"cluster": cluster, "user": username})
124
- if ri is None:
125
- continue
126
- oc_resource, resource_name = construct_user_oc_resource(
127
- permission["cluster_role"], username
128
- )
129
- with contextlib.suppress(ResourceKeyExistsError):
130
- # a user may have a Role assigned to them
131
- # from multiple app-interface roles
132
- ri.add_desired(
133
- cluster,
134
- namespace_cluster_scope,
135
- "ClusterRoleBinding.rbac.authorization.k8s.io",
136
- resource_name,
137
- oc_resource,
138
- )
139
-
140
- for sa in service_accounts:
141
- if ri is None:
142
- continue
143
- namespace, sa_name = sa.split("/")
144
- oc_resource, resource_name = construct_sa_oc_resource(
145
- permission["cluster_role"], namespace, sa_name
146
- )
147
-
148
- with contextlib.suppress(ResourceKeyExistsError):
149
- # a ServiceAccount may have a Role assigned to it
150
- # from multiple app-interface roles
151
- ri.add_desired(
152
- cluster,
153
- namespace_cluster_scope,
154
- "ClusterRoleBinding.rbac.authorization.k8s.io",
155
- resource_name,
156
- oc_resource,
157
- )
158
-
159
- return users_desired_state
160
-
161
-
162
- @defer
163
- def run(
164
- dry_run: bool,
165
- thread_pool_size: int = DEFAULT_THREAD_POOL_SIZE,
166
- internal: bool | None = None,
167
- use_jump_host: bool = True,
168
- defer: Callable | None = None,
169
- ) -> None:
170
- clusters = [
171
- cluster_info
172
- for cluster_info in queries.get_clusters()
173
- if cluster_info.get("managedClusterRoles")
174
- and cluster_info.get("automationToken")
175
- ]
176
- ri, oc_map = ob.fetch_current_state(
177
- clusters=clusters,
178
- thread_pool_size=thread_pool_size,
179
- integration=QONTRACT_INTEGRATION,
180
- integration_version=QONTRACT_INTEGRATION_VERSION,
181
- override_managed_types=["ClusterRoleBinding.rbac.authorization.k8s.io"],
182
- internal=internal,
183
- use_jump_host=use_jump_host,
184
- )
185
- if defer:
186
- defer(oc_map.cleanup)
187
- fetch_desired_state(ri, oc_map)
188
- ob.publish_metrics(ri, QONTRACT_INTEGRATION)
189
- ob.realize_data(dry_run, oc_map, ri, thread_pool_size)
190
-
191
- if ri.has_error_registered():
192
- sys.exit(1)