qontract-reconcile 0.10.2.dev28__py3-none-any.whl → 0.10.2.dev29__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.4
2
2
  Name: qontract-reconcile
3
- Version: 0.10.2.dev28
3
+ Version: 0.10.2.dev29
4
4
  Summary: Collection of tools to reconcile services with their desired state as defined in the app-interface DB.
5
5
  Project-URL: homepage, https://github.com/app-sre/qontract-reconcile
6
6
  Project-URL: repository, https://github.com/app-sre/qontract-reconcile
@@ -225,6 +225,9 @@ reconcile/gql_definitions/advanced_upgrade_service/aus_clusters.py,sha256=RpOrRY
225
225
  reconcile/gql_definitions/advanced_upgrade_service/aus_organization.py,sha256=zU-WJ9CASV1Ok-1jUro6K426v3ug5YNR1XoXmV7SwQ8,3364
226
226
  reconcile/gql_definitions/app_interface_metrics_exporter/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
227
227
  reconcile/gql_definitions/app_interface_metrics_exporter/onboarding_status.py,sha256=uVEEqU6YYmKsNTo6EWlFnoVmqha2rvBDx-wiD64VmG0,1679
228
+ reconcile/gql_definitions/app_sre_tekton_access_revalidation/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
229
+ reconcile/gql_definitions/app_sre_tekton_access_revalidation/roles.py,sha256=8Y4NsS5T7tumDWxY5MuoV50MK2i-DsLYSpCRjb7KaLE,2353
230
+ reconcile/gql_definitions/app_sre_tekton_access_revalidation/users.py,sha256=XdVxBxiyTR6Cy939EHNw__0k7iWrZWlhrgS5DakST0I,2504
228
231
  reconcile/gql_definitions/aws_account_manager/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
229
232
  reconcile/gql_definitions/aws_account_manager/aws_accounts.py,sha256=JdqtE3gMpeodymPJST-aFVkYP_MO--_CcwjF070R5Cs,4883
230
233
  reconcile/gql_definitions/aws_ami_cleanup/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -272,7 +275,7 @@ reconcile/gql_definitions/common/ocm_env_telemeter.py,sha256=jW0Q9WazDQVOxh4u0LM
272
275
  reconcile/gql_definitions/common/ocm_environments.py,sha256=6-_4Bf6-wBWykNBxVAFYnDkgM8sSoATKdabakDR9ENs,2018
273
276
  reconcile/gql_definitions/common/pagerduty_instances.py,sha256=CqHMNyI0O1knfzLJoDMmsa5cbVEppng6ae4OMYsvJFQ,2059
274
277
  reconcile/gql_definitions/common/pgp_reencryption_settings.py,sha256=NPLmO6J-zSu5B9QiYbDezLHY3TuOO9ihRBV-Zr84R9w,2259
275
- reconcile/gql_definitions/common/pipeline_providers.py,sha256=JJgmmghqLIwjKOdcWYHPnf4PDgAq4GF7046i0ozrqgI,9127
278
+ reconcile/gql_definitions/common/pipeline_providers.py,sha256=9rpsqPuvj82B4ki56xHlBde0yvGFOdXMH0RDQkBRVx8,9394
276
279
  reconcile/gql_definitions/common/quay_instances.py,sha256=toBkdYYVTmEafezAHZKgaW-mQ29xEW6jeronzsAlNyI,1786
277
280
  reconcile/gql_definitions/common/quay_orgs.py,sha256=NhA8kqvVUDbrsryEvEL5mlIv5R3T4XNhSRXtfL_yptY,1788
278
281
  reconcile/gql_definitions/common/reserved_networks.py,sha256=yP9qSQCaSQcva-ZgTnZp09qH27ur5_qK080ToIs04MY,2560
@@ -681,15 +684,17 @@ reconcile/utils/merge_request_manager/parser.py,sha256=5pGoz8Q6EuYXlUc1z-D0FahdR
681
684
  reconcile/utils/mr/README.md,sha256=i9sCLkDFhSxAUtpa_I1_TxhR5vPOLcowuwn2VEWO41w,5794
682
685
  reconcile/utils/mr/__init__.py,sha256=hcfHDIIIsJT4C0BnzDnyeZEfZdamrqHzMLcBzIT1ibI,2578
683
686
  reconcile/utils/mr/app_interface_reporter.py,sha256=6Kpg93V9FvcOke9Jimkva359MQ-ZyBIkUpf8QIA6-to,1793
687
+ reconcile/utils/mr/app_sre_tekton_access_report.py,sha256=zSO_-d_5KA-wcb0uAx4WWIj_LjIKqozHS-I2leTOzRU,1508
684
688
  reconcile/utils/mr/aws_access.py,sha256=9MMpYD24j2lLr_hLeMSh_OsJ07waalrlNpz-JlOsKAM,2575
685
689
  reconcile/utils/mr/base.py,sha256=O8BWr6dibeQ22FDE9y56r6DK3UnC-5IhRXT7IWGrnxk,8069
686
690
  reconcile/utils/mr/clusters_updates.py,sha256=pcusPAwRUkvyk_-bixsRNTzSvpTLypJ1kflq5UEVgcM,2271
687
- reconcile/utils/mr/glitchtip_access_reporter.py,sha256=i5vo9jjBifX5wIcLwEMH5_VFVg-NY-pBKe0HysSw4CQ,5031
691
+ reconcile/utils/mr/glitchtip_access_reporter.py,sha256=cTkOtzdgeKPaqro0VS2hDuAClQiN4nZATh-mplQC-AI,1369
688
692
  reconcile/utils/mr/labels.py,sha256=9QRTRjZAtq45zELd9SwavaraczMjwjn5no3RK1YxFTg,825
689
693
  reconcile/utils/mr/notificator.py,sha256=f8IcGQ1_iBsXJFnhPsWQ7UE3NfigaOrXcVieJPplYrY,2955
690
694
  reconcile/utils/mr/ocm_update_recommended_version.py,sha256=p_aVP0TGrlKk9WBwgQnYWqUDsED_Hg6G5Bqj0UvtRwA,1536
691
695
  reconcile/utils/mr/ocm_upgrade_scheduler_org_updates.py,sha256=5EncHGr4QRnZgHedRfCwMYZ9CaijYzHGj7-M6lhtQRo,3004
692
696
  reconcile/utils/mr/promote_qontract.py,sha256=wgvX2CBlcZaihKJSXJ0zcEK8NGaEP2_DUQDz0STzGes,7158
697
+ reconcile/utils/mr/update_access_report_base.py,sha256=0vhF-eZTIjl7keBAOb2bO7LrlRAiticuUGh5EmI5MWc,4357
693
698
  reconcile/utils/mr/user_maintenance.py,sha256=ZlR1Id_r2BUXsoerJW-0Ioh5bcbwlnQxBBhSs-ri9Dk,5099
694
699
  reconcile/utils/ocm/__init__.py,sha256=Y-bp8GomMpyCo0tFW6kJ78-ZG1UIupYRtBzbMWU0kwM,798
695
700
  reconcile/utils/ocm/addons.py,sha256=_LDdJ-gapM3s5exKlIUt-MlXZTAUoHezbYBU0QmvfWQ,7335
@@ -737,9 +742,11 @@ reconcile/utils/unleash/server.py,sha256=907gDh9Ee8UxLqusnfpzE-7LUnttB38D4xhVJ0v
737
742
  tools/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
738
743
  tools/app_interface_metrics_exporter.py,sha256=f1qwTmQfEcs98uBVRyBa0k7GQXdiSwd7w1hDVjhdGcQ,2303
739
744
  tools/app_interface_reporter.py,sha256=gR2EgHmgSIxzK5xxDW1SduFU6OkPaf2LlAQjhV3NYIg,17623
740
- tools/glitchtip_access_reporter.py,sha256=oPBnk_YoDuljU3v0FaChzOwwnk4vap1xEE67QEjzdqs,2948
745
+ tools/app_sre_tekton_access_reporter.py,sha256=o9prLUgQpwO3msRWc2as1xT1y9OB3znkpgvLr0Ys8_M,3146
746
+ tools/app_sre_tekton_access_revalidation.py,sha256=66nHEaY-bIqxIhpcmwN8AvQZu6ZXenfkg4Fut0pVZRM,2726
747
+ tools/glitchtip_access_reporter.py,sha256=o01A6b88t3Wie6tj_tJWWVo2J01LxQ_a9giGm4UzEaU,2901
741
748
  tools/glitchtip_access_revalidation.py,sha256=8kbBJk04mkq28kWoRDDkfCGIF3GRg3pJrFAh1sW0dbk,2821
742
- tools/qontract_cli.py,sha256=T637u3EVpodta2SSIjMa-3doLqXci1AEDt3Bspng4mE,145561
749
+ tools/qontract_cli.py,sha256=76jUbYqgF_ViudSg4rAJCBCLrrQV5aR0nMSlZwH3MWU,147170
743
750
  tools/sd_app_sre_alert_report.py,sha256=jQpJdXVID68bSNtJNOGDh0-ei1CfEUS4Itr4MAaBNFA,5062
744
751
  tools/template_validation.py,sha256=qpKYaTgk0GOPGa2Ct5_5sKdwIHtCAKIBGzsMPuJU5fw,3371
745
752
  tools/cli_commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -766,7 +773,7 @@ tools/saas_promotion_state/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJ
766
773
  tools/saas_promotion_state/saas_promotion_state.py,sha256=UfwwRLS5Ya4_Nh1w5n1dvoYtchQvYE9yj1VANt2IKqI,3925
767
774
  tools/sre_checkpoints/__init__.py,sha256=CDaDaywJnmRCLyl_NCcvxi-Zc0hTi_3OdwKiFOyS39I,145
768
775
  tools/sre_checkpoints/util.py,sha256=zEDbGr18ZeHNQwW8pUsr2JRjuXIPz--WAGJxZo9sv_Y,894
769
- qontract_reconcile-0.10.2.dev28.dist-info/METADATA,sha256=b2MPIc5XBW0hxukARgJFyKClbJwKN80IL4K4BLeU_G4,24665
770
- qontract_reconcile-0.10.2.dev28.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
771
- qontract_reconcile-0.10.2.dev28.dist-info/entry_points.txt,sha256=JniHZPadNOILPyfSl0LF2YSp3Db7K2_W2CN7i9f3Gos,540
772
- qontract_reconcile-0.10.2.dev28.dist-info/RECORD,,
776
+ qontract_reconcile-0.10.2.dev29.dist-info/METADATA,sha256=a6JAveCgr6HyV2-o-zIa88NmQXGd8ljE9r-SChKfa6o,24665
777
+ qontract_reconcile-0.10.2.dev29.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
778
+ qontract_reconcile-0.10.2.dev29.dist-info/entry_points.txt,sha256=5i9l54La3vQrDLAdwDKQWC0iG4sV9RRfOb1BpvzOWLc,698
779
+ qontract_reconcile-0.10.2.dev29.dist-info/RECORD,,
@@ -1,6 +1,8 @@
1
1
  [console_scripts]
2
2
  app-interface-metrics-exporter = tools.app_interface_metrics_exporter:main
3
3
  app-interface-reporter = tools.app_interface_reporter:main
4
+ app-sre-tekton-access-reporter = tools.app_sre_tekton_access_reporter:main
5
+ app-sre-tekton-access-revalidation = tools.app_sre_tekton_access_revalidation:main
4
6
  glitchtip-access-reporter = tools.glitchtip_access_reporter:main
5
7
  glitchtip-access-revalidation = tools.glitchtip_access_revalidation:main
6
8
  qontract-cli = tools.qontract_cli:root
@@ -0,0 +1,86 @@
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
+
21
+ DEFINITION = """
22
+ query AppSRETektonAccessRevalidationRoles {
23
+ roles: roles_v1 {
24
+ path
25
+ access {
26
+ role
27
+ namespace {
28
+ path
29
+ }
30
+ }
31
+ users {
32
+ org_username
33
+ path
34
+ }
35
+ }
36
+ }
37
+ """
38
+
39
+
40
+ class ConfiguredBaseModel(BaseModel):
41
+ class Config:
42
+ smart_union=True
43
+ extra=Extra.forbid
44
+
45
+
46
+ class NamespaceV1(ConfiguredBaseModel):
47
+ path: str = Field(..., alias="path")
48
+
49
+
50
+ class AccessV1(ConfiguredBaseModel):
51
+ role: Optional[str] = Field(..., alias="role")
52
+ namespace: Optional[NamespaceV1] = Field(..., alias="namespace")
53
+
54
+
55
+ class UserV1(ConfiguredBaseModel):
56
+ org_username: str = Field(..., alias="org_username")
57
+ path: str = Field(..., alias="path")
58
+
59
+
60
+ class RoleV1(ConfiguredBaseModel):
61
+ path: str = Field(..., alias="path")
62
+ access: Optional[list[AccessV1]] = Field(..., alias="access")
63
+ users: list[UserV1] = Field(..., alias="users")
64
+
65
+
66
+ class AppSRETektonAccessRevalidationRolesQueryData(ConfiguredBaseModel):
67
+ roles: Optional[list[RoleV1]] = Field(..., alias="roles")
68
+
69
+
70
+ def query(query_func: Callable, **kwargs: Any) -> AppSRETektonAccessRevalidationRolesQueryData:
71
+ """
72
+ This is a convenience function which queries and parses the data into
73
+ concrete types. It should be compatible with most GQL clients.
74
+ You do not have to use it to consume the generated data classes.
75
+ Alternatively, you can also mime and alternate the behavior
76
+ of this function in the caller.
77
+
78
+ Parameters:
79
+ query_func (Callable): Function which queries your GQL Server
80
+ kwargs: optional arguments that will be passed to the query function
81
+
82
+ Returns:
83
+ AppSRETektonAccessRevalidationRolesQueryData: queried data parsed into generated classes
84
+ """
85
+ raw_data: dict[Any, Any] = query_func(DEFINITION, **kwargs)
86
+ return AppSRETektonAccessRevalidationRolesQueryData(**raw_data)
@@ -0,0 +1,92 @@
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
+
21
+ DEFINITION = """
22
+ query AppSRETektonAccessRevalidationUsers {
23
+ users: users_v1 {
24
+ name
25
+ org_username
26
+ roles {
27
+ access {
28
+ role
29
+ namespace {
30
+ name
31
+ cluster {
32
+ name
33
+ }
34
+ }
35
+ }
36
+ }
37
+ }
38
+ }
39
+ """
40
+
41
+
42
+ class ConfiguredBaseModel(BaseModel):
43
+ class Config:
44
+ smart_union=True
45
+ extra=Extra.forbid
46
+
47
+
48
+ class ClusterV1(ConfiguredBaseModel):
49
+ name: str = Field(..., alias="name")
50
+
51
+
52
+ class NamespaceV1(ConfiguredBaseModel):
53
+ name: str = Field(..., alias="name")
54
+ cluster: ClusterV1 = Field(..., alias="cluster")
55
+
56
+
57
+ class AccessV1(ConfiguredBaseModel):
58
+ role: Optional[str] = Field(..., alias="role")
59
+ namespace: Optional[NamespaceV1] = Field(..., alias="namespace")
60
+
61
+
62
+ class RoleV1(ConfiguredBaseModel):
63
+ access: Optional[list[AccessV1]] = Field(..., alias="access")
64
+
65
+
66
+ class UserV1(ConfiguredBaseModel):
67
+ name: str = Field(..., alias="name")
68
+ org_username: str = Field(..., alias="org_username")
69
+ roles: Optional[list[RoleV1]] = Field(..., alias="roles")
70
+
71
+
72
+ class AppSRETektonAccessRevalidationUsersQueryData(ConfiguredBaseModel):
73
+ users: Optional[list[UserV1]] = Field(..., alias="users")
74
+
75
+
76
+ def query(query_func: Callable, **kwargs: Any) -> AppSRETektonAccessRevalidationUsersQueryData:
77
+ """
78
+ This is a convenience function which queries and parses the data into
79
+ concrete types. It should be compatible with most GQL clients.
80
+ You do not have to use it to consume the generated data classes.
81
+ Alternatively, you can also mime and alternate the behavior
82
+ of this function in the caller.
83
+
84
+ Parameters:
85
+ query_func (Callable): Function which queries your GQL Server
86
+ kwargs: optional arguments that will be passed to the query function
87
+
88
+ Returns:
89
+ AppSRETektonAccessRevalidationUsersQueryData: queried data parsed into generated classes
90
+ """
91
+ raw_data: dict[Any, Any] = query_func(DEFINITION, **kwargs)
92
+ return AppSRETektonAccessRevalidationUsersQueryData(**raw_data)
@@ -95,7 +95,12 @@ query PipelineProviders {
95
95
  }
96
96
  namespace {
97
97
  name
98
+ path
98
99
  clusterAdmin
100
+ app {
101
+ name
102
+ path
103
+ }
99
104
  cluster {
100
105
  name
101
106
  serverUrl
@@ -193,6 +198,11 @@ class PipelinesProviderTektonProviderDefaultsV1(ConfiguredBaseModel):
193
198
  deploy_resources: Optional[DeployResourcesV1] = Field(..., alias="deployResources")
194
199
 
195
200
 
201
+ class AppV1(ConfiguredBaseModel):
202
+ name: str = Field(..., alias="name")
203
+ path: str = Field(..., alias="path")
204
+
205
+
196
206
  class DisableClusterAutomationsV1(ConfiguredBaseModel):
197
207
  integrations: Optional[list[str]] = Field(..., alias="integrations")
198
208
 
@@ -210,7 +220,9 @@ class ClusterV1(ConfiguredBaseModel):
210
220
 
211
221
  class NamespaceV1(ConfiguredBaseModel):
212
222
  name: str = Field(..., alias="name")
223
+ path: str = Field(..., alias="path")
213
224
  cluster_admin: Optional[bool] = Field(..., alias="clusterAdmin")
225
+ app: AppV1 = Field(..., alias="app")
214
226
  cluster: ClusterV1 = Field(..., alias="cluster")
215
227
 
216
228
 
@@ -0,0 +1,45 @@
1
+ from pydantic import BaseModel
2
+
3
+ from reconcile.utils.mr.update_access_report_base import UpdateAccessReportBase
4
+
5
+
6
+ class UpdateAppSRETektonAccessReport(UpdateAccessReportBase):
7
+ name = "app_sre_tekton_access_report_mr"
8
+ short_description = "AppSRE Tekton Access Report"
9
+ template = """
10
+ | Username | Name | Apps with pipeline namespace access |
11
+ | -------- | ---- | ----------------------------------- |
12
+ {% for user in users|sort -%}
13
+ | {{ user.org_username }} | {{ user.name }} |{% for app in user.apps %} {{app}}{% if not loop.last%},{% endif %}{% endfor %} |
14
+ {% endfor %}
15
+ """.strip()
16
+
17
+
18
+ # User model
19
+ class AppSRETektonAccessReportUserModel(BaseModel):
20
+ org_username: str
21
+ name: str
22
+ apps: set[str]
23
+
24
+ def __lt__(self, other: object) -> bool:
25
+ if not isinstance(other, AppSRETektonAccessReportUserModel):
26
+ raise NotImplementedError(
27
+ "Cannot compare to non AppSRETektonAccessReportUser objects."
28
+ )
29
+ return self.org_username < other.org_username
30
+
31
+
32
+ # Mutable User class
33
+ class AppSRETektonAccessReportUser:
34
+ def __init__(self, name: str, org_username: str, apps: set):
35
+ self._org_username = org_username
36
+ self._name = name
37
+ self._apps = apps
38
+
39
+ def add_app(self, app: str) -> None:
40
+ self._apps.add(app)
41
+
42
+ def generate_model(self) -> AppSRETektonAccessReportUserModel:
43
+ return AppSRETektonAccessReportUserModel(
44
+ name=self._name, org_username=self._org_username, apps=self._apps
45
+ )
@@ -1,17 +1,12 @@
1
- import logging
2
- from collections.abc import Sequence
3
- from datetime import UTC, date
4
- from datetime import datetime as dt
5
- from pathlib import Path
6
-
7
- from jinja2 import Template
8
1
  from pydantic import BaseModel
9
2
 
10
- from reconcile.utils.gitlab_api import GitLabApi
11
- from reconcile.utils.mr.base import MergeRequestBase
12
- from reconcile.utils.mr.labels import AUTO_MERGE
3
+ from reconcile.utils.mr.update_access_report_base import UpdateAccessReportBase
4
+
13
5
 
14
- CURRENT_USERS_TABLE_TEMPLATE = """
6
+ class UpdateGlitchtipAccessReport(UpdateAccessReportBase):
7
+ name = "glitchtip_access_report_mr"
8
+ short_description = "glitchtip access report"
9
+ template = """
15
10
  | Username | Name | Organizations and Access Levels |
16
11
  | -------- | ---- | ------------------------------- |
17
12
  {% for user in users|sort -%}
@@ -43,92 +38,3 @@ class GlitchtipAccessReportUser(BaseModel):
43
38
  "Cannot compare to non GlitchtipAccessReportUser objects."
44
39
  )
45
40
  return self.username < other.username
46
-
47
-
48
- class UpdateGlitchtipAccessReport(MergeRequestBase):
49
- name = "glitchtip_access_report_mr"
50
-
51
- def __init__(
52
- self,
53
- users: Sequence[GlitchtipAccessReportUser],
54
- glitchtip_access_revalidation_workbook: Path,
55
- dry_run: bool = True,
56
- ):
57
- super().__init__()
58
- self.labels = [AUTO_MERGE]
59
- self._users = users
60
- self._glitchtip_access_revalidation_workbook = str(
61
- glitchtip_access_revalidation_workbook
62
- )
63
- self._isodate = dt.now(tz=UTC).isoformat()
64
- self._dry_run = dry_run
65
-
66
- @property
67
- def title(self) -> str:
68
- return f"[{self.name}] reports for {self._isodate}"
69
-
70
- @property
71
- def description(self) -> str:
72
- return f"glitchtip access report for {self._isodate}"
73
-
74
- def _render_current_users_table(self) -> str:
75
- template = Template(CURRENT_USERS_TABLE_TEMPLATE, keep_trailing_newline=True)
76
- return template.render(users=self._users)
77
-
78
- def _render_tracking_table_row(self, old_number_of_users: int) -> str:
79
- # | Date Reviewed | Number of Current Users | +/- Red Hat Users |
80
- return f"| {date.today()} | {len(self._users)} | {len(self._users) - old_number_of_users} |\n"
81
-
82
- def _update_workbook(self, workbook_md: str) -> str:
83
- new_workbook_md = ""
84
- number_of_skipped_lines = 0
85
- skip = False
86
- for line in workbook_md.splitlines():
87
- if "<!-- current users table: start -->" in line:
88
- # do not copy the old current users table
89
- skip = True
90
- # insert the new table including the marker
91
- new_workbook_md += line + "\n"
92
- new_workbook_md += self._render_current_users_table()
93
- elif "<!-- current users table: end -->" in line:
94
- skip = False
95
- # insert the marker
96
- new_workbook_md += line + "\n"
97
- elif "<!-- tracking table: next row -->" in line:
98
- # insert the new row including the marker
99
- new_workbook_md += self._render_tracking_table_row(
100
- old_number_of_users=number_of_skipped_lines - 2
101
- if number_of_skipped_lines > 0
102
- else 0
103
- )
104
- new_workbook_md += line + "\n"
105
- elif not skip:
106
- new_workbook_md += line + "\n"
107
- else:
108
- # count the number of skipped current users table lines
109
- # this number minus the table header is the old number of users
110
- number_of_skipped_lines += 1
111
-
112
- return new_workbook_md
113
-
114
- def process(self, gitlab_cli: GitLabApi) -> None:
115
- workbook_md = gitlab_cli.project.files.get(
116
- file_path=self._glitchtip_access_revalidation_workbook, ref=self.branch
117
- )
118
- workbook_md = self._update_workbook(workbook_md.decode().decode("utf-8"))
119
-
120
- if not self._dry_run:
121
- logging.info(
122
- f"updating glitchtip access report: {self._glitchtip_access_revalidation_workbook}"
123
- )
124
- gitlab_cli.update_file(
125
- branch_name=self.branch,
126
- file_path=self._glitchtip_access_revalidation_workbook,
127
- commit_message="update glitchtip access report",
128
- content=workbook_md,
129
- )
130
- else:
131
- logging.info(
132
- f"dry-run: not updating glitchtip access report: {self._glitchtip_access_revalidation_workbook}"
133
- )
134
- logging.info(workbook_md)
@@ -0,0 +1,122 @@
1
+ import logging
2
+ from abc import abstractmethod
3
+ from collections.abc import Sequence
4
+ from datetime import UTC, date
5
+ from datetime import datetime as dt
6
+ from pathlib import Path
7
+ from typing import TypeVar
8
+
9
+ from jinja2 import Template
10
+ from pydantic import BaseModel
11
+
12
+ from reconcile.utils.gitlab_api import GitLabApi
13
+ from reconcile.utils.mr.base import MergeRequestBase
14
+ from reconcile.utils.mr.labels import AUTO_MERGE
15
+
16
+ AccessReportUser = TypeVar("AccessReportUser", bound=BaseModel)
17
+
18
+
19
+ class UpdateAccessReportBase(MergeRequestBase):
20
+ def __init__(
21
+ self,
22
+ users: Sequence[AccessReportUser],
23
+ workbook_path: Path,
24
+ dry_run: bool = True,
25
+ ):
26
+ super().__init__()
27
+ self.labels = [AUTO_MERGE]
28
+ self._users = users
29
+ self._workbook_file_name = str(workbook_path)
30
+ self._isodate = dt.now(tz=UTC).isoformat()
31
+ self._dry_run = dry_run
32
+
33
+ @property
34
+ @abstractmethod
35
+ def short_description(self) -> str:
36
+ """
37
+ Short Description of the Merge Request (without dates). It will be used to
38
+ build the Merge Request description as seen in the UI.
39
+
40
+ :return: Merge Request description as seen in the Gitlab Web UI without date.
41
+ :rtype: str
42
+ """
43
+
44
+ @property
45
+ @abstractmethod
46
+ def template(self) -> str:
47
+ """
48
+ Jinja2 template to generate the report main table.
49
+
50
+ :return: report jinja2 template.
51
+ :rtype: str
52
+ """
53
+
54
+ @property
55
+ def title(self) -> str:
56
+ return f"[{self.name}] reports for {self._isodate}"
57
+
58
+ @property
59
+ def description(self) -> str:
60
+ return f"{self.short_description} for {self._isodate}"
61
+
62
+ def _render_current_users_table(self) -> str:
63
+ template = Template(self.template, keep_trailing_newline=True)
64
+ return template.render(users=self._users)
65
+
66
+ def _render_tracking_table_row(self, old_number_of_users: int) -> str:
67
+ # | Date Reviewed | Number of Current Users | +/- Red Hat Users |
68
+ return f"| {date.today()} | {len(self._users)} | {len(self._users) - old_number_of_users} |\n"
69
+
70
+ def _update_workbook(self, workbook_md: str) -> str:
71
+ new_workbook_md = ""
72
+ number_of_skipped_lines = 0
73
+ skip = False
74
+ for line in workbook_md.splitlines():
75
+ if "<!-- current users table: start -->" in line:
76
+ # do not copy the old current users table
77
+ skip = True
78
+ # insert the new table including the marker
79
+ new_workbook_md += line + "\n"
80
+ new_workbook_md += self._render_current_users_table()
81
+ elif "<!-- current users table: end -->" in line:
82
+ skip = False
83
+ # insert the marker
84
+ new_workbook_md += line + "\n"
85
+ elif "<!-- tracking table: next row -->" in line:
86
+ # insert the new row including the marker
87
+ new_workbook_md += self._render_tracking_table_row(
88
+ old_number_of_users=number_of_skipped_lines - 2
89
+ if number_of_skipped_lines > 0
90
+ else 0
91
+ )
92
+ new_workbook_md += line + "\n"
93
+ elif not skip:
94
+ new_workbook_md += line + "\n"
95
+ else:
96
+ # count the number of skipped current users table lines
97
+ # this number minus the table header is the old number of users
98
+ number_of_skipped_lines += 1
99
+
100
+ return new_workbook_md
101
+
102
+ def process(self, gitlab_cli: GitLabApi) -> None:
103
+ workbook_md = gitlab_cli.project.files.get(
104
+ file_path=self._workbook_file_name, ref=self.branch
105
+ )
106
+ workbook_md = self._update_workbook(workbook_md.decode().decode("utf-8"))
107
+
108
+ if not self._dry_run:
109
+ logging.info(
110
+ f"updating {self.short_description}: {self._workbook_file_name}"
111
+ )
112
+ gitlab_cli.update_file(
113
+ branch_name=self.branch,
114
+ file_path=self._workbook_file_name,
115
+ commit_message=f"update {self.short_description}",
116
+ content=workbook_md,
117
+ )
118
+ else:
119
+ logging.info(
120
+ f"dry-run: not updating {self.short_description}: {self._workbook_file_name}"
121
+ )
122
+ logging.info(workbook_md)
@@ -0,0 +1,99 @@
1
+ import logging
2
+ from collections import defaultdict
3
+ from pathlib import Path
4
+
5
+ import click
6
+
7
+ from reconcile import mr_client_gateway
8
+ from reconcile.cli import (
9
+ config_file,
10
+ dry_run,
11
+ gitlab_project_id,
12
+ log_level,
13
+ )
14
+ from reconcile.gql_definitions.app_sre_tekton_access_revalidation.users import (
15
+ query as users_query,
16
+ )
17
+ from reconcile.typed_queries.tekton_pipeline_providers import (
18
+ get_tekton_pipeline_providers,
19
+ )
20
+ from reconcile.utils import gql
21
+ from reconcile.utils.mr.app_sre_tekton_access_report import (
22
+ AppSRETektonAccessReportUser,
23
+ UpdateAppSRETektonAccessReport,
24
+ )
25
+ from reconcile.utils.runtime.environment import init_env
26
+
27
+
28
+ @click.command()
29
+ @config_file
30
+ @dry_run
31
+ @log_level
32
+ @gitlab_project_id
33
+ @click.option(
34
+ "--workbook-path",
35
+ help="path to AppSRE Tekton access revalidation workbook markdown file",
36
+ default="docs/app-sre/tekton/access-revalidation-workbook.md",
37
+ )
38
+ def main(
39
+ configfile: str,
40
+ dry_run: bool,
41
+ log_level: str,
42
+ gitlab_project_id: int,
43
+ workbook_path: str,
44
+ ) -> None:
45
+ """Update AppSRE Tekton access report.
46
+
47
+ This script updates the AppSRE Tekton access report (markdown file) with the latest
48
+ access information.
49
+ """
50
+
51
+ init_env(log_level=log_level, config_file=configfile)
52
+
53
+ # pipeline providers namespaces dict, containing the all the pipelines namespaces
54
+ # the (cluster_name, namespace_name) tuple to app_name correspondence.
55
+ pp_namespaces_apps = {
56
+ (p.namespace.cluster.name, p.namespace.name): p.namespace.app.name
57
+ for p in get_tekton_pipeline_providers()
58
+ }
59
+
60
+ report_users: dict[str, AppSRETektonAccessReportUser] = {}
61
+ users = users_query(query_func=gql.get_api().query).users or []
62
+ for u in users:
63
+ namespace_roles = defaultdict(set)
64
+ for r in u.roles or []:
65
+ if r.access is None:
66
+ continue
67
+
68
+ for a in r.access:
69
+ if a.namespace is None:
70
+ continue
71
+
72
+ namespace_tuple = (a.namespace.cluster.name, a.namespace.name)
73
+ namespace_roles[namespace_tuple].add(a.role)
74
+
75
+ for namespace_tuple, roles in namespace_roles.items():
76
+ if pp_app := pp_namespaces_apps.get(namespace_tuple):
77
+ if "tekton-trigger-access" in roles or "view" in roles:
78
+ if ru := report_users.get(u.org_username):
79
+ ru.add_app(pp_app)
80
+ else:
81
+ report_users[u.org_username] = AppSRETektonAccessReportUser(
82
+ name=u.name, org_username=u.org_username, apps={pp_app}
83
+ )
84
+
85
+ mr = UpdateAppSRETektonAccessReport(
86
+ users=[u.generate_model() for u in report_users.values()],
87
+ workbook_path=Path(workbook_path),
88
+ dry_run=dry_run,
89
+ )
90
+ with mr_client_gateway.init(
91
+ gitlab_project_id=gitlab_project_id, sqs_or_gitlab="gitlab"
92
+ ) as mr_cli:
93
+ result = mr.submit(cli=mr_cli)
94
+ if result:
95
+ logging.info(["created_mr", result.web_url])
96
+
97
+
98
+ if __name__ == "__main__":
99
+ main() # pylint: disable=no-value-for-parameter
@@ -0,0 +1,90 @@
1
+ import logging
2
+ from pathlib import Path
3
+
4
+ import click
5
+
6
+ from reconcile import mr_client_gateway
7
+ from reconcile.cli import (
8
+ config_file,
9
+ dry_run,
10
+ gitlab_project_id,
11
+ log_level,
12
+ )
13
+ from reconcile.typed_queries.tekton_pipeline_providers import (
14
+ get_tekton_pipeline_providers,
15
+ )
16
+ from reconcile.utils.mr.labels import AUTO_MERGE
17
+ from reconcile.utils.mr.notificator import (
18
+ CreateAppInterfaceNotificator,
19
+ Notification,
20
+ )
21
+ from reconcile.utils.runtime.environment import init_env
22
+
23
+ EMAIL_BODY = """Hello app-interface service owner,
24
+
25
+ Access to all Tekton pipelines namespaces must be revalidated regularly. This ensures
26
+ that the access is still valid and is needed to safeguard against unauthorized access.
27
+
28
+ Please review, within one week, that all your app-interface roles that grant access to
29
+ those namespaces are assigned to the appropriate users. In order to help you identifying
30
+ those roles and users, please take a look into the app-interface documentation:
31
+ https://gitlab.cee.redhat.com/service/app-interface/-/blob/master/docs/app-sre/tekton/access-revalidation.md
32
+
33
+ If you have questions about this, please post a question in the #sd-app-sre Slack channel.
34
+
35
+ Thank you,
36
+
37
+ The AppSRE team
38
+ """
39
+
40
+
41
+ @click.command()
42
+ @config_file
43
+ @dry_run
44
+ @log_level
45
+ @gitlab_project_id
46
+ @click.option(
47
+ "--email-dir",
48
+ help="app-interface dir to store new AppSRE Tekton revalidation emails",
49
+ default="data/app-interface/emails/app-sre-tekton",
50
+ )
51
+ def main(
52
+ configfile: str,
53
+ dry_run: bool,
54
+ log_level: str,
55
+ gitlab_project_id: int,
56
+ email_dir: str,
57
+ ) -> None:
58
+ """Revalidate Glitchtip access.
59
+
60
+ This script sends an email (via MR) to all app-interface service owners (apps)
61
+ that have a pipelines provider associated to the application. The email asks the
62
+ service owners to revalidate the access to the pipelines providers namespaces.
63
+ """
64
+ init_env(log_level=log_level, config_file=configfile)
65
+
66
+ apps = {p.namespace.app.path for p in get_tekton_pipeline_providers()}
67
+ notification = Notification(
68
+ notification_type="Action Required",
69
+ short_description="AppSRE Tekton Access Revalidation",
70
+ description=EMAIL_BODY,
71
+ services=list(apps),
72
+ recipients=[],
73
+ )
74
+ mr = CreateAppInterfaceNotificator(
75
+ notification,
76
+ labels=[AUTO_MERGE],
77
+ email_base_path=Path(email_dir),
78
+ dry_run=dry_run,
79
+ )
80
+
81
+ with mr_client_gateway.init(
82
+ gitlab_project_id=gitlab_project_id, sqs_or_gitlab="gitlab"
83
+ ) as mr_cli:
84
+ result = mr.submit(cli=mr_cli)
85
+ if result:
86
+ logging.info(["created_mr", result.web_url])
87
+
88
+
89
+ if __name__ == "__main__":
90
+ main() # pylint: disable=no-value-for-parameter
@@ -77,9 +77,7 @@ def main(
77
77
 
78
78
  mr = UpdateGlitchtipAccessReport(
79
79
  users=list(users.values()),
80
- glitchtip_access_revalidation_workbook=Path(
81
- glitchtip_access_revalidation_workbook_path
82
- ),
80
+ workbook_path=Path(glitchtip_access_revalidation_workbook_path),
83
81
  dry_run=dry_run,
84
82
  )
85
83
  with mr_client_gateway.init(
tools/qontract_cli.py CHANGED
@@ -75,6 +75,9 @@ from reconcile.cli import (
75
75
  from reconcile.gql_definitions.advanced_upgrade_service.aus_clusters import (
76
76
  query as aus_clusters_query,
77
77
  )
78
+ from reconcile.gql_definitions.app_sre_tekton_access_revalidation.roles import (
79
+ query as app_sre_tekton_access_revalidation_roles_query,
80
+ )
78
81
  from reconcile.gql_definitions.common.app_interface_vault_settings import (
79
82
  AppInterfaceSettingsV1,
80
83
  )
@@ -96,6 +99,9 @@ from reconcile.typed_queries.clusters import get_clusters
96
99
  from reconcile.typed_queries.saas_files import get_saas_files
97
100
  from reconcile.typed_queries.slo_documents import get_slo_documents
98
101
  from reconcile.typed_queries.status_board import get_status_board
102
+ from reconcile.typed_queries.tekton_pipeline_providers import (
103
+ get_tekton_pipeline_providers,
104
+ )
99
105
  from reconcile.utils import (
100
106
  amtool,
101
107
  config,
@@ -4460,5 +4466,49 @@ You can view the source of this Markdown to extract the JSON data.
4460
4466
  print_output(ctx.obj["options"], results, columns)
4461
4467
 
4462
4468
 
4469
+ @get.command(help="Get all app tekton pipelines providers roles and users")
4470
+ @click.argument("app-name")
4471
+ @click.pass_context
4472
+ def tekton_roles_and_users(ctx, app_name):
4473
+ pp_namespaces = {
4474
+ p.namespace.path
4475
+ for p in get_tekton_pipeline_providers()
4476
+ if p.namespace.app.name == app_name
4477
+ }
4478
+
4479
+ roles = (
4480
+ app_sre_tekton_access_revalidation_roles_query(
4481
+ query_func=gql.get_api().query
4482
+ ).roles
4483
+ or []
4484
+ )
4485
+ columns = ["namespace_path", "role_path", "users"]
4486
+ results = []
4487
+ for r in roles:
4488
+ if r.access is None:
4489
+ continue
4490
+
4491
+ seen = False # to avoid printing a namespace more than once
4492
+ for a in r.access:
4493
+ if a.namespace is None:
4494
+ continue
4495
+ if a.namespace.path in pp_namespaces:
4496
+ if not seen:
4497
+ seen = True
4498
+
4499
+ if ctx.obj["options"]["output"] == "table":
4500
+ users = ", ".join([u.org_username for u in r.users])
4501
+ else:
4502
+ users = [u.path for u in r.users]
4503
+
4504
+ results.append({
4505
+ "namespace_path": a.namespace.path,
4506
+ "role_path": r.path,
4507
+ "users": users,
4508
+ })
4509
+
4510
+ print_output(ctx.obj["options"], results, columns)
4511
+
4512
+
4463
4513
  if __name__ == "__main__":
4464
4514
  root() # pylint: disable=no-value-for-parameter