qontract-reconcile 0.10.1rc796__py3-none-any.whl → 0.10.1rc798__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.1rc796
3
+ Version: 0.10.1rc798
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
@@ -10,7 +10,7 @@ reconcile/aws_iam_password_reset.py,sha256=NwErtrqgBiXr7eGCAHdtGGOx0S7-4JnSc29Ie
10
10
  reconcile/aws_support_cases_sos.py,sha256=Jk6_XjDeJSYxgRGqcEAOcynt9qJF2r5HPIPcSKmoBv8,2974
11
11
  reconcile/blackbox_exporter_endpoint_monitoring.py,sha256=W_VJagnsJR1v5oqjlI3RJJE0_nhtJ0m81RS8zWA5u5c,3538
12
12
  reconcile/checkpoint.py,sha256=R2WFXUXLTB4sWMi4GeA4eegsuf_1-Q4vH8M0Toh3Ij4,5036
13
- reconcile/cli.py,sha256=IQEXC-Fg-c-9h3j_9JewMYMqgSLkJKT8ChWAHXmtt3k,100436
13
+ reconcile/cli.py,sha256=-JkVDI5QMXiSmcxjTOy6YcmYM1JDN6uYYBH38bcM8OI,100465
14
14
  reconcile/closedbox_endpoint_monitoring_base.py,sha256=SMhkcQqprWvThrIJa3U_3uh5w1h-alleW1QnCJFY4Qw,4909
15
15
  reconcile/cluster_deployment_mapper.py,sha256=2Ah-nu-Mdig0pjuiZl_XLrmVAjYzFjORR3dMlCgkmw0,2352
16
16
  reconcile/dashdotdb_base.py,sha256=a5aPLVxyqPSbjdB0Ty-uliOtxwvEbbEljHJKxdK3-Zk,4813
@@ -349,7 +349,7 @@ reconcile/gql_definitions/slo_documents/slo_documents.py,sha256=pOrm9NXAonlo6Lxq
349
349
  reconcile/gql_definitions/status_board/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
350
350
  reconcile/gql_definitions/status_board/status_board.py,sha256=vHEzncabujkqbjJ-ibMYNJTODgTc4DMf4y6TW3I_7II,4700
351
351
  reconcile/gql_definitions/statuspage/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
352
- reconcile/gql_definitions/statuspage/statuspages.py,sha256=gxDb42H93nwtBg7oFRb6Gk9pbAZpsWk_y4Y0s3_g3nE,3520
352
+ reconcile/gql_definitions/statuspage/statuspages.py,sha256=z1Qh2s0Aqn4UoVxQ-hRBD1EluAMO7FUFn-WmSIPAnUk,3458
353
353
  reconcile/gql_definitions/templating/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
354
354
  reconcile/gql_definitions/templating/template_collection.py,sha256=lS0vzEKV2ZrzOqOEriqpy0yBgKjb2Ftrzgx6PIH46_4,3310
355
355
  reconcile/gql_definitions/templating/templates.py,sha256=ejAvQ13zfNMQTz3FWtRUic6dSvio3aAgBKEqt600hbk,2821
@@ -430,12 +430,6 @@ reconcile/skupper_network/integration.py,sha256=178Q9RSYuZ9NmrCK4jRMLMekrewUaaRd
430
430
  reconcile/skupper_network/models.py,sha256=DNTI7HZv-rqY42GIIxyRuvroHLvdH6rJerjIq9lj3RU,6663
431
431
  reconcile/skupper_network/reconciler.py,sha256=XS-1oKBr_1l3dYUAVqUH6gCHg1G5ZuOfY_7fgGVAiFA,9996
432
432
  reconcile/skupper_network/site_controller.py,sha256=A3K-62BjJ5HiFVydV0ouGoD1NwrO7XhAH15BHAcS9fk,1550
433
- reconcile/statuspage/__init__.py,sha256=o9vR6sp3ARDQFZrbCEShelTxjF1XgfLaElK_QVt_248,261
434
- reconcile/statuspage/atlassian.py,sha256=1W9wQyO0B0qcw5Zrke8h7Rv9xhpXLyMezM7N-5XE8Dg,13665
435
- reconcile/statuspage/integration.py,sha256=---tzyl381RddAkIhXb7n3ySjUhuX7FBBI152SYsRfk,3654
436
- reconcile/statuspage/page.py,sha256=cJH2sDA8jiAmSdaDitQqNjkyDq_UP2w3s7eauCi-yt4,3740
437
- reconcile/statuspage/state.py,sha256=HD9EOoKm_nEqCMLIwW809En3cq5VhyzKJPUbsh-bae8,1617
438
- reconcile/statuspage/status.py,sha256=mfRJ_tW7jM4_Vy_1cc8C0fKJEoA2GwrA3gJeV1KImAw,2834
439
433
  reconcile/templates/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
440
434
  reconcile/templates/aws_access_key_email.j2,sha256=2MUr1ERmyISzKgHqsWYLd-1Wbl-peUa-FsGUS-JLUFc,238
441
435
  reconcile/templates/email.yml.j2,sha256=OZgczNRgXPj2gVYTgwQyHAQrMGu7xp-e4W1rX19GcrU,690
@@ -788,8 +782,8 @@ tools/test/test_app_interface_metrics_exporter.py,sha256=SX7qL3D1SIRKFo95FoQztvf
788
782
  tools/test/test_qontract_cli.py,sha256=w2l4BHB09k1d-BGJ1jBUNCqDv7zkqYrMHojQXg-21kQ,4155
789
783
  tools/test/test_sd_app_sre_alert_report.py,sha256=v363r9zM7__0kR5K6mvJoGFcM9BvE33fWAayrqkpojA,2116
790
784
  tools/test/test_sre_checkpoints.py,sha256=SKqPPTl9ua0RFdSSofnoQX-JZE6dFLO3LRhfQzqtfh8,2607
791
- qontract_reconcile-0.10.1rc796.dist-info/METADATA,sha256=GQarCBXsn_w6KRkt0BIseHFItIEHlKjlJUE4UqBfGdQ,2314
792
- qontract_reconcile-0.10.1rc796.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
793
- qontract_reconcile-0.10.1rc796.dist-info/entry_points.txt,sha256=rIxI5zWtHNlfpDeq1a7pZXAPoqf7HG32KMTN3MeWK_8,429
794
- qontract_reconcile-0.10.1rc796.dist-info/top_level.txt,sha256=l5ISPoXzt0SdR4jVdkfa7RPSKNc8zAHYWAnR-Dw8Ey8,24
795
- qontract_reconcile-0.10.1rc796.dist-info/RECORD,,
785
+ qontract_reconcile-0.10.1rc798.dist-info/METADATA,sha256=Q3RJK3iUjM4Z1ecb0Li6SoWTliTEbD7aKGQwHYRPilg,2314
786
+ qontract_reconcile-0.10.1rc798.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
787
+ qontract_reconcile-0.10.1rc798.dist-info/entry_points.txt,sha256=rIxI5zWtHNlfpDeq1a7pZXAPoqf7HG32KMTN3MeWK_8,429
788
+ qontract_reconcile-0.10.1rc798.dist-info/top_level.txt,sha256=l5ISPoXzt0SdR4jVdkfa7RPSKNc8zAHYWAnR-Dw8Ey8,24
789
+ qontract_reconcile-0.10.1rc798.dist-info/RECORD,,
reconcile/cli.py CHANGED
@@ -3177,7 +3177,9 @@ def gabi_authorized_users(ctx, thread_pool_size, internal, use_jump_host):
3177
3177
  )
3178
3178
  @click.pass_context
3179
3179
  def status_page_components(ctx):
3180
- from reconcile.statuspage.integration import StatusPageComponentsIntegration
3180
+ from reconcile.statuspage.integrations.components import (
3181
+ StatusPageComponentsIntegration,
3182
+ )
3181
3183
 
3182
3184
  run_class_integration(StatusPageComponentsIntegration(), ctx.obj)
3183
3185
 
@@ -33,7 +33,6 @@ query StatusPages {
33
33
  name
34
34
  pageId
35
35
  apiUrl
36
- provider
37
36
  credentials {
38
37
  ...VaultSecret
39
38
  }
@@ -100,7 +99,6 @@ class StatusPageV1(ConfiguredBaseModel):
100
99
  name: str = Field(..., alias="name")
101
100
  page_id: str = Field(..., alias="pageId")
102
101
  api_url: str = Field(..., alias="apiUrl")
103
- provider: str = Field(..., alias="provider")
104
102
  credentials: VaultSecret = Field(..., alias="credentials")
105
103
  components: Optional[list[StatusPageComponentV1]] = Field(..., alias="components")
106
104
 
@@ -1,5 +0,0 @@
1
- from reconcile.statuspage import atlassian
2
- from reconcile.statuspage.page import register_provider
3
-
4
- # Status page providers are registered here to prevent all kinds of cyclic imports.
5
- register_provider(atlassian.PROVIDER_NAME, atlassian.init_provider_for_page)
@@ -1,372 +0,0 @@
1
- import logging
2
- import time
3
- from typing import (
4
- Any,
5
- Optional,
6
- Protocol,
7
- )
8
-
9
- import requests
10
- from pydantic import BaseModel
11
- from requests import Response
12
- from sretoolbox.utils import retry
13
-
14
- from reconcile.gql_definitions.statuspage.statuspages import StatusPageV1
15
- from reconcile.statuspage.page import (
16
- StatusComponent,
17
- StatusPage,
18
- StatusPageProvider,
19
- )
20
- from reconcile.statuspage.state import ComponentBindingState
21
- from reconcile.statuspage.status import ManualStatusProvider
22
-
23
- PROVIDER_NAME = "atlassian"
24
-
25
-
26
- class AtlassianRawComponent(BaseModel):
27
- """
28
- atlassian status page REST schema for component
29
- """
30
-
31
- id: str
32
- name: str
33
- description: Optional[str]
34
- position: int
35
- status: str
36
- automation_email: Optional[str]
37
- group_id: Optional[str]
38
- group: Optional[bool]
39
-
40
-
41
- class AtlassianAPI(Protocol):
42
- def list_components(self) -> list[AtlassianRawComponent]: ...
43
-
44
- def update_component(self, id: str, data: dict[str, Any]) -> None: ...
45
-
46
- def create_component(self, data: dict[str, Any]) -> str: ...
47
-
48
- def delete_component(self, id: str) -> None: ...
49
-
50
-
51
- class AtlassianRESTAPI:
52
- """
53
- This API class wraps the statuspageio REST API for basic component operations.
54
- """
55
-
56
- def __init__(self, page_id: str, api_url: str, token: str):
57
- self.page_id = page_id
58
- self.api_url = api_url
59
- self.token = token
60
- self.auth_headers = {"Authorization": f"OAuth {self.token}"}
61
-
62
- @retry(max_attempts=10)
63
- def _do_get(self, url: str, params: dict[str, Any]) -> Response:
64
- response = requests.get(
65
- url, params=params, headers=self.auth_headers, timeout=30
66
- )
67
- response.raise_for_status()
68
- return response
69
-
70
- def list_components(self) -> list[AtlassianRawComponent]:
71
- url = f"{self.api_url}/v1/pages/{self.page_id}/components"
72
- all_components: list[AtlassianRawComponent] = []
73
- page = 1
74
- per_page = 100
75
- while True:
76
- params = {"page": page, "per_page": per_page}
77
- response = self._do_get(url, params=params)
78
- components = [AtlassianRawComponent(**c) for c in response.json()]
79
- all_components += components
80
- if len(components) < per_page:
81
- break
82
- page += 1
83
- # https://developer.statuspage.io/#section/Rate-Limiting
84
- # Each API token is limited to 1 request / second as measured on a 60 second rolling window
85
- time.sleep(1)
86
-
87
- return all_components
88
-
89
- def update_component(self, id: str, data: dict[str, Any]) -> None:
90
- url = f"{self.api_url}/v1/pages/{self.page_id}/components/{id}"
91
- requests.patch(
92
- url, json={"component": data}, headers=self.auth_headers
93
- ).raise_for_status()
94
-
95
- def create_component(self, data: dict[str, Any]) -> str:
96
- url = f"{self.api_url}/v1/pages/{self.page_id}/components"
97
- response = requests.post(
98
- url, json={"component": data}, headers=self.auth_headers
99
- )
100
- response.raise_for_status()
101
- return response.json()["id"]
102
-
103
- def delete_component(self, id: str) -> None:
104
- url = f"{self.api_url}/v1/pages/{self.page_id}/components/{id}"
105
- requests.delete(url, headers=self.auth_headers).raise_for_status()
106
-
107
-
108
- class AtlassianStatusPageProvider(StatusPageProvider):
109
- """
110
- The provider implements CRUD operations for Atlassian status pages.
111
- It also takes care of a mixed set of components on a page, where some
112
- components are managed by app-interface and some are managed manually
113
- by various teams. The term `bound` used throughout the code refers to
114
- components that are managed by app-interface. The binding status is
115
- managed by the injected `ComponentBindingState` instance and persists
116
- the binding information (what app-interface component name is bound to
117
- what status page component id).
118
- """
119
-
120
- def __init__(
121
- self,
122
- page_name: str,
123
- api: AtlassianAPI,
124
- component_binding_state: ComponentBindingState,
125
- ):
126
- self.page_name = page_name
127
- self._api = api
128
- self._binding_state = component_binding_state
129
-
130
- # component cache
131
- self._components: list[AtlassianRawComponent] = []
132
- self._components_by_id: dict[str, AtlassianRawComponent] = {}
133
- self._components_by_displayname: dict[str, AtlassianRawComponent] = {}
134
- self._group_name_to_id: dict[str, str] = {}
135
- self._group_id_to_name: dict[str, str] = {}
136
- self._build_component_cache()
137
-
138
- def _build_component_cache(self):
139
- self._components = self._api.list_components()
140
- self._components_by_id = {c.id: c for c in self._components}
141
- self._components_by_displayname = {c.name: c for c in self._components}
142
- self._group_name_to_id = {g.name: g.id for g in self._components if g.group}
143
- self._group_id_to_name = {g.id: g.name for g in self._components if g.group}
144
-
145
- def get_component_by_id(self, id: str) -> Optional[StatusComponent]:
146
- raw = self.get_raw_component_by_id(id)
147
- if raw:
148
- return self._bound_raw_component_to_status_component(raw)
149
- return None
150
-
151
- def get_raw_component_by_id(self, id: str) -> Optional[AtlassianRawComponent]:
152
- return self._components_by_id.get(id)
153
-
154
- def get_current_page(self) -> StatusPage:
155
- """
156
- Builds a StatusPage instance from the current state of the page. This
157
- way the current state of the page can be compared to the desired state
158
- of the page coming from GQL.
159
- """
160
- components = [
161
- self._bound_raw_component_to_status_component(c) for c in self._components
162
- ]
163
- return StatusPage(
164
- name=self.page_name,
165
- components=[c for c in components if c is not None],
166
- )
167
-
168
- def _bound_raw_component_to_status_component(
169
- self, raw_component: AtlassianRawComponent
170
- ) -> Optional[StatusComponent]:
171
- bound_component_name = self._binding_state.get_name_for_component_id(
172
- raw_component.id
173
- )
174
- if bound_component_name:
175
- group_name = (
176
- self._group_id_to_name.get(raw_component.group_id)
177
- if raw_component.group_id
178
- else None
179
- )
180
- return StatusComponent(
181
- name=bound_component_name,
182
- display_name=raw_component.name,
183
- description=raw_component.description,
184
- group_name=group_name,
185
- status_provider_configs=[
186
- ManualStatusProvider(
187
- component_status=raw_component.status,
188
- )
189
- ],
190
- )
191
- return None
192
-
193
- def lookup_component(
194
- self, desired_component: StatusComponent
195
- ) -> tuple[Optional[AtlassianRawComponent], bool]:
196
- """
197
- Finds the component on the page that matches the desired component. This
198
- is either done explicitely by using binding information if available or
199
- by using the display name of the desired component to find a matching
200
- component on the page. This way, this provider offers adoption logic
201
- for existing components on the page that are not yes bound to app-interface.
202
- """
203
- component_id = self._binding_state.get_id_for_component_name(
204
- desired_component.name
205
- )
206
- component = None
207
- bound = True
208
- if component_id:
209
- component = self.get_raw_component_by_id(component_id)
210
-
211
- if component is None:
212
- bound = False
213
- # either the component name is not bound to an ID or for whatever
214
- # reason or the component is not found on the page anymore
215
- component = self._components_by_displayname.get(
216
- desired_component.display_name
217
- )
218
- if component and self._binding_state.get_name_for_component_id(
219
- component.id
220
- ):
221
- # this component is already bound to a different component
222
- # in app-interface. we are protecting this binding here by
223
- # not allowing this component to be found via display name
224
- component = None
225
-
226
- return component, bound
227
-
228
- def should_apply(
229
- self, desired: StatusComponent, current: Optional[AtlassianRawComponent]
230
- ) -> bool:
231
- """
232
- Verifies if the desired component should be applied to the status page
233
- when compared to the current state of the component on the page.
234
- """
235
- current_group_name = (
236
- self._group_id_to_name.get(current.group_id)
237
- if current and current.group_id
238
- else None
239
- )
240
-
241
- # check if group exists
242
- group_id = None
243
- if desired.group_name:
244
- group_id = self._group_name_to_id.get(desired.group_name, None)
245
- if not group_id:
246
- raise ValueError(
247
- f"Group {desired.group_name} referenced "
248
- f"by {desired.name} does not exist"
249
- )
250
-
251
- # Special handling if a component needs to be moved out of any grouping.
252
- # We would need to use the component_group endpoint but for not lets
253
- # ignore this situation.
254
- if current and current_group_name and not desired.group_name:
255
- raise ValueError(
256
- f"Remove grouping from the component "
257
- f"{desired.group_name} is currently unsupported"
258
- )
259
-
260
- # component status
261
- desired_component_status = desired.desired_component_status()
262
- status_update_required = desired_component_status is not None and (
263
- not current or desired_component_status != current.status
264
- )
265
-
266
- # shortcut execution if there is nothing to do
267
- update_required = (
268
- current is None
269
- or desired.display_name != current.name
270
- or desired.description != current.description
271
- or desired.group_name != current_group_name
272
- or status_update_required
273
- )
274
- return update_required
275
-
276
- def apply_component(self, dry_run: bool, desired: StatusComponent) -> None:
277
- current_component, bound = self.lookup_component(desired)
278
-
279
- # if the component is not yet bound to a statuspage component, bind it now
280
- if current_component and not bound:
281
- self._bind_component(
282
- dry_run=dry_run,
283
- component_name=desired.name,
284
- component_id=current_component.id,
285
- )
286
-
287
- # validte the component and check if the current state needs to be updated
288
- needs_update = self.should_apply(desired, current_component)
289
- if not needs_update:
290
- return
291
-
292
- # calculate update
293
- component_update = {
294
- "name": desired.display_name,
295
- "description": desired.description,
296
- }
297
-
298
- # resolve group
299
- group_id = (
300
- self._group_name_to_id.get(desired.group_name, None)
301
- if desired.group_name
302
- else None
303
- )
304
- if group_id:
305
- component_update["group_id"] = group_id
306
-
307
- # resolve status
308
- desired_component_status = desired.desired_component_status()
309
- if desired_component_status:
310
- component_update["status"] = desired_component_status
311
-
312
- if current_component:
313
- logging.info(f"update component {desired.name}: {component_update}")
314
- if not dry_run:
315
- self._api.update_component(current_component.id, component_update)
316
- else:
317
- logging.info(f"create component {desired.name}: {component_update}")
318
- if not dry_run:
319
- component_id = self._api.create_component(component_update)
320
- self._bind_component(
321
- dry_run=dry_run,
322
- component_name=desired.name,
323
- component_id=component_id,
324
- )
325
-
326
- def delete_component(self, dry_run: bool, component_name: str) -> None:
327
- component_id = self._binding_state.get_id_for_component_name(component_name)
328
- if component_id:
329
- if not dry_run:
330
- self._api.delete_component(component_id)
331
- self._binding_state.forget_component(component_name)
332
- self._build_component_cache()
333
- else:
334
- logging.warning(
335
- f"can't delete component {component_name} because it is not "
336
- f"bound to any component on page {self.page_name}"
337
- )
338
-
339
- def has_component_binding_for(self, component_name: str) -> bool:
340
- return self._binding_state.get_id_for_component_name(component_name) is not None
341
-
342
- def _bind_component(
343
- self,
344
- dry_run: bool,
345
- component_name: str,
346
- component_id: str,
347
- ) -> None:
348
- logging.info(
349
- f"bind component {component_name} to ID {component_id} "
350
- f"on page {self.page_name}"
351
- )
352
- if not dry_run:
353
- self._binding_state.bind_component(component_name, component_id)
354
-
355
-
356
- def init_provider_for_page(
357
- page: StatusPageV1,
358
- token: str,
359
- component_binding_state: ComponentBindingState,
360
- ) -> AtlassianStatusPageProvider:
361
- """
362
- Initializes the provider for atlassian status page.
363
- """
364
- return AtlassianStatusPageProvider(
365
- page_name=page.name,
366
- api=AtlassianRESTAPI(
367
- page_id=page.page_id,
368
- api_url=page.api_url,
369
- token=token,
370
- ),
371
- component_binding_state=component_binding_state,
372
- )
@@ -1,110 +0,0 @@
1
- import logging
2
- import sys
3
- from collections.abc import Callable
4
-
5
- from reconcile.gql_definitions.statuspage import statuspages
6
- from reconcile.gql_definitions.statuspage.statuspages import StatusPageV1
7
- from reconcile.statuspage.page import (
8
- StatusPage,
9
- StatusPageProvider,
10
- build_status_page,
11
- init_provider_for_page,
12
- )
13
- from reconcile.statuspage.state import S3ComponentBindingState
14
- from reconcile.typed_queries.app_interface_vault_settings import (
15
- get_app_interface_vault_settings,
16
- )
17
- from reconcile.utils import gql
18
- from reconcile.utils.runtime.integration import (
19
- NoParams,
20
- QontractReconcileIntegration,
21
- )
22
- from reconcile.utils.secret_reader import (
23
- SecretReaderBase,
24
- create_secret_reader,
25
- )
26
- from reconcile.utils.semver_helper import make_semver
27
- from reconcile.utils.state import (
28
- State,
29
- init_state,
30
- )
31
-
32
- QONTRACT_INTEGRATION = "status-page-components"
33
- QONTRACT_INTEGRATION_VERSION = make_semver(0, 1, 0)
34
-
35
-
36
- def get_status_pages(query_func: Callable) -> list[StatusPageV1]:
37
- return statuspages.query(query_func).status_pages or []
38
-
39
-
40
- def get_state(secret_reader: SecretReaderBase) -> State:
41
- return init_state(
42
- integration=QONTRACT_INTEGRATION,
43
- secret_reader=secret_reader,
44
- )
45
-
46
-
47
- class StatusPageComponentsIntegration(QontractReconcileIntegration[NoParams]):
48
- def __init__(self) -> None:
49
- super().__init__(NoParams())
50
-
51
- @property
52
- def name(self) -> str:
53
- return QONTRACT_INTEGRATION
54
-
55
- def reconcile(
56
- self,
57
- dry_run: bool,
58
- desired_state: StatusPage,
59
- current_state: StatusPage,
60
- provider: StatusPageProvider,
61
- ) -> None:
62
- """
63
- Reconcile the desired state with the current state of a status page.
64
- """
65
- #
66
- # D E L E T E
67
- #
68
- desired_component_names = {c.name for c in desired_state.components}
69
- current_component_names = {c.name for c in current_state.components}
70
- component_names_to_delete = current_component_names - desired_component_names
71
- for component_name in component_names_to_delete:
72
- logging.info(
73
- f"delete component {component_name} from page {desired_state.name}"
74
- )
75
- provider.delete_component(dry_run, component_name)
76
-
77
- #
78
- # C R E A T E OR U P D A T E
79
- #
80
- for desired in desired_state.components:
81
- provider.apply_component(dry_run, desired)
82
-
83
- def run(self, dry_run: bool = False) -> None:
84
- vault_settings = get_app_interface_vault_settings()
85
- secret_reader = create_secret_reader(use_vault=vault_settings.vault)
86
- with get_state(secret_reader) as state:
87
- binding_state = S3ComponentBindingState(state)
88
- pages = get_status_pages(query_func=gql.get_api().query)
89
-
90
- error = False
91
- for p in pages:
92
- try:
93
- desired_state = build_status_page(p)
94
- page_provider = init_provider_for_page(
95
- page=p,
96
- token=secret_reader.read_secret(p.credentials),
97
- component_binding_state=binding_state,
98
- )
99
- self.reconcile(
100
- dry_run,
101
- desired_state=desired_state,
102
- current_state=page_provider.get_current_page(),
103
- provider=page_provider,
104
- )
105
- except Exception:
106
- logging.exception(f"failed to reconcile statuspage {p.name}")
107
- error = True
108
-
109
- if error:
110
- sys.exit(1)
@@ -1,136 +0,0 @@
1
- from abc import (
2
- ABC,
3
- abstractmethod,
4
- )
5
- from collections.abc import Callable
6
- from typing import Optional
7
-
8
- from pydantic import BaseModel
9
-
10
- from reconcile.gql_definitions.statuspage.statuspages import (
11
- StatusPageComponentV1,
12
- StatusPageV1,
13
- )
14
- from reconcile.statuspage.state import ComponentBindingState
15
- from reconcile.statuspage.status import (
16
- StatusProvider,
17
- build_status_provider_config,
18
- )
19
-
20
-
21
- def build_status_page_component(component: StatusPageComponentV1) -> "StatusComponent":
22
- status_configs = [
23
- build_status_provider_config(cfg) for cfg in component.status_config or []
24
- ]
25
- return StatusComponent(
26
- name=component.name,
27
- display_name=component.display_name,
28
- description=component.description,
29
- group_name=component.group_name,
30
- status_provider_configs=[c for c in status_configs if c is not None],
31
- )
32
-
33
-
34
- class StatusComponent(BaseModel):
35
- """
36
- Represents a status page component from the desired state.
37
- """
38
-
39
- name: str
40
- display_name: str
41
- description: Optional[str]
42
- group_name: Optional[str]
43
- status_provider_configs: list[StatusProvider]
44
- """
45
- Status provider configs hold different ways for a component to determine its status
46
- """
47
-
48
- def status_management_enabled(self) -> bool:
49
- """
50
- Determines if this component has any status configurations available for
51
- it to be able to manage its status.
52
- """
53
- return bool(self.status_provider_configs)
54
-
55
- def desired_component_status(self) -> Optional[str]:
56
- if self.status_management_enabled():
57
- for provider in self.status_provider_configs:
58
- status = provider.get_status()
59
- if status:
60
- return status
61
- return "operational"
62
- return None
63
-
64
- class Config:
65
- arbitrary_types_allowed = True
66
-
67
-
68
- class StatusPageProvider(ABC):
69
- """
70
- Provider specific status page reconcile implementation.
71
- """
72
-
73
- @abstractmethod
74
- def get_current_page(self) -> "StatusPage": ...
75
-
76
- @abstractmethod
77
- def apply_component(self, dry_run: bool, desired: StatusComponent) -> None: ...
78
-
79
- @abstractmethod
80
- def delete_component(self, dry_run: bool, component_name: str) -> None: ...
81
-
82
-
83
- def build_status_page(
84
- page: StatusPageV1,
85
- ) -> "StatusPage":
86
- """
87
- Translate a desired state status page into a status page object.
88
- """
89
- return StatusPage(
90
- name=page.name,
91
- components=[
92
- build_status_page_component(component=c) for c in page.components or []
93
- ],
94
- )
95
-
96
-
97
- def init_provider_for_page(
98
- page: StatusPageV1,
99
- token: str,
100
- component_binding_state: ComponentBindingState,
101
- ) -> StatusPageProvider:
102
- """
103
- Initialize a status page provider for a given status page.
104
- """
105
- if page.provider in _PROVIDERS:
106
- return _PROVIDERS[page.provider](page, token, component_binding_state)
107
- raise ValueError(f"provider {page.provider} is not supported")
108
-
109
-
110
- class StatusPage(BaseModel):
111
- """
112
- Represents the desired state of a status page and its components.
113
- """
114
-
115
- name: str
116
- """
117
- The name of the status page.
118
- """
119
-
120
- components: list[StatusComponent]
121
- """
122
- The desired components of the status page are represented in this list.
123
- Important note: the actual status page might have more components than
124
- this desired state does. People can still manage components manually.
125
- """
126
-
127
-
128
- ProviderInitializer = Callable[
129
- [StatusPageV1, str, ComponentBindingState], StatusPageProvider
130
- ]
131
-
132
- _PROVIDERS: dict[str, ProviderInitializer] = {}
133
-
134
-
135
- def register_provider(provider: str, provider_init: ProviderInitializer) -> None:
136
- _PROVIDERS[provider] = provider_init
@@ -1,47 +0,0 @@
1
- from typing import (
2
- Optional,
3
- Protocol,
4
- )
5
-
6
- from reconcile.utils.state import State
7
-
8
- # This module manages the binding state of components in the desired state
9
- # and their representation on an actual status page.
10
- #
11
- # This state management is required to map component identities between app-interface
12
- # and the status page provider.
13
-
14
-
15
- class ComponentBindingState(Protocol):
16
- def get_id_for_component_name(self, component_name: str) -> Optional[str]: ...
17
-
18
- def get_name_for_component_id(self, component_id: str) -> Optional[str]: ...
19
-
20
- def bind_component(self, component_name: str, component_id: str) -> None: ...
21
-
22
- def forget_component(self, component_name: str) -> None: ...
23
-
24
-
25
- class S3ComponentBindingState(ComponentBindingState):
26
- def __init__(self, state: State):
27
- self.state = state
28
- self._update_cache()
29
-
30
- def _update_cache(self) -> None:
31
- self.name_to_id_cache: dict[str, str] = self.state.get_all("")
32
- self.id_to_name_cache: dict[str, str] = {
33
- v: k for k, v in self.name_to_id_cache.items()
34
- }
35
-
36
- def get_id_for_component_name(self, component_name: str) -> Optional[str]:
37
- return self.name_to_id_cache.get(component_name)
38
-
39
- def get_name_for_component_id(self, component_id: str) -> Optional[str]:
40
- return self.id_to_name_cache.get(component_id)
41
-
42
- def bind_component(self, component_name: str, component_id: str) -> None:
43
- self.state.add(component_name, component_id, force=True)
44
- self._update_cache()
45
-
46
- def forget_component(self, component_name: str) -> None:
47
- self.state.rm(component_name)
@@ -1,97 +0,0 @@
1
- from abc import (
2
- ABC,
3
- abstractmethod,
4
- )
5
- from datetime import (
6
- datetime,
7
- timezone,
8
- )
9
- from typing import Optional
10
-
11
- from dateutil.parser import isoparse
12
- from pydantic import BaseModel
13
-
14
- from reconcile.gql_definitions.statuspage.statuspages import (
15
- ManualStatusProviderV1,
16
- StatusProviderV1,
17
- )
18
-
19
- # This module defines the interface for status providers for components on status
20
- # pages. A status provider is responsible for determining the status of a component.
21
- # This status will then be used to update the status page component.
22
-
23
-
24
- class StatusProvider(ABC):
25
- """
26
- The generic static provider interface that can be used to determine the
27
- status of a component.
28
- """
29
-
30
- @abstractmethod
31
- def get_status(self) -> Optional[str]: ...
32
-
33
-
34
- class ManualStatusProvider(StatusProvider, BaseModel):
35
- """
36
- This status provider is used to manually define the status of a component.
37
- An optional time windows can be defined in which the status is applied to
38
- the component.
39
- """
40
-
41
- start: Optional[datetime] = None
42
- """
43
- The optional start time of the time window in which the manually
44
- defined status is active.
45
- """
46
-
47
- end: Optional[datetime] = None
48
- """
49
- The optional end time of the time window in which the manually
50
- defined status is active.
51
- """
52
-
53
- component_status: str
54
- """
55
- The status to be used for the component if the
56
- time window is active or if no time window is defined.
57
- """
58
-
59
- def get_status(self) -> Optional[str]:
60
- if self._is_active():
61
- return self.component_status
62
- return None
63
-
64
- def _is_active(self) -> bool:
65
- """
66
- Returns true if the config is active in regards to the start and
67
- end time window. If no time window is defined, the config is always
68
- active.
69
- """
70
- if self.start and self.end and self.end < self.start:
71
- raise ValueError(
72
- "manual component status time window is invalid: end before start"
73
- )
74
- now = datetime.now(timezone.utc)
75
- if self.start and now < self.start:
76
- return False
77
- if self.end and self.end < now:
78
- return False
79
- return True
80
-
81
-
82
- def build_status_provider_config(
83
- cfg: StatusProviderV1,
84
- ) -> Optional[StatusProvider]:
85
- """
86
- Translates a status provider config from the desired state into
87
- provider specific implementation that provides the status resolution logic.
88
- """
89
- if isinstance(cfg, ManualStatusProviderV1):
90
- start = isoparse(cfg.manual.q_from) if cfg.manual.q_from else None
91
- end = isoparse(cfg.manual.until) if cfg.manual.until else None
92
- return ManualStatusProvider(
93
- component_status=cfg.manual.component_status,
94
- start=start,
95
- end=end,
96
- )
97
- return None