qontract-reconcile 0.10.2.dev226__py3-none-any.whl → 0.10.2.dev228__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.
- {qontract_reconcile-0.10.2.dev226.dist-info → qontract_reconcile-0.10.2.dev228.dist-info}/METADATA +1 -1
- {qontract_reconcile-0.10.2.dev226.dist-info → qontract_reconcile-0.10.2.dev228.dist-info}/RECORD +7 -7
- reconcile/openshift_saas_deploy_trigger_base.py +9 -1
- reconcile/status_board.py +285 -87
- reconcile/utils/ocm/status_board.py +83 -11
- {qontract_reconcile-0.10.2.dev226.dist-info → qontract_reconcile-0.10.2.dev228.dist-info}/WHEEL +0 -0
- {qontract_reconcile-0.10.2.dev226.dist-info → qontract_reconcile-0.10.2.dev228.dist-info}/entry_points.txt +0 -0
{qontract_reconcile-0.10.2.dev226.dist-info → qontract_reconcile-0.10.2.dev228.dist-info}/METADATA
RENAMED
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: qontract-reconcile
|
3
|
-
Version: 0.10.2.
|
3
|
+
Version: 0.10.2.dev228
|
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
|
{qontract_reconcile-0.10.2.dev226.dist-info → qontract_reconcile-0.10.2.dev228.dist-info}/RECORD
RENAMED
@@ -77,7 +77,7 @@ reconcile/openshift_rolebindings.py,sha256=9mlJ2FjWUoH-rsjtasreA_hV-K5Z_YR00qR_R
|
|
77
77
|
reconcile/openshift_routes.py,sha256=fXvuPSjcjVw1X3j2EQvUAdbOepmIFdKk-M3qP8QzPiw,1075
|
78
78
|
reconcile/openshift_saas_deploy.py,sha256=T1dvb9zajisaJNjbnR6-AZHU-itscHtr4oCqLj8KCK0,13037
|
79
79
|
reconcile/openshift_saas_deploy_change_tester.py,sha256=12uyBwaeMka1C3_pejmQPIBPAx2V1sJ4dJkScq-2e2M,8793
|
80
|
-
reconcile/openshift_saas_deploy_trigger_base.py,sha256=
|
80
|
+
reconcile/openshift_saas_deploy_trigger_base.py,sha256=fHCnlskcRKcitmIWWF3ePhKhQ0WlMeEaRDZM2IAW9BY,14649
|
81
81
|
reconcile/openshift_saas_deploy_trigger_cleaner.py,sha256=roLyVAVntaQptKaZbnN1LyLvCA8fyvqELfjU6M8xfeY,3511
|
82
82
|
reconcile/openshift_saas_deploy_trigger_configs.py,sha256=eUejMGWuaQabZTLuvPLLvROfN5HOFyYZOpH4YEsiU_g,928
|
83
83
|
reconcile/openshift_saas_deploy_trigger_images.py,sha256=iUsiBGJf-CyFw7tSLWo59rXmSvsVnN6TTaAObbsVpNg,936
|
@@ -108,7 +108,7 @@ reconcile/slack_base.py,sha256=I-msunWxfgu5bSwXYulGbtLjxUB_tRmTCAUCU-3nabI,3484
|
|
108
108
|
reconcile/slack_usergroups.py,sha256=xFkVe67RXSUj8JvpfSFEiRdQzB0TnJJEHW_b5PEwLng,30213
|
109
109
|
reconcile/sql_query.py,sha256=auZCWe6dytsDp83Imfo4zqkpMCLRXU007IUlPeUE3j4,26376
|
110
110
|
reconcile/status.py,sha256=cY4IJFXemhxptRJqR4qaaOWqei9e4jgLXuVSGajMsjg,544
|
111
|
-
reconcile/status_board.py,sha256=
|
111
|
+
reconcile/status_board.py,sha256=Hxx6sFFyPTNi6zY-qlAco9OqPvdx6Kbgce0DunDUsNQ,15838
|
112
112
|
reconcile/terraform_aws_route53.py,sha256=dQzzT46YhwRA902_H6pi-f7WlX4EaH187wXSdmJAUkQ,9958
|
113
113
|
reconcile/terraform_cloudflare_dns.py,sha256=-aLEe2QnH5cJPu7HWqs-R9NmQ1NlFbcVUm0v7alVL3I,13431
|
114
114
|
reconcile/terraform_cloudflare_resources.py,sha256=pq8Ieo5NmB-dYQ9X2F0s6iEoINMzhiqGw2yQK4ovok4,14980
|
@@ -740,7 +740,7 @@ reconcile/utils/ocm/products.py,sha256=Ki9o0VV4z_FsXQaJtSFzlUnxLvpk1H-RamvJpUwwb
|
|
740
740
|
reconcile/utils/ocm/search_filters.py,sha256=uUCJ-XOEp4D5uxPW7lDqNe6s-mQWLOCqMu9_xvO6PXU,14798
|
741
741
|
reconcile/utils/ocm/service_log.py,sha256=RG1f0MMn6joKaRCAm2xveSJCavdOPP1BVo9FXecDxaI,2018
|
742
742
|
reconcile/utils/ocm/sre_capability_labels.py,sha256=nqh0imrYczNeeeC7ZNX3pEwuAIVkKLTKZf0YHSPZYpE,1537
|
743
|
-
reconcile/utils/ocm/status_board.py,sha256=
|
743
|
+
reconcile/utils/ocm/status_board.py,sha256=ptPRzwP1qjmg1d_ZD4X050gB6W2XNnJs8p7hc06tmbw,4142
|
744
744
|
reconcile/utils/ocm/subscriptions.py,sha256=hehKXsDXIhnhqvWOuiYvx6y2FGq3zt0APGYj7WiBIdI,2765
|
745
745
|
reconcile/utils/ocm/syncsets.py,sha256=9IQm1l5BodOVZa2OFbQmow3afmh4nXe5pn-CCJ5LxTI,1169
|
746
746
|
reconcile/utils/ocm/upgrades.py,sha256=Hs3R9pkl5GoFgSyF7ZYhqG1gXOBCSGPL_ymDYAT3jXA,4804
|
@@ -803,7 +803,7 @@ tools/saas_promotion_state/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJ
|
|
803
803
|
tools/saas_promotion_state/saas_promotion_state.py,sha256=UfwwRLS5Ya4_Nh1w5n1dvoYtchQvYE9yj1VANt2IKqI,3925
|
804
804
|
tools/sre_checkpoints/__init__.py,sha256=CDaDaywJnmRCLyl_NCcvxi-Zc0hTi_3OdwKiFOyS39I,145
|
805
805
|
tools/sre_checkpoints/util.py,sha256=zEDbGr18ZeHNQwW8pUsr2JRjuXIPz--WAGJxZo9sv_Y,894
|
806
|
-
qontract_reconcile-0.10.2.
|
807
|
-
qontract_reconcile-0.10.2.
|
808
|
-
qontract_reconcile-0.10.2.
|
809
|
-
qontract_reconcile-0.10.2.
|
806
|
+
qontract_reconcile-0.10.2.dev228.dist-info/METADATA,sha256=5Vilv7XrctUYz2MggGRV5YpQQzRms_cQB_isU8a-HNE,24431
|
807
|
+
qontract_reconcile-0.10.2.dev228.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
808
|
+
qontract_reconcile-0.10.2.dev228.dist-info/entry_points.txt,sha256=5i9l54La3vQrDLAdwDKQWC0iG4sV9RRfOb1BpvzOWLc,698
|
809
|
+
qontract_reconcile-0.10.2.dev228.dist-info/RECORD,,
|
@@ -38,7 +38,7 @@ from reconcile.utils.saasherder import (
|
|
38
38
|
TriggerSpecUnion,
|
39
39
|
)
|
40
40
|
from reconcile.utils.saasherder.interfaces import SaasPipelinesProviderTekton
|
41
|
-
from reconcile.utils.saasherder.models import TriggerTypes
|
41
|
+
from reconcile.utils.saasherder.models import TriggerSpecConfig, TriggerTypes
|
42
42
|
from reconcile.utils.secret_reader import create_secret_reader
|
43
43
|
from reconcile.utils.sharding import is_in_shard
|
44
44
|
from reconcile.utils.state import init_state
|
@@ -267,6 +267,10 @@ def _trigger_tekton(
|
|
267
267
|
)
|
268
268
|
return False
|
269
269
|
|
270
|
+
target_ref = None
|
271
|
+
if isinstance(spec, TriggerSpecConfig):
|
272
|
+
target_ref = spec.target_ref
|
273
|
+
|
270
274
|
tkn_trigger_resource, tkn_name = _construct_tekton_trigger_resource(
|
271
275
|
spec.saas_file_name,
|
272
276
|
spec.env_name,
|
@@ -278,6 +282,7 @@ def _trigger_tekton(
|
|
278
282
|
integration_version,
|
279
283
|
saasherder.include_trigger_trace,
|
280
284
|
spec.reason,
|
285
|
+
target_ref,
|
281
286
|
)
|
282
287
|
|
283
288
|
error = False
|
@@ -334,6 +339,7 @@ def _construct_tekton_trigger_resource(
|
|
334
339
|
integration_version: str,
|
335
340
|
include_trigger_trace: bool,
|
336
341
|
reason: str | None,
|
342
|
+
target_ref: str | None,
|
337
343
|
) -> tuple[OR, str]:
|
338
344
|
"""Construct a resource (PipelineRun) to trigger a deployment via Tekton.
|
339
345
|
|
@@ -348,6 +354,7 @@ def _construct_tekton_trigger_resource(
|
|
348
354
|
integration_version (string): Version of calling integration
|
349
355
|
include_trigger_trace (bool): Should include traces of the triggering integration and reason
|
350
356
|
reason (string): The reason this trigger was created
|
357
|
+
target_ref (string): the SHA ref of the target
|
351
358
|
|
352
359
|
Returns:
|
353
360
|
OpenshiftResource: OpenShift resource to be applied
|
@@ -385,6 +392,7 @@ def _construct_tekton_trigger_resource(
|
|
385
392
|
"labels": {
|
386
393
|
"qontract.saas_file_name": saas_file_name,
|
387
394
|
"qontract.env_name": env_name,
|
395
|
+
"qontract.target_ref": target_ref or "",
|
388
396
|
},
|
389
397
|
},
|
390
398
|
"spec": {
|
reconcile/status_board.py
CHANGED
@@ -4,26 +4,36 @@ from abc import (
|
|
4
4
|
abstractmethod,
|
5
5
|
)
|
6
6
|
from collections.abc import Iterable, Mapping
|
7
|
+
from enum import Enum
|
7
8
|
from typing import (
|
8
|
-
Any,
|
9
9
|
Optional,
|
10
10
|
)
|
11
11
|
|
12
12
|
from pydantic import BaseModel
|
13
13
|
|
14
|
+
from reconcile.gql_definitions.slo_documents.slo_documents import SLODocumentV1
|
14
15
|
from reconcile.gql_definitions.status_board.status_board import StatusBoardV1
|
16
|
+
from reconcile.typed_queries.slo_documents import get_slo_documents
|
15
17
|
from reconcile.typed_queries.status_board import (
|
16
18
|
get_selected_app_names,
|
17
19
|
get_status_board,
|
18
20
|
)
|
19
21
|
from reconcile.utils.differ import diff_mappings
|
20
22
|
from reconcile.utils.ocm.status_board import (
|
23
|
+
ApplicationOCMSpec,
|
24
|
+
BaseOCMSpec,
|
25
|
+
ServiceMetadataSpec,
|
26
|
+
ServiceOCMSpec,
|
21
27
|
create_application,
|
22
28
|
create_product,
|
29
|
+
create_service,
|
23
30
|
delete_application,
|
24
31
|
delete_product,
|
32
|
+
delete_service,
|
33
|
+
get_application_services,
|
25
34
|
get_managed_products,
|
26
35
|
get_product_applications,
|
36
|
+
update_service,
|
27
37
|
)
|
28
38
|
from reconcile.utils.ocm_base_client import (
|
29
39
|
OCMBaseClient,
|
@@ -34,6 +44,12 @@ from reconcile.utils.runtime.integration import QontractReconcileIntegration
|
|
34
44
|
QONTRACT_INTEGRATION = "status-board-exporter"
|
35
45
|
|
36
46
|
|
47
|
+
class Action(Enum):
|
48
|
+
create = "create"
|
49
|
+
update = "update"
|
50
|
+
delete = "delete"
|
51
|
+
|
52
|
+
|
37
53
|
class AbstractStatusBoard(ABC, BaseModel):
|
38
54
|
"""Abstract class for upgrade policies
|
39
55
|
Used to create and delete upgrade policies in OCM."""
|
@@ -41,12 +57,15 @@ class AbstractStatusBoard(ABC, BaseModel):
|
|
41
57
|
id: str | None
|
42
58
|
name: str
|
43
59
|
fullname: str
|
44
|
-
metadata: dict[str, Any] | None
|
45
60
|
|
46
61
|
@abstractmethod
|
47
62
|
def create(self, ocm: OCMBaseClient) -> None:
|
48
63
|
pass
|
49
64
|
|
65
|
+
@abstractmethod
|
66
|
+
def update(self, ocm: OCMBaseClient) -> None:
|
67
|
+
pass
|
68
|
+
|
50
69
|
@abstractmethod
|
51
70
|
def delete(self, ocm: OCMBaseClient) -> None:
|
52
71
|
pass
|
@@ -60,16 +79,28 @@ class AbstractStatusBoard(ABC, BaseModel):
|
|
60
79
|
def get_priority() -> int:
|
61
80
|
pass
|
62
81
|
|
82
|
+
@abstractmethod
|
83
|
+
def to_ocm_spec(self) -> BaseOCMSpec:
|
84
|
+
pass
|
85
|
+
|
86
|
+
def __eq__(self, other: object) -> bool:
|
87
|
+
if not isinstance(other, AbstractStatusBoard):
|
88
|
+
return NotImplemented
|
89
|
+
return self.name == other.name and self.fullname == other.fullname
|
90
|
+
|
63
91
|
|
64
92
|
class Product(AbstractStatusBoard):
|
65
93
|
applications: list["Application"] | None
|
66
94
|
|
67
95
|
def create(self, ocm: OCMBaseClient) -> None:
|
68
|
-
spec = self.
|
69
|
-
spec.pop("applications")
|
70
|
-
spec.pop("id")
|
96
|
+
spec = self.to_ocm_spec()
|
71
97
|
self.id = create_product(ocm, spec)
|
72
98
|
|
99
|
+
def update(self, ocm: OCMBaseClient) -> None:
|
100
|
+
err_msg = "Called update on StatusBoardHandler that doesn't have update method"
|
101
|
+
logging.error(err_msg)
|
102
|
+
raise UpdateNotSupported(err_msg)
|
103
|
+
|
73
104
|
def delete(self, ocm: OCMBaseClient) -> None:
|
74
105
|
if not self.id:
|
75
106
|
logging.error(f'Trying to delete Product "{self.name}" without id')
|
@@ -79,6 +110,12 @@ class Product(AbstractStatusBoard):
|
|
79
110
|
def summarize(self) -> str:
|
80
111
|
return f'Product: "{self.name}"'
|
81
112
|
|
113
|
+
def to_ocm_spec(self) -> BaseOCMSpec:
|
114
|
+
return {
|
115
|
+
"name": self.name,
|
116
|
+
"fullname": self.fullname,
|
117
|
+
}
|
118
|
+
|
82
119
|
@staticmethod
|
83
120
|
def get_priority() -> int:
|
84
121
|
return 0
|
@@ -86,18 +123,20 @@ class Product(AbstractStatusBoard):
|
|
86
123
|
|
87
124
|
class Application(AbstractStatusBoard):
|
88
125
|
product: Optional["Product"]
|
126
|
+
services: list["Service"] | None
|
89
127
|
|
90
128
|
def create(self, ocm: OCMBaseClient) -> None:
|
91
|
-
|
92
|
-
|
93
|
-
product = spec.pop("product")
|
94
|
-
product_id = product.get("id")
|
95
|
-
if product_id:
|
96
|
-
spec["product"] = {"id": product_id}
|
129
|
+
if self.product and self.product.id:
|
130
|
+
spec = self.to_ocm_spec()
|
97
131
|
self.id = create_application(ocm, spec)
|
98
132
|
else:
|
99
133
|
logging.warning("Missing product id for application")
|
100
134
|
|
135
|
+
def update(self, ocm: OCMBaseClient) -> None:
|
136
|
+
err_msg = "Called update on StatusBoardHandler that doesn't have update method"
|
137
|
+
logging.error(err_msg)
|
138
|
+
raise UpdateNotSupported(err_msg)
|
139
|
+
|
101
140
|
def delete(self, ocm: OCMBaseClient) -> None:
|
102
141
|
if not self.id:
|
103
142
|
logging.error(f'Trying to delete Application "{self.name}" without id')
|
@@ -107,25 +146,110 @@ class Application(AbstractStatusBoard):
|
|
107
146
|
def summarize(self) -> str:
|
108
147
|
return f'Application: "{self.name}" "{self.fullname}"'
|
109
148
|
|
149
|
+
def to_ocm_spec(self) -> ApplicationOCMSpec:
|
150
|
+
product_id = self.product.id if self.product and self.product.id else ""
|
151
|
+
return {
|
152
|
+
"name": self.name,
|
153
|
+
"fullname": self.fullname,
|
154
|
+
"product_id": product_id,
|
155
|
+
}
|
156
|
+
|
110
157
|
@staticmethod
|
111
158
|
def get_priority() -> int:
|
112
159
|
return 1
|
113
160
|
|
114
161
|
|
162
|
+
class Service(AbstractStatusBoard):
|
163
|
+
# `application` here is used to create a flat map to easily compare state.
|
164
|
+
# This field is optional so we can create the Service object without the
|
165
|
+
# need to create an Application object first.
|
166
|
+
# This filed is needed when we are creating a Service on teh OCM API.
|
167
|
+
# This field is not used when we are mapping the services that belongs to an
|
168
|
+
# application in that case we use the `services` field in Application class.
|
169
|
+
application: Optional["Application"]
|
170
|
+
metadata: ServiceMetadataSpec
|
171
|
+
|
172
|
+
def create(self, ocm: OCMBaseClient) -> None:
|
173
|
+
spec = self.to_ocm_spec()
|
174
|
+
if self.application and self.application.id:
|
175
|
+
self.id = create_service(ocm, spec)
|
176
|
+
else:
|
177
|
+
logging.warning("Missing application id for service")
|
178
|
+
|
179
|
+
def delete(self, ocm: OCMBaseClient) -> None:
|
180
|
+
if not self.id:
|
181
|
+
logging.error(f'Trying to delete Service "{self.name}" without id')
|
182
|
+
return
|
183
|
+
delete_service(ocm, self.id)
|
184
|
+
|
185
|
+
def update(self, ocm: OCMBaseClient) -> None:
|
186
|
+
if not self.id:
|
187
|
+
logging.error(f'Trying to update Service "{self.name}" without id')
|
188
|
+
return
|
189
|
+
spec = self.to_ocm_spec()
|
190
|
+
if self.application and self.application.id:
|
191
|
+
update_service(ocm, self.id, spec)
|
192
|
+
else:
|
193
|
+
logging.warning("Missing application id for service")
|
194
|
+
|
195
|
+
def summarize(self) -> str:
|
196
|
+
return f'Service: "{self.name}" "{self.fullname}"'
|
197
|
+
|
198
|
+
def to_ocm_spec(self) -> ServiceOCMSpec:
|
199
|
+
application_id = (
|
200
|
+
self.application.id if self.application and self.application.id else ""
|
201
|
+
)
|
202
|
+
|
203
|
+
return {
|
204
|
+
"name": self.name,
|
205
|
+
"fullname": self.fullname,
|
206
|
+
"metadata": self.metadata,
|
207
|
+
"status_type": "traffic_light",
|
208
|
+
"service_endpoint": "none",
|
209
|
+
"application_id": application_id,
|
210
|
+
}
|
211
|
+
|
212
|
+
@staticmethod
|
213
|
+
def get_priority() -> int:
|
214
|
+
return 2
|
215
|
+
|
216
|
+
def __eq__(self, other: object) -> bool:
|
217
|
+
if not isinstance(other, Service):
|
218
|
+
return NotImplemented
|
219
|
+
return (
|
220
|
+
self.name == other.name
|
221
|
+
and self.fullname == other.fullname
|
222
|
+
and self.metadata == other.metadata
|
223
|
+
)
|
224
|
+
|
225
|
+
|
226
|
+
# Resolve forward references after class definitions
|
227
|
+
Product.update_forward_refs()
|
228
|
+
Application.update_forward_refs()
|
229
|
+
Service.update_forward_refs()
|
230
|
+
|
231
|
+
|
232
|
+
class UpdateNotSupported(Exception):
|
233
|
+
pass
|
234
|
+
|
235
|
+
|
115
236
|
class StatusBoardHandler(BaseModel):
|
116
|
-
action:
|
237
|
+
action: Action
|
117
238
|
status_board_object: AbstractStatusBoard
|
118
239
|
|
119
240
|
def act(self, dry_run: bool, ocm: OCMBaseClient) -> None:
|
120
241
|
logging.info(f"{self.action} - {self.status_board_object.summarize()}")
|
242
|
+
|
121
243
|
if dry_run:
|
122
244
|
return
|
123
245
|
|
124
246
|
match self.action:
|
125
|
-
case
|
247
|
+
case Action.delete:
|
126
248
|
self.status_board_object.delete(ocm)
|
127
|
-
case
|
249
|
+
case Action.create:
|
128
250
|
self.status_board_object.create(ocm)
|
251
|
+
case Action.update:
|
252
|
+
self.status_board_object.update(ocm)
|
129
253
|
|
130
254
|
|
131
255
|
class StatusBoardExporterIntegration(QontractReconcileIntegration):
|
@@ -146,7 +270,9 @@ class StatusBoardExporterIntegration(QontractReconcileIntegration):
|
|
146
270
|
}
|
147
271
|
|
148
272
|
@staticmethod
|
149
|
-
def
|
273
|
+
def get_current_products_applications_services(
|
274
|
+
ocm_api: OCMBaseClient,
|
275
|
+
) -> list[Product]:
|
150
276
|
products_raw = get_managed_products(ocm_api)
|
151
277
|
products = [Product(**p) for p in products_raw]
|
152
278
|
|
@@ -157,82 +283,135 @@ class StatusBoardExporterIntegration(QontractReconcileIntegration):
|
|
157
283
|
p.applications = [
|
158
284
|
Application(**a) for a in get_product_applications(ocm_api, p.id)
|
159
285
|
]
|
286
|
+
for a in p.applications:
|
287
|
+
if not a.id:
|
288
|
+
logging.error(f'Application "{a.name}" has no id')
|
289
|
+
continue
|
290
|
+
a.services = [
|
291
|
+
Service(**s) for s in get_application_services(ocm_api, a.id)
|
292
|
+
]
|
160
293
|
|
161
294
|
return products
|
162
295
|
|
163
296
|
@staticmethod
|
164
|
-
def
|
165
|
-
desired_product_apps: Mapping[str, set[str]],
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
297
|
+
def desired_abstract_status_board_map(
|
298
|
+
desired_product_apps: Mapping[str, set[str]], slodocs: list[SLODocumentV1]
|
299
|
+
) -> dict[str, AbstractStatusBoard]:
|
300
|
+
"""
|
301
|
+
Returns a Mapping of all the AbstractStatusBoard data objects as dictionaries.
|
302
|
+
The key is formed by combining the Product, Application and Service name
|
303
|
+
separeted by a '/' character. This is the same format as the fullname property
|
304
|
+
on Status Board OCM API.
|
305
|
+
"""
|
306
|
+
desired_abstract_status_board_map: dict[str, AbstractStatusBoard] = {}
|
307
|
+
for product, apps in desired_product_apps.items():
|
308
|
+
desired_abstract_status_board_map[product] = Product(
|
309
|
+
name=product, fullname=product, applications=[], metadata={}
|
173
310
|
)
|
311
|
+
for a in apps:
|
312
|
+
key = f"{product}/{a}"
|
313
|
+
desired_abstract_status_board_map[key] = Application(
|
314
|
+
name=a,
|
315
|
+
fullname=key,
|
316
|
+
services=[],
|
317
|
+
product=desired_abstract_status_board_map[product],
|
318
|
+
metadata={},
|
319
|
+
)
|
320
|
+
for slodoc in slodocs:
|
321
|
+
products = [
|
322
|
+
ns.namespace.environment.product.name for ns in slodoc.namespaces
|
323
|
+
]
|
324
|
+
for slo in slodoc.slos or []:
|
325
|
+
for product in products:
|
326
|
+
if slodoc.app.parent_app:
|
327
|
+
app = f"{slodoc.app.parent_app.name}-{slodoc.app.name}"
|
328
|
+
else:
|
329
|
+
app = slodoc.app.name
|
330
|
+
|
331
|
+
# Check if the product or app is excluded from the desired list
|
332
|
+
product_or_app_excluded = (
|
333
|
+
product not in desired_product_apps
|
334
|
+
or app not in desired_product_apps.get(product, set())
|
335
|
+
)
|
336
|
+
|
337
|
+
# Check if statusBoard label exists and is explicitly disabled
|
338
|
+
status_board_enabled = (
|
339
|
+
slodoc.labels is not None
|
340
|
+
and "statusBoard" in slodoc.labels
|
341
|
+
and slodoc.labels["statusBoard"] == "enabled"
|
342
|
+
)
|
343
|
+
|
344
|
+
if product_or_app_excluded or not status_board_enabled:
|
345
|
+
continue
|
346
|
+
|
347
|
+
key = f"{product}/{app}/{slo.name}"
|
348
|
+
metadata = {
|
349
|
+
"sli_type": slo.sli_type,
|
350
|
+
"sli_specification": slo.sli_specification,
|
351
|
+
"slo_details": slo.slo_details,
|
352
|
+
"target": slo.slo_target,
|
353
|
+
"target_unit": slo.slo_target_unit,
|
354
|
+
"window": slo.slo_parameters.window,
|
355
|
+
}
|
356
|
+
desired_abstract_status_board_map[key] = Service(
|
357
|
+
name=slo.name,
|
358
|
+
fullname=key,
|
359
|
+
metadata=metadata,
|
360
|
+
application=desired_abstract_status_board_map[
|
361
|
+
f"{product}/{app}"
|
362
|
+
],
|
363
|
+
)
|
364
|
+
|
365
|
+
return desired_abstract_status_board_map
|
174
366
|
|
175
|
-
|
176
|
-
|
367
|
+
@staticmethod
|
368
|
+
def current_abstract_status_board_map(
|
369
|
+
current_products_applications_services: Iterable[Product],
|
370
|
+
) -> dict[str, AbstractStatusBoard]:
|
371
|
+
return_value: dict[str, AbstractStatusBoard] = {}
|
372
|
+
for product in current_products_applications_services:
|
373
|
+
return_value[product.name] = product
|
374
|
+
for app in product.applications or []:
|
375
|
+
return_value[f"{product.name}/{app.name}"] = app
|
376
|
+
for service in app.services or []:
|
377
|
+
return_value[f"{product.name}/{app.name}/{service.name}"] = service
|
378
|
+
|
379
|
+
return return_value
|
177
380
|
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
381
|
+
@staticmethod
|
382
|
+
def get_diff(
|
383
|
+
desired_abstract_status_board_map: Mapping[str, AbstractStatusBoard],
|
384
|
+
current_abstract_status_board_map: Mapping[str, AbstractStatusBoard],
|
385
|
+
current_products: Mapping[str, Product],
|
386
|
+
) -> list[StatusBoardHandler]:
|
387
|
+
return_list: list[StatusBoardHandler] = []
|
182
388
|
|
183
389
|
diff_result = diff_mappings(
|
184
|
-
|
185
|
-
|
390
|
+
current_abstract_status_board_map,
|
391
|
+
desired_abstract_status_board_map,
|
186
392
|
)
|
187
393
|
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
action="create",
|
193
|
-
status_board_object=product,
|
194
|
-
)
|
195
|
-
)
|
196
|
-
# new product, so it misses also the applications
|
197
|
-
return_list.extend(
|
198
|
-
StatusBoardHandler(
|
199
|
-
action="create",
|
200
|
-
status_board_object=create_app(app_name, product),
|
201
|
-
)
|
202
|
-
for app_name in desired_product_apps[product_name]
|
203
|
-
)
|
394
|
+
return_list.extend(
|
395
|
+
StatusBoardHandler(action=Action.create, status_board_object=o)
|
396
|
+
for o in diff_result.add.values()
|
397
|
+
)
|
204
398
|
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
)
|
221
|
-
for application in product.applications or []
|
222
|
-
if application.name in to_delete
|
223
|
-
)
|
399
|
+
return_list.extend(
|
400
|
+
StatusBoardHandler(action=Action.delete, status_board_object=o)
|
401
|
+
for o in diff_result.delete.values()
|
402
|
+
)
|
403
|
+
|
404
|
+
services_to_update = [
|
405
|
+
s.desired
|
406
|
+
for _, s in diff_result.change.items()
|
407
|
+
if isinstance(s.desired, Service)
|
408
|
+
]
|
409
|
+
|
410
|
+
return_list.extend(
|
411
|
+
StatusBoardHandler(action=Action.update, status_board_object=s)
|
412
|
+
for s in services_to_update
|
413
|
+
)
|
224
414
|
|
225
|
-
# product is deleted entirely
|
226
|
-
for product_name in diff_result.delete:
|
227
|
-
return_list.extend(
|
228
|
-
StatusBoardHandler(action="delete", status_board_object=application)
|
229
|
-
for application in current_products[product_name].applications or []
|
230
|
-
)
|
231
|
-
return_list.append(
|
232
|
-
StatusBoardHandler(
|
233
|
-
action="delete", status_board_object=current_products[product_name]
|
234
|
-
)
|
235
|
-
)
|
236
415
|
return return_list
|
237
416
|
|
238
417
|
@staticmethod
|
@@ -241,35 +420,54 @@ class StatusBoardExporterIntegration(QontractReconcileIntegration):
|
|
241
420
|
) -> None:
|
242
421
|
creations: list[StatusBoardHandler] = []
|
243
422
|
deletions: list[StatusBoardHandler] = []
|
423
|
+
updates: list[StatusBoardHandler] = []
|
244
424
|
|
245
425
|
for o in diff:
|
246
426
|
match o.action:
|
247
|
-
case
|
427
|
+
case Action.create:
|
248
428
|
creations.append(o)
|
249
|
-
case
|
429
|
+
case Action.delete:
|
250
430
|
deletions.append(o)
|
431
|
+
case Action.update:
|
432
|
+
updates.append(o)
|
251
433
|
|
252
434
|
# Products need to be created before Applications
|
435
|
+
# Applications need to be created before Services
|
253
436
|
creations.sort(key=lambda x: x.status_board_object.get_priority())
|
254
437
|
|
438
|
+
# Services need to be deleted before Applications
|
255
439
|
# Applications need to be deleted before Products
|
256
440
|
deletions.sort(key=lambda x: x.status_board_object.get_priority(), reverse=True)
|
257
441
|
|
258
|
-
for d in creations + deletions:
|
442
|
+
for d in creations + deletions + updates:
|
259
443
|
d.act(dry_run, ocm_api)
|
260
444
|
|
261
445
|
def run(self, dry_run: bool) -> None:
|
262
|
-
|
263
|
-
Product.update_forward_refs()
|
264
|
-
|
446
|
+
slodocs = get_slo_documents()
|
265
447
|
for sb in get_status_board():
|
266
448
|
ocm_api = init_ocm_base_client(sb.ocm, self.secret_reader)
|
449
|
+
|
450
|
+
# Desired state
|
267
451
|
desired_product_apps: dict[str, set[str]] = self.get_product_apps(sb)
|
452
|
+
desired_abstract_status_board_map = self.desired_abstract_status_board_map(
|
453
|
+
desired_product_apps, slodocs
|
454
|
+
)
|
268
455
|
|
269
|
-
|
270
|
-
|
456
|
+
# Current state
|
457
|
+
current_products_applications_services = (
|
458
|
+
self.get_current_products_applications_services(ocm_api)
|
459
|
+
)
|
460
|
+
current_abstract_status_board_map = self.current_abstract_status_board_map(
|
461
|
+
current_products_applications_services
|
271
462
|
)
|
272
463
|
|
273
|
-
|
464
|
+
current_products = {
|
465
|
+
p.name: p for p in current_products_applications_services
|
466
|
+
}
|
467
|
+
diff = self.get_diff(
|
468
|
+
desired_abstract_status_board_map,
|
469
|
+
current_abstract_status_board_map,
|
470
|
+
current_products,
|
471
|
+
)
|
274
472
|
|
275
473
|
self.apply_diff(dry_run, ocm_api, diff)
|
@@ -1,7 +1,8 @@
|
|
1
|
-
from typing import Any
|
1
|
+
from typing import Any, TypedDict
|
2
2
|
|
3
3
|
from reconcile.utils.ocm_base_client import OCMBaseClient
|
4
4
|
|
5
|
+
SERVICE_DESIRED_KEYS = {"id", "name", "fullname", "metadata"}
|
5
6
|
APPLICATION_DESIRED_KEYS = {"id", "name", "fullname", "metadata"}
|
6
7
|
PRODUCTS_DESIRED_KEYS = {"id", "name", "fullname", "metadata"}
|
7
8
|
|
@@ -10,6 +11,33 @@ METADATA_MANAGED_BY_KEY = "managedBy"
|
|
10
11
|
METADATA_MANAGED_BY_VALUE = "qontract-reconcile"
|
11
12
|
|
12
13
|
|
14
|
+
class BaseOCMSpec(TypedDict):
|
15
|
+
name: str
|
16
|
+
fullname: str
|
17
|
+
|
18
|
+
|
19
|
+
class ApplicationOCMSpec(BaseOCMSpec):
|
20
|
+
product_id: str
|
21
|
+
|
22
|
+
|
23
|
+
class ServiceMetadataSpec(TypedDict):
|
24
|
+
sli_type: str
|
25
|
+
sli_specification: str
|
26
|
+
slo_details: str
|
27
|
+
target: float
|
28
|
+
target_unit: str
|
29
|
+
window: str
|
30
|
+
|
31
|
+
|
32
|
+
class ServiceOCMSpec(BaseOCMSpec):
|
33
|
+
# The next two fields come from the orignal script at
|
34
|
+
# https://gitlab.cee.redhat.com/service/status-board/-/blob/main/scripts/create-services-from-app-intf.sh?ref_type=heads#L116
|
35
|
+
status_type: str
|
36
|
+
service_endpoint: str
|
37
|
+
application_id: str
|
38
|
+
metadata: ServiceMetadataSpec
|
39
|
+
|
40
|
+
|
13
41
|
def get_product_applications(
|
14
42
|
ocm_api: OCMBaseClient, product_id: str
|
15
43
|
) -> list[dict[str, Any]]:
|
@@ -29,6 +57,25 @@ def get_product_applications(
|
|
29
57
|
return results
|
30
58
|
|
31
59
|
|
60
|
+
def get_application_services(
|
61
|
+
ocm_api: OCMBaseClient, app_id: str
|
62
|
+
) -> list[dict[str, Any]]:
|
63
|
+
results: list[dict[str, Any]] = []
|
64
|
+
|
65
|
+
for service in ocm_api.get_paginated(
|
66
|
+
f"/api/status-board/v1/applications/{app_id}/services"
|
67
|
+
):
|
68
|
+
if (
|
69
|
+
service.get("metadata", {}).get(METADATA_MANAGED_BY_KEY, "")
|
70
|
+
== METADATA_MANAGED_BY_VALUE
|
71
|
+
):
|
72
|
+
results.append({ # noqa: PERF401
|
73
|
+
k: v for k, v in service.items() if k in SERVICE_DESIRED_KEYS
|
74
|
+
})
|
75
|
+
|
76
|
+
return results
|
77
|
+
|
78
|
+
|
32
79
|
def get_managed_products(ocm_api: OCMBaseClient) -> list[dict[str, Any]]:
|
33
80
|
results: list[dict[str, Any]] = []
|
34
81
|
for product in ocm_api.get_paginated("/api/status-board/v1/products/"):
|
@@ -42,25 +89,50 @@ def get_managed_products(ocm_api: OCMBaseClient) -> list[dict[str, Any]]:
|
|
42
89
|
return results
|
43
90
|
|
44
91
|
|
45
|
-
def create_product(ocm_api: OCMBaseClient, spec:
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
resp
|
92
|
+
def create_product(ocm_api: OCMBaseClient, spec: BaseOCMSpec) -> str:
|
93
|
+
data = spec | {"metadata": {METADATA_MANAGED_BY_KEY: METADATA_MANAGED_BY_VALUE}}
|
94
|
+
|
95
|
+
resp = ocm_api.post("/api/status-board/v1/products/", data=data)
|
96
|
+
return resp["id"]
|
97
|
+
|
98
|
+
|
99
|
+
def create_application(ocm_api: OCMBaseClient, spec: ApplicationOCMSpec) -> str:
|
100
|
+
data = spec | {"metadata": {METADATA_MANAGED_BY_KEY: METADATA_MANAGED_BY_VALUE}}
|
101
|
+
|
102
|
+
resp = ocm_api.post("/api/status-board/v1/applications/", data=data)
|
50
103
|
return resp["id"]
|
51
104
|
|
52
105
|
|
53
|
-
def
|
54
|
-
|
55
|
-
spec["metadata"]
|
56
|
-
|
57
|
-
|
106
|
+
def create_service(ocm_api: OCMBaseClient, spec: ServiceOCMSpec) -> str:
|
107
|
+
data = spec | {
|
108
|
+
"metadata": spec["metadata"]
|
109
|
+
| {METADATA_MANAGED_BY_KEY: METADATA_MANAGED_BY_VALUE}
|
110
|
+
}
|
111
|
+
|
112
|
+
resp = ocm_api.post("/api/status-board/v1/services/", data=data)
|
58
113
|
return resp["id"]
|
59
114
|
|
60
115
|
|
116
|
+
def update_service(
|
117
|
+
ocm_api: OCMBaseClient,
|
118
|
+
service_id: str,
|
119
|
+
spec: ServiceOCMSpec,
|
120
|
+
) -> None:
|
121
|
+
data = spec | {
|
122
|
+
"metadata": spec["metadata"]
|
123
|
+
| {METADATA_MANAGED_BY_KEY: METADATA_MANAGED_BY_VALUE}
|
124
|
+
}
|
125
|
+
|
126
|
+
ocm_api.patch(f"/api/status-board/v1/services/{service_id}", data=data)
|
127
|
+
|
128
|
+
|
61
129
|
def delete_product(ocm_api: OCMBaseClient, product_id: str) -> None:
|
62
130
|
ocm_api.delete(f"/api/status-board/v1/products/{product_id}")
|
63
131
|
|
64
132
|
|
65
133
|
def delete_application(ocm_api: OCMBaseClient, application_id: str) -> None:
|
66
134
|
ocm_api.delete(f"/api/status-board/v1/applications/{application_id}")
|
135
|
+
|
136
|
+
|
137
|
+
def delete_service(ocm_api: OCMBaseClient, service_id: str) -> None:
|
138
|
+
ocm_api.delete(f"/api/status-board/v1/services/{service_id}")
|
{qontract_reconcile-0.10.2.dev226.dist-info → qontract_reconcile-0.10.2.dev228.dist-info}/WHEEL
RENAMED
File without changes
|
File without changes
|