qontract-reconcile 0.10.1rc508__py3-none-any.whl → 0.10.1rc510__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.1rc508
3
+ Version: 0.10.1rc510
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
@@ -62,7 +62,7 @@ reconcile/ocm_update_recommended_version.py,sha256=IYkfLXIprOW1jguZeELcGP1iBPuj-
62
62
  reconcile/ocm_upgrade_scheduler_org_updater.py,sha256=ta8hMJ-su5mRcPpYrvB1COsojXV-SU3PzLPbQhy2Q0I,4190
63
63
  reconcile/openshift_base.py,sha256=7aifvl-ay5wpY6encbUX9pGbKdjiwJmevZ3XWGRzpCM,49696
64
64
  reconcile/openshift_cluster_bots.py,sha256=cdvIIXBh7QZl3g7ZheCR2NeI_Pq4eKn-hJPoixl9NS8,10168
65
- reconcile/openshift_clusterrolebindings.py,sha256=uSptf8xPiPpPRq94dKVe3w--grO1mMeZU5G17oIbUoQ,5816
65
+ reconcile/openshift_clusterrolebindings.py,sha256=QfSy1Ik8eEY5XObc1Q4xyhqyErZenJmbPv_u9wcDNNo,5864
66
66
  reconcile/openshift_groups.py,sha256=d-qGI1aUEpZZLZq7PuSnjVDgsy5EB063CQr2tNvYPCE,9419
67
67
  reconcile/openshift_limitranges.py,sha256=UvCGo_OQ4XoDK55TJmn55qEhhlkhLzhU12tX8nT5kPQ,3442
68
68
  reconcile/openshift_namespace_labels.py,sha256=dLkQgtgsD51WtDHiQOc-lF2yaaFzkiUAZ7ueKUkg-ZM,15669
@@ -585,6 +585,10 @@ reconcile/utils/unleash.py,sha256=1D56CsZfE3ShDtN3IErE1T2eeIwNmxhK-yYbCotJ99E,36
585
585
  reconcile/utils/vault.py,sha256=S0eHqvZ9N3fya1E8YDaUffEvLk_fdtpzL4rvWn6f828,14991
586
586
  reconcile/utils/vaultsecretref.py,sha256=3Ed2uBy36TzSvL0B-l4FoWQqB2SbBKDKEuUPIO608Bo,931
587
587
  reconcile/utils/vcs.py,sha256=o1r0n_IrU2El75CED_6sjR2GZGM-exuWsj5F7jONaMU,6779
588
+ reconcile/utils/acs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
589
+ reconcile/utils/acs/base.py,sha256=Qih-xZ3RBJZEE291iHHlv7lUY6ShcAvSj1PA3_aTTnM,2276
590
+ reconcile/utils/acs/policies.py,sha256=ucmvWhcu9I5uumOyFIr5POb64TqHNNJ8LutZWo2Jw7k,5099
591
+ reconcile/utils/acs/rbac.py,sha256=ugsLM9Pb7FbUbdq85E3VzXGMaB9ZovXob7tdWCxwqZ8,8808
588
592
  reconcile/utils/cloud_resource_best_practice/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
589
593
  reconcile/utils/cloud_resource_best_practice/aws_rds.py,sha256=EvE6XKLsrZ531MJptKqPht2lOETrOjySTHXk6CzMgo0,2279
590
594
  reconcile/utils/glitchtip/__init__.py,sha256=FT6iBhGqoe7KExFdbgL8AYUb64iW_4snF5__Dcl7yt0,258
@@ -661,8 +665,8 @@ tools/test/test_app_interface_metrics_exporter.py,sha256=SX7qL3D1SIRKFo95FoQztvf
661
665
  tools/test/test_qontract_cli.py,sha256=d18KrdhtUGqoC7_kWZU128U0-VJEj-0rjFkLVufcI6I,2755
662
666
  tools/test/test_sd_app_sre_alert_report.py,sha256=v363r9zM7__0kR5K6mvJoGFcM9BvE33fWAayrqkpojA,2116
663
667
  tools/test/test_sre_checkpoints.py,sha256=SKqPPTl9ua0RFdSSofnoQX-JZE6dFLO3LRhfQzqtfh8,2607
664
- qontract_reconcile-0.10.1rc508.dist-info/METADATA,sha256=Dbs0FBqea-1B7hfU1DYUgteYYPDKOLLm9M49EKc_yDs,2349
665
- qontract_reconcile-0.10.1rc508.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
666
- qontract_reconcile-0.10.1rc508.dist-info/entry_points.txt,sha256=rTjAv28I_CHLM8ID3OPqMI_suoQ9s7tFbim4aYjn9kk,376
667
- qontract_reconcile-0.10.1rc508.dist-info/top_level.txt,sha256=l5ISPoXzt0SdR4jVdkfa7RPSKNc8zAHYWAnR-Dw8Ey8,24
668
- qontract_reconcile-0.10.1rc508.dist-info/RECORD,,
668
+ qontract_reconcile-0.10.1rc510.dist-info/METADATA,sha256=GZqpYG08olXHPFA1b6JcnDqPweC-vf7spJk2BXC6vZU,2349
669
+ qontract_reconcile-0.10.1rc510.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
670
+ qontract_reconcile-0.10.1rc510.dist-info/entry_points.txt,sha256=rTjAv28I_CHLM8ID3OPqMI_suoQ9s7tFbim4aYjn9kk,376
671
+ qontract_reconcile-0.10.1rc510.dist-info/top_level.txt,sha256=l5ISPoXzt0SdR4jVdkfa7RPSKNc8zAHYWAnR-Dw8Ey8,24
672
+ qontract_reconcile-0.10.1rc510.dist-info/RECORD,,
@@ -159,6 +159,7 @@ def run(dry_run, thread_pool_size=10, internal=None, use_jump_host=True, defer=N
159
159
  cluster_info
160
160
  for cluster_info in queries.get_clusters()
161
161
  if cluster_info.get("managedClusterRoles")
162
+ and cluster_info.get("automationToken")
162
163
  ]
163
164
  ri, oc_map = ob.fetch_current_state(
164
165
  clusters=clusters,
File without changes
@@ -0,0 +1,72 @@
1
+ from collections.abc import Callable
2
+ from typing import (
3
+ Any,
4
+ Optional,
5
+ Self,
6
+ )
7
+
8
+ import requests
9
+
10
+ from reconcile.gql_definitions.acs.acs_instances import AcsInstanceV1
11
+ from reconcile.gql_definitions.acs.acs_instances import query as acs_instances_query
12
+ from reconcile.utils.exceptions import AppInterfaceSettingsError
13
+
14
+
15
+ class AcsBaseApi:
16
+ def __init__(
17
+ self,
18
+ instance: Any,
19
+ timeout: int = 30,
20
+ ) -> None:
21
+ self.base_url = instance["url"]
22
+ self.token = instance["token"]
23
+ self.timeout = timeout
24
+ self.session = requests.Session()
25
+
26
+ def __enter__(self) -> Self:
27
+ return self
28
+
29
+ def __exit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> None:
30
+ self.session.close()
31
+
32
+ @staticmethod
33
+ def get_acs_instance(query_func: Callable) -> AcsInstanceV1:
34
+ """
35
+ Get an ACS instance
36
+
37
+ :param query_func: function which queries GQL Server
38
+ """
39
+ if instances := acs_instances_query(query_func=query_func).instances:
40
+ # mirroring logic for gitlab instances
41
+ # current assumption is for appsre to only utilize one instance
42
+ if len(instances) != 1:
43
+ raise AppInterfaceSettingsError("More than one ACS instance found!")
44
+ return instances[0]
45
+ raise AppInterfaceSettingsError("No ACS instance found!")
46
+
47
+ @staticmethod
48
+ def check_len_attributes(attrs: list[Any], api_data: Any) -> None:
49
+ # generic attribute check function for expected types with valid len()
50
+ for attr in attrs:
51
+ value = api_data.get(attr)
52
+ if value is None or len(value) == 0:
53
+ raise ValueError(
54
+ f"Attribute '{attr}' must exist and not be empty\n\t{api_data}"
55
+ )
56
+
57
+ def generic_request(
58
+ self, path: str, verb: str, json: Optional[Any] = None
59
+ ) -> requests.Response:
60
+ url = f"{self.base_url}{path}"
61
+ headers = {
62
+ "Authorization": f"Bearer {self.token}",
63
+ }
64
+ response = self.session.request(
65
+ verb,
66
+ url,
67
+ headers=headers,
68
+ json=json,
69
+ timeout=self.timeout,
70
+ )
71
+ response.raise_for_status()
72
+ return response
@@ -0,0 +1,151 @@
1
+ from typing import Any, Optional
2
+
3
+ from pydantic import BaseModel
4
+
5
+ from reconcile.utils.acs.base import AcsBaseApi
6
+
7
+
8
+ class Scope(BaseModel):
9
+ """
10
+ Scope represents a single cluster or namespace that an acs security policy is applied to.
11
+ """
12
+
13
+ cluster: str
14
+ namespace: Optional[str]
15
+
16
+
17
+ class PolicyCondition(BaseModel):
18
+ """
19
+ PolicyCondition represents a single condition criteria enforced within an ACS security policy.
20
+ Current attributes support subset of "build" lifecycle condition criteria.
21
+ See section 6.4.3 for table of all condition criteria:
22
+ https://access.redhat.com/documentation/en-us/red_hat_advanced_cluster_security_for_kubernetes/4.3/html/operating/manage-security-policies
23
+ """
24
+
25
+ field_name: str
26
+ negate: Optional[bool]
27
+ values: list[str]
28
+
29
+
30
+ class Policy(BaseModel):
31
+ """
32
+ Policy is minimum attributes required to represent an ACS security policy
33
+ https://access.redhat.com/documentation/en-us/red_hat_advanced_cluster_security_for_kubernetes/4.3/html/operating/manage-security-policies
34
+ """
35
+
36
+ name: str
37
+ description: str
38
+ notifiers: list[str]
39
+ categories: list[str]
40
+ severity: str
41
+ scope: list[Scope]
42
+ conditions: list[PolicyCondition]
43
+
44
+
45
+ class AcsPolicyApi(AcsBaseApi):
46
+ """
47
+ Implements methods to support reconcile operations against the ACS PolicyService api
48
+ """
49
+
50
+ def _build_policy(
51
+ self, api_policy: Any, conditions: list[PolicyCondition]
52
+ ) -> Policy:
53
+ return Policy(
54
+ name=api_policy["name"],
55
+ description=api_policy["description"],
56
+ notifiers=sorted(api_policy["notifiers"]),
57
+ categories=sorted(api_policy["categories"]),
58
+ severity=api_policy["severity"],
59
+ scope=sorted(
60
+ [
61
+ Scope(
62
+ cluster=s["cluster"],
63
+ namespace=s["namespace"],
64
+ )
65
+ for s in api_policy["scope"]
66
+ ],
67
+ key=lambda s: s.cluster,
68
+ ),
69
+ conditions=conditions,
70
+ )
71
+
72
+ def _build_policy_condition(self, api_policy_group: Any) -> PolicyCondition:
73
+ return PolicyCondition(
74
+ field_name=api_policy_group["fieldName"],
75
+ values=[v["value"] for v in api_policy_group["values"]],
76
+ negate=api_policy_group["negate"],
77
+ )
78
+
79
+ def list_custom_policies(self) -> list[Any]:
80
+ # retrieve summary data for each custom policy
81
+ return [
82
+ p
83
+ for p in self.generic_request("/v1/policies", "GET").json()["policies"]
84
+ if not p["isDefault"]
85
+ ]
86
+
87
+ def get_custom_policies(self) -> list[Policy]:
88
+ custom_policy_ids = [p["id"] for p in self.list_custom_policies()]
89
+ # make individual policy requests to obtain further details
90
+ custom_policies_api_result = [
91
+ self.generic_request(f"/v1/policies/{pid}", "GET").json()
92
+ for pid in custom_policy_ids
93
+ ]
94
+ return [
95
+ self._build_policy(
96
+ api_policy=cp,
97
+ conditions=[
98
+ self._build_policy_condition(group)
99
+ for section in cp["policySections"]
100
+ for group in section.get("policyGroups", [])
101
+ ],
102
+ )
103
+ for cp in custom_policies_api_result
104
+ ]
105
+
106
+ def create_or_update_policy(self, desired: Policy, id: str = "") -> None:
107
+ body = {
108
+ "name": desired.name,
109
+ "description": desired.description,
110
+ "categories": desired.categories,
111
+ "severity": desired.severity,
112
+ "notifiers": desired.notifiers,
113
+ "isDefault": False,
114
+ "disabled": False,
115
+ "scope": [
116
+ {"cluster": s.cluster, "namespace": s.namespace} for s in desired.scope
117
+ ],
118
+ "lifecycleStages": [
119
+ "BUILD"
120
+ ], # all currently supported policy criteria are classified as 'build' stage
121
+ "policySections": [
122
+ {
123
+ "sectionName": "primary",
124
+ "policyGroups": [
125
+ {
126
+ "fieldName": c.field_name,
127
+ "negate": c.negate,
128
+ "values": [{"value": v} for v in c.values],
129
+ }
130
+ for c in desired.conditions
131
+ ],
132
+ }
133
+ ],
134
+ }
135
+ if id:
136
+ self.generic_request(f"/v1/policies/{id}", "PUT", body)
137
+ else:
138
+ self.generic_request("/v1/policies", "POST", body)
139
+
140
+ def delete_policy(self, id: str) -> None:
141
+ self.generic_request(f"/v1/policies/{id}", "DELETE")
142
+
143
+ class NotifierIdentifiers(BaseModel):
144
+ id: str
145
+ name: str
146
+
147
+ def list_notifiers(self) -> list[NotifierIdentifiers]:
148
+ return [
149
+ self.NotifierIdentifiers(id=c["id"], name=c["name"])
150
+ for c in self.generic_request("/v1/notifiers", "GET").json()["notifiers"]
151
+ ]
@@ -0,0 +1,277 @@
1
+ from typing import Any
2
+
3
+ from pydantic import BaseModel
4
+
5
+ from reconcile.utils.acs.base import AcsBaseApi
6
+
7
+
8
+ class Role(BaseModel):
9
+ name: str
10
+ permission_set_id: str
11
+ access_scope_id: str
12
+ description: str
13
+ system_default: bool
14
+
15
+ def __init__(self, api_data: Any) -> None:
16
+ # attributes defined within stackrox(ACS) API for GET /v1/roles
17
+ AcsBaseApi.check_len_attributes(
18
+ ["name", "permissionSetId", "accessScopeId"],
19
+ api_data,
20
+ )
21
+
22
+ # traits is populated for system default resources and contains `origin: DEFAULT`
23
+ # this attribute is used to ignore such resources from reconciliation
24
+ traits = api_data.get("traits")
25
+ is_default = traits is not None and traits.get("origin") == "DEFAULT"
26
+
27
+ super().__init__(
28
+ name=api_data["name"],
29
+ permission_set_id=api_data["permissionSetId"],
30
+ access_scope_id=api_data["accessScopeId"],
31
+ description=api_data.get("description", ""),
32
+ system_default=is_default,
33
+ )
34
+
35
+
36
+ class Group(BaseModel):
37
+ id: str
38
+ role_name: str
39
+ auth_provider_id: str
40
+ key: str
41
+ value: str
42
+
43
+ def __init__(self, api_data: Any) -> None:
44
+ # attributes defined within stackrox(ACS) API for GET /v1/groups
45
+ AcsBaseApi.check_len_attributes(["roleName", "props"], api_data)
46
+ if api_data["roleName"] != "None":
47
+ AcsBaseApi.check_len_attributes(
48
+ ["id", "authProviderId", "key", "value"], api_data["props"]
49
+ )
50
+ else:
51
+ # it is valid for the default None group to contain empty key/value
52
+ AcsBaseApi.check_len_attributes(["id", "authProviderId"], api_data["props"])
53
+
54
+ super().__init__(
55
+ role_name=api_data["roleName"],
56
+ id=api_data["props"]["id"],
57
+ auth_provider_id=api_data["props"]["authProviderId"],
58
+ key=api_data["props"]["key"],
59
+ value=api_data["props"]["value"],
60
+ )
61
+
62
+
63
+ class AccessScope(BaseModel):
64
+ id: str
65
+ name: str
66
+ description: str
67
+ clusters: list[str]
68
+ namespaces: list[dict[str, str]]
69
+
70
+ def __init__(self, api_data: Any) -> None:
71
+ # attributes defined within stackrox(ACS) API for GET /v1/simpleaccessscopes/{id}
72
+ unrestricted = False
73
+ AcsBaseApi.check_len_attributes(["id", "name"], api_data)
74
+
75
+ # it is valid for the default Unrestricted access scope to have null 'rules'
76
+ unrestricted = api_data["name"] == "Unrestricted"
77
+ if not unrestricted:
78
+ AcsBaseApi.check_len_attributes(["rules"], api_data)
79
+
80
+ super().__init__(
81
+ id=api_data["id"],
82
+ name=api_data["name"],
83
+ clusters=[]
84
+ if unrestricted
85
+ else api_data["rules"].get("includedClusters", []),
86
+ namespaces=[]
87
+ if unrestricted
88
+ else api_data["rules"].get("includedNamespaces", []),
89
+ description=api_data.get("description", ""),
90
+ )
91
+
92
+
93
+ class PermissionSet(BaseModel):
94
+ id: str
95
+ name: str
96
+
97
+ def __init__(self, api_data: Any) -> None:
98
+ # attributes defined within stackrox(ACS) API for GET /v1/permissionsets/{id}
99
+ AcsBaseApi.check_len_attributes(["id", "name"], api_data)
100
+
101
+ super().__init__(id=api_data["id"], name=api_data["name"])
102
+
103
+
104
+ class RbacResources(BaseModel):
105
+ roles: list[Role]
106
+ access_scopes: list[AccessScope]
107
+ groups: list[Group]
108
+ permission_sets: list[PermissionSet]
109
+
110
+
111
+ class AcsRbacApi(AcsBaseApi):
112
+ def get_roles(self) -> list[Role]:
113
+ response = self.generic_request("/v1/roles", "GET")
114
+ return [Role(r) for r in response.json()["roles"]]
115
+
116
+ def create_role(
117
+ self, name: str, desc: str, permission_set_id: str, access_scope_id: str
118
+ ) -> None:
119
+ json = {
120
+ "name": name,
121
+ "description": desc,
122
+ "permissionSetId": permission_set_id,
123
+ "accessScopeId": access_scope_id,
124
+ }
125
+
126
+ self.generic_request(f"/v1/roles/{name}", "POST", json)
127
+
128
+ def update_role(
129
+ self, name: str, desc: str, permission_set_id: str, access_scope_id: str
130
+ ) -> None:
131
+ json = {
132
+ "name": name,
133
+ "description": desc,
134
+ "permissionSetId": permission_set_id,
135
+ "accessScopeId": access_scope_id,
136
+ }
137
+
138
+ self.generic_request(f"/v1/roles/{name}", "PUT", json)
139
+
140
+ def delete_role(self, name: str) -> None:
141
+ self.generic_request(f"/v1/roles/{name}", "DELETE")
142
+
143
+ def get_groups(self) -> list[Group]:
144
+ response = self.generic_request("/v1/groups", "GET")
145
+ return [Group(g) for g in response.json()["groups"]]
146
+
147
+ class GroupAdd(BaseModel):
148
+ role_name: str
149
+ key: str
150
+ value: str
151
+ auth_provider_id: str
152
+
153
+ def create_group_batch(self, additions: list[GroupAdd]) -> None:
154
+ json = {
155
+ "previousGroups": [],
156
+ "requiredGroups": [
157
+ {
158
+ "roleName": group.role_name,
159
+ "props": {
160
+ "id": "",
161
+ "authProviderId": group.auth_provider_id,
162
+ "key": group.key,
163
+ "value": group.value,
164
+ },
165
+ }
166
+ for group in additions
167
+ ],
168
+ }
169
+
170
+ self.generic_request("/v1/groupsbatch", "POST", json)
171
+
172
+ def delete_group_batch(self, removals: list[Group]) -> None:
173
+ json = {
174
+ "previousGroups": [
175
+ {
176
+ "roleName": group.role_name,
177
+ "props": {
178
+ "id": group.id,
179
+ "authProviderId": group.auth_provider_id,
180
+ "key": group.key,
181
+ "value": group.value,
182
+ },
183
+ }
184
+ for group in removals
185
+ ],
186
+ "requiredGroups": [],
187
+ }
188
+
189
+ self.generic_request("/v1/groupsbatch", "POST", json)
190
+
191
+ def update_group_batch(self, old: list[Group], new: list[GroupAdd]) -> None:
192
+ json = {
193
+ "previousGroups": [
194
+ {
195
+ "roleName": o.role_name,
196
+ "props": {
197
+ "id": o.id,
198
+ "authProviderId": o.auth_provider_id,
199
+ "key": o.key,
200
+ "value": o.value,
201
+ },
202
+ }
203
+ for o in old
204
+ ],
205
+ "requiredGroups": [
206
+ {
207
+ "roleName": n.role_name,
208
+ "props": {
209
+ "id": "",
210
+ "authProviderId": n.auth_provider_id,
211
+ "key": n.key,
212
+ "value": n.value,
213
+ },
214
+ }
215
+ for n in new
216
+ ],
217
+ }
218
+ self.generic_request("/v1/groupsbatch", "POST", json)
219
+
220
+ def get_access_scopes(self) -> list[AccessScope]:
221
+ response = self.generic_request("/v1/simpleaccessscopes", "GET")
222
+ return [AccessScope(a) for a in response.json()["accessScopes"]]
223
+
224
+ def create_access_scope(
225
+ self,
226
+ name: str,
227
+ desc: str,
228
+ clusters: list[str],
229
+ namespaces: list[dict[str, str]],
230
+ ) -> str:
231
+ # response is the created access_scope id
232
+ json = {
233
+ "name": name,
234
+ "description": desc,
235
+ "rules": {
236
+ "includedClusters": clusters,
237
+ "includedNamespaces": namespaces,
238
+ },
239
+ }
240
+
241
+ response = self.generic_request("/v1/simpleaccessscopes", "POST", json)
242
+
243
+ return response.json()["id"]
244
+
245
+ def delete_access_scope(self, id: str) -> None:
246
+ self.generic_request(f"/v1/simpleaccessscopes/{id}", "DELETE")
247
+
248
+ def update_access_scope(
249
+ self,
250
+ id: str,
251
+ name: str,
252
+ desc: str,
253
+ clusters: list[str],
254
+ namespaces: list[dict[str, str]],
255
+ ) -> None:
256
+ json = {
257
+ "name": name,
258
+ "description": desc,
259
+ "rules": {
260
+ "includedClusters": clusters,
261
+ "includedNamespaces": namespaces,
262
+ },
263
+ }
264
+
265
+ self.generic_request(f"/v1/simpleaccessscopes/{id}", "PUT", json)
266
+
267
+ def get_permission_sets(self) -> list[PermissionSet]:
268
+ response = self.generic_request("/v1/permissionsets", "GET")
269
+ return [PermissionSet(p) for p in response.json()["permissionSets"]]
270
+
271
+ def get_rbac_resources(self) -> RbacResources:
272
+ return RbacResources(
273
+ roles=self.get_roles(),
274
+ access_scopes=self.get_access_scopes(),
275
+ groups=self.get_groups(),
276
+ permission_sets=self.get_permission_sets(),
277
+ )