qontract-reconcile 0.10.2.dev298__py3-none-any.whl → 0.10.2.dev300__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.dev298
3
+ Version: 0.10.2.dev300
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
@@ -102,7 +102,7 @@ reconcile/slack_base.py,sha256=I-msunWxfgu5bSwXYulGbtLjxUB_tRmTCAUCU-3nabI,3484
102
102
  reconcile/slack_usergroups.py,sha256=3uQVZK0WeZfvE1g7xQwciKCcC3LifDa3NuE1ygQ0cRk,30174
103
103
  reconcile/sql_query.py,sha256=FVwANLPWjkUHqN2OXJ-vnX5hqqcO6rTdyLEO4HkmAgM,26397
104
104
  reconcile/status.py,sha256=cY4IJFXemhxptRJqR4qaaOWqei9e4jgLXuVSGajMsjg,544
105
- reconcile/status_board.py,sha256=0vTQzxrFPTmJtzNOC-iaJG_BmXbDe2vgBUe0LMUyfDE,15313
105
+ reconcile/status_board.py,sha256=Wt8aqCmhTbKxRa9GUj5U_aoqif18z9XL5hRK3zy8too,20091
106
106
  reconcile/terraform_aws_route53.py,sha256=CWp5bE3ddUrJGNNvG8YmkSPyNHCWtOc1GEDVLnbOY9Q,10043
107
107
  reconcile/terraform_cloudflare_dns.py,sha256=0Eu46o_BBEEq-B-CCvKop9VTbwrvliCKGSS9gLBSJE4,13456
108
108
  reconcile/terraform_cloudflare_resources.py,sha256=tK-BxQeNdZjf59deKd51Roz868e7UXe52XvcHsffJK0,14982
@@ -213,7 +213,7 @@ reconcile/glitchtip_project_alerts/integration.py,sha256=d3PMy-mQSbSZdIGAVaZCA2U
213
213
  reconcile/glitchtip_project_dsn/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
214
214
  reconcile/glitchtip_project_dsn/integration.py,sha256=3GgcqUM6hWhLpo9Yx5Xr9vrdexF-WNevVCNL9bJ0Upc,8162
215
215
  reconcile/gql_definitions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
216
- reconcile/gql_definitions/introspection.json,sha256=Aak1w7K7K1fhgZoZNN2gQ2TV3Jwh0X2GhqfpqRLwWp4,2359202
216
+ reconcile/gql_definitions/introspection.json,sha256=jJN4kUVnrBkb39pHZ0lwt_0ZuIyrhjyzsL17pQuGwXo,2358826
217
217
  reconcile/gql_definitions/acs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
218
218
  reconcile/gql_definitions/acs/acs_instances.py,sha256=L91WW9LbhJbBSrECqShQpFtjoBOsmNIYLRpMbx1io5o,2181
219
219
  reconcile/gql_definitions/acs/acs_policies.py,sha256=Ygpfl2-VkYLSlJvHgp_dJBfb66K_Rwfdfpsa18w1v1s,4338
@@ -405,7 +405,7 @@ reconcile/gql_definitions/slack_usergroups/users.py,sha256=0KFLYHYXL_bGIKf5LAJSY
405
405
  reconcile/gql_definitions/slo_documents/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
406
406
  reconcile/gql_definitions/slo_documents/slo_documents.py,sha256=pOrm9NXAonlo6Lxq6NkD3mHkZ53ZeBnZOZMkDvOEwds,3746
407
407
  reconcile/gql_definitions/status_board/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
408
- reconcile/gql_definitions/status_board/status_board.py,sha256=BYq-1g_-AjNKHIKmY2rQT8l3ql4Mcwa_CIxN_FS4F6M,4690
408
+ reconcile/gql_definitions/status_board/status_board.py,sha256=qm7TkvvBvkMvf_9SQ50wz8ke8LEvwZYQjMkUDrN-BV0,5370
409
409
  reconcile/gql_definitions/statuspage/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
410
410
  reconcile/gql_definitions/statuspage/statuspages.py,sha256=CTRzjiR9k41LqlkgyoNHwC2JERsoD_Run_aK7jw_Ono,5299
411
411
  reconcile/gql_definitions/templating/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -426,7 +426,7 @@ reconcile/gql_definitions/terraform_repo/__init__.py,sha256=47DEQpj8HBSa-_TImW-5
426
426
  reconcile/gql_definitions/terraform_repo/terraform_repo.py,sha256=9cDKdP9ziBh9J_mw2Gi6GUOP4mFxMABY_D62qSeMtJI,3881
427
427
  reconcile/gql_definitions/terraform_resources/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
428
428
  reconcile/gql_definitions/terraform_resources/database_access_manager.py,sha256=yv0_YC-LmhaKD_gyGG3le1w5BtypBjlsO894-Zgdg4U,4813
429
- reconcile/gql_definitions/terraform_resources/terraform_resources_namespaces.py,sha256=EKqXL8Bx6NXqsVI4nfyQpKq8B_uHQl0L7QWnerBQTy0,44668
429
+ reconcile/gql_definitions/terraform_resources/terraform_resources_namespaces.py,sha256=j1xemQQIjR4O80Ni4RbJhDOWzk9iYcGinO79jZ3kZow,44688
430
430
  reconcile/gql_definitions/terraform_tgw_attachments/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
431
431
  reconcile/gql_definitions/terraform_tgw_attachments/aws_accounts.py,sha256=r6RacQX243Rrtm_6wobSLJZlObehqzkV-seyCVCqiv8,2596
432
432
  reconcile/gql_definitions/unleash_feature_toggles/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -563,7 +563,7 @@ reconcile/typed_queries/saas_files.py,sha256=SOE36sWPBcuaRmEaNxXCQZMQdJiUZX8_A92
563
563
  reconcile/typed_queries/slack.py,sha256=r30lspctHloyygPn8_DVybxPwUWwiBpvBRRXiTVcQYk,251
564
564
  reconcile/typed_queries/slo_documents.py,sha256=YMdox_-lBRqrdxamPhdnUlRTY_Ro35ptsupq7OaynUQ,362
565
565
  reconcile/typed_queries/smtp.py,sha256=aSLglYa5bHKmlGwKkxq2RZqyMWuAf0a4S_mOuhDa084,542
566
- reconcile/typed_queries/status_board.py,sha256=XB8kn3qh2cIqGTPCImS5eUyZOaolvSNfWss5kUkARYg,1826
566
+ reconcile/typed_queries/status_board.py,sha256=ghu3jRJVzEPm5qHQUJWSCGLUdQzBcrtxU4mEeCCdXjo,3091
567
567
  reconcile/typed_queries/tekton_pipeline_providers.py,sha256=LtoSnSRkuckYsXIU64L1Mf-o3iuUjaN-5O-ARzIROZA,515
568
568
  reconcile/typed_queries/terraform_namespaces.py,sha256=4H9WE90jN_BVYBAt1DxJITS4vkL-vykbXZIS1H4EKNM,413
569
569
  reconcile/typed_queries/unleash.py,sha256=7HDc4owF044xM9Thx4WsXV7DZgETxJjy4lbpwmqz1vU,282
@@ -658,7 +658,7 @@ reconcile/utils/sqs_gateway.py,sha256=XNIf3PY4UCPNufP2Ul0UJj3fKlt5larBba-VTT-41F
658
658
  reconcile/utils/state.py,sha256=vCHYIfrWLfPyIWEHSaADWlc4OqhwcOiqM3Egqvw-lfo,16372
659
659
  reconcile/utils/structs.py,sha256=LcbLEg8WxfRqM6nW7NhcWN0YeqF7SQzxOgntmLs1SgY,352
660
660
  reconcile/utils/terraform_client.py,sha256=GoLbfs4d4YItNCeV3NZnrth4sD8ziNYgY2IszruRDpg,37303
661
- reconcile/utils/terrascript_aws_client.py,sha256=jVzh5PmphbCAN7Pog_PFYHoHj7lmQGb6Q4FwT_c8pF8,295634
661
+ reconcile/utils/terrascript_aws_client.py,sha256=o5-K61gEbQN48IRfdHVDfgt0sW-sYN9WYho4ZZ7j7io,295917
662
662
  reconcile/utils/three_way_diff_strategy.py,sha256=oQcHXd9LVhirJfoaOBoHUYuZVGfyL2voKr6KVI34zZE,4833
663
663
  reconcile/utils/throughput.py,sha256=iP4UWAe2LVhDo69mPPmgo9nQ7RxHD6_GS8MZe-aSiuM,344
664
664
  reconcile/utils/vault.py,sha256=6V15LByFghp-U3k0N4lum6V7qt2EAlRfcAxjy5e-FAU,15146
@@ -733,7 +733,7 @@ reconcile/utils/ocm/products.py,sha256=UtWpkAvSMCxPOulEB7aV5ZY8ej_rmErlE_HVdm9Gn
733
733
  reconcile/utils/ocm/search_filters.py,sha256=09p4Wq1d1HGrDiinf1dmLJ46VtFhkkRCOL4V-N-zwjY,14808
734
734
  reconcile/utils/ocm/service_log.py,sha256=RG1f0MMn6joKaRCAm2xveSJCavdOPP1BVo9FXecDxaI,2018
735
735
  reconcile/utils/ocm/sre_capability_labels.py,sha256=nqh0imrYczNeeeC7ZNX3pEwuAIVkKLTKZf0YHSPZYpE,1537
736
- reconcile/utils/ocm/status_board.py,sha256=8DYeIrOsW8Bh5PCtKdvGGpaxb9Wugcc5rLxZJ8Z7b_s,4181
736
+ reconcile/utils/ocm/status_board.py,sha256=1T9iD3DxWiVlY_8sK94twH01j_WrUbh-A2FC9dpXYPE,4548
737
737
  reconcile/utils/ocm/subscriptions.py,sha256=hehKXsDXIhnhqvWOuiYvx6y2FGq3zt0APGYj7WiBIdI,2765
738
738
  reconcile/utils/ocm/syncsets.py,sha256=9IQm1l5BodOVZa2OFbQmow3afmh4nXe5pn-CCJ5LxTI,1169
739
739
  reconcile/utils/ocm/upgrades.py,sha256=W8-sLgETI_418ftY9vBlXswyjx_KdhJTJO9cwBL3hfY,4162
@@ -770,7 +770,7 @@ tools/app_sre_tekton_access_reporter.py,sha256=5qmkevJdlb2j_lpGC5Pu1Pmo0eomX5Zxz
770
770
  tools/app_sre_tekton_access_revalidation.py,sha256=vwL1o_j7oSTOhrHNH1znpgjA2LHGzb8yc5iG3aaY4m0,2684
771
771
  tools/glitchtip_access_reporter.py,sha256=wnaiDGW4MkYONV_erltnJ6nGkEj0kQrAiv04NNnOS0k,2859
772
772
  tools/glitchtip_access_revalidation.py,sha256=jjeLO53LTbz_LfQw3G2Cs8lVLO_6xqU39BYyTH3cEPE,2764
773
- tools/qontract_cli.py,sha256=KyK2iCSh68cPolZygQtYb18ZtjIUlFw4aZT9D5p91kw,159892
773
+ tools/qontract_cli.py,sha256=oOyvoY5-9UzLZbl7iUhENVs8DSH4bORbG90Dxww8GrQ,159925
774
774
  tools/template_validation.py,sha256=Xn9X4sGFznx-rvBDnq9Kq16rfET8V3bqH1EwavsGBac,3335
775
775
  tools/cli_commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
776
776
  tools/cli_commands/container_images_report.py,sha256=8mAjCS6XR0yD7k0mfiVBlt6xbYU47q_ftdYNi5o5VKE,5566
@@ -796,7 +796,7 @@ tools/saas_promotion_state/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJ
796
796
  tools/saas_promotion_state/saas_promotion_state.py,sha256=uQv2QJAmUXP1g2GPIH30WTlvL9soY6m9lefpZEVDM5w,3965
797
797
  tools/sre_checkpoints/__init__.py,sha256=CDaDaywJnmRCLyl_NCcvxi-Zc0hTi_3OdwKiFOyS39I,145
798
798
  tools/sre_checkpoints/util.py,sha256=zEDbGr18ZeHNQwW8pUsr2JRjuXIPz--WAGJxZo9sv_Y,894
799
- qontract_reconcile-0.10.2.dev298.dist-info/METADATA,sha256=gruuhkwrqSh_2pDHyY0JBlIRMju96xZrOUm2bP1qoVA,24916
800
- qontract_reconcile-0.10.2.dev298.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
801
- qontract_reconcile-0.10.2.dev298.dist-info/entry_points.txt,sha256=5i9l54La3vQrDLAdwDKQWC0iG4sV9RRfOb1BpvzOWLc,698
802
- qontract_reconcile-0.10.2.dev298.dist-info/RECORD,,
799
+ qontract_reconcile-0.10.2.dev300.dist-info/METADATA,sha256=ZDSvK_d6EHWuZ5pW0oPw4ETFGG4X5Sexs4LYDjlII-U,24916
800
+ qontract_reconcile-0.10.2.dev300.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
801
+ qontract_reconcile-0.10.2.dev300.dist-info/entry_points.txt,sha256=5i9l54La3vQrDLAdwDKQWC0iG4sV9RRfOb1BpvzOWLc,698
802
+ qontract_reconcile-0.10.2.dev300.dist-info/RECORD,,
@@ -47157,13 +47157,9 @@
47157
47157
  "description": null,
47158
47158
  "args": [],
47159
47159
  "type": {
47160
- "kind": "NON_NULL",
47161
- "name": null,
47162
- "ofType": {
47163
- "kind": "SCALAR",
47164
- "name": "String",
47165
- "ofType": null
47166
- }
47160
+ "kind": "SCALAR",
47161
+ "name": "String",
47162
+ "ofType": null
47167
47163
  },
47168
47164
  "isDeprecated": false,
47169
47165
  "deprecationReason": null
@@ -47173,13 +47169,9 @@
47173
47169
  "description": null,
47174
47170
  "args": [],
47175
47171
  "type": {
47176
- "kind": "NON_NULL",
47177
- "name": null,
47178
- "ofType": {
47179
- "kind": "SCALAR",
47180
- "name": "String",
47181
- "ofType": null
47182
- }
47172
+ "kind": "SCALAR",
47173
+ "name": "String",
47174
+ "ofType": null
47183
47175
  },
47184
47176
  "isDeprecated": false,
47185
47177
  "deprecationReason": null
@@ -51,11 +51,19 @@ query StatusBoard {
51
51
  childrenApps {
52
52
  name
53
53
  onboardingStatus
54
+ saasFiles {
55
+ name
56
+ managedResourceTypes
57
+ }
54
58
  }
55
59
  parentApp {
56
60
  name
57
61
  onboardingStatus
58
62
  }
63
+ saasFiles {
64
+ name
65
+ managedResourceTypes
66
+ }
59
67
  }
60
68
  }
61
69
  }
@@ -96,9 +104,15 @@ class ProductV1(ConfiguredBaseModel):
96
104
  name: str = Field(..., alias="name")
97
105
 
98
106
 
107
+ class SaasFileV2(ConfiguredBaseModel):
108
+ name: str = Field(..., alias="name")
109
+ managed_resource_types: list[str] = Field(..., alias="managedResourceTypes")
110
+
111
+
99
112
  class AppV1_AppV1(ConfiguredBaseModel):
100
113
  name: str = Field(..., alias="name")
101
114
  onboarding_status: str = Field(..., alias="onboardingStatus")
115
+ saas_files: Optional[list[SaasFileV2]] = Field(..., alias="saasFiles")
102
116
 
103
117
 
104
118
  class NamespaceV1_AppV1_AppV1(ConfiguredBaseModel):
@@ -106,11 +120,17 @@ class NamespaceV1_AppV1_AppV1(ConfiguredBaseModel):
106
120
  onboarding_status: str = Field(..., alias="onboardingStatus")
107
121
 
108
122
 
123
+ class AppV1_SaasFileV2(ConfiguredBaseModel):
124
+ name: str = Field(..., alias="name")
125
+ managed_resource_types: list[str] = Field(..., alias="managedResourceTypes")
126
+
127
+
109
128
  class AppV1(ConfiguredBaseModel):
110
129
  name: str = Field(..., alias="name")
111
130
  onboarding_status: str = Field(..., alias="onboardingStatus")
112
131
  children_apps: Optional[list[AppV1_AppV1]] = Field(..., alias="childrenApps")
113
132
  parent_app: Optional[NamespaceV1_AppV1_AppV1] = Field(..., alias="parentApp")
133
+ saas_files: Optional[list[AppV1_SaasFileV2]] = Field(..., alias="saasFiles")
114
134
 
115
135
 
116
136
  class NamespaceV1(ConfiguredBaseModel):
@@ -817,8 +817,8 @@ class NamespaceTerraformResourceS3CloudFrontPublicKeyV1(NamespaceTerraformResour
817
817
 
818
818
  class NamespaceTerraformResourceALBMutualAuthenticationV1(ConfiguredBaseModel):
819
819
  mode: str = Field(..., alias="mode")
820
- ca_cert_bundle_s3_bucket_name: str = Field(..., alias="ca_cert_bundle_s3_bucket_name")
821
- ca_cert_bundle_s3_bucket_key: str = Field(..., alias="ca_cert_bundle_s3_bucket_key")
820
+ ca_cert_bundle_s3_bucket_name: Optional[str] = Field(..., alias="ca_cert_bundle_s3_bucket_name")
821
+ ca_cert_bundle_s3_bucket_key: Optional[str] = Field(..., alias="ca_cert_bundle_s3_bucket_key")
822
822
 
823
823
 
824
824
  class NamespaceTerraformResourceALBTargetHealthcheckV1(ConfiguredBaseModel):
reconcile/status_board.py CHANGED
@@ -6,6 +6,9 @@ from abc import (
6
6
  from collections.abc import Iterable, Mapping
7
7
  from enum import Enum
8
8
  from itertools import chain
9
+ from typing import (
10
+ Any,
11
+ )
9
12
 
10
13
  from pydantic import BaseModel
11
14
 
@@ -13,7 +16,7 @@ from reconcile.gql_definitions.slo_documents.slo_documents import SLODocumentV1
13
16
  from reconcile.gql_definitions.status_board.status_board import StatusBoardV1
14
17
  from reconcile.typed_queries.slo_documents import get_slo_documents
15
18
  from reconcile.typed_queries.status_board import (
16
- get_selected_app_names,
19
+ get_selected_app_data,
17
20
  get_status_board,
18
21
  )
19
22
  from reconcile.utils.differ import diff_mappings
@@ -31,6 +34,7 @@ from reconcile.utils.ocm.status_board import (
31
34
  get_application_services,
32
35
  get_managed_products,
33
36
  get_product_applications,
37
+ update_application,
34
38
  update_service,
35
39
  )
36
40
  from reconcile.utils.ocm_base_client import (
@@ -55,6 +59,7 @@ class AbstractStatusBoard(ABC, BaseModel):
55
59
  id: str | None
56
60
  name: str
57
61
  fullname: str
62
+ metadata: dict[str, Any] | ServiceMetadataSpec | None
58
63
 
59
64
  @abstractmethod
60
65
  def create(self, ocm: OCMBaseClient) -> None:
@@ -122,6 +127,7 @@ class Product(AbstractStatusBoard):
122
127
  class Application(AbstractStatusBoard):
123
128
  product: Product
124
129
  services: list["Service"] | None
130
+ metadata: dict[str, Any]
125
131
 
126
132
  def create(self, ocm: OCMBaseClient) -> None:
127
133
  if self.product.id:
@@ -131,9 +137,11 @@ class Application(AbstractStatusBoard):
131
137
  logging.warning("Missing product id for application")
132
138
 
133
139
  def update(self, ocm: OCMBaseClient) -> None:
134
- err_msg = "Called update on StatusBoardHandler that doesn't have update method"
135
- logging.error(err_msg)
136
- raise UpdateNotSupportedError(err_msg)
140
+ if not self.id:
141
+ logging.error(f'Trying to update Application "{self.name}" without id')
142
+ return
143
+ spec = self.to_ocm_spec()
144
+ update_application(ocm, self.id, spec)
137
145
 
138
146
  def delete(self, ocm: OCMBaseClient) -> None:
139
147
  if not self.id:
@@ -149,6 +157,7 @@ class Application(AbstractStatusBoard):
149
157
  return {
150
158
  "name": self.name,
151
159
  "fullname": self.fullname,
160
+ "metadata": self.metadata,
152
161
  "product": {"id": product_id},
153
162
  }
154
163
 
@@ -248,12 +257,14 @@ class StatusBoardExporterIntegration(QontractReconcileIntegration):
248
257
  return QONTRACT_INTEGRATION
249
258
 
250
259
  @staticmethod
251
- def get_product_apps(sb: StatusBoardV1) -> dict[str, set[str]]:
260
+ def get_product_apps(
261
+ sb: StatusBoardV1,
262
+ ) -> dict[str, dict[str, dict[str, dict[str, set[str]]]]]:
252
263
  global_selectors = (
253
264
  sb.global_app_selectors.exclude or [] if sb.global_app_selectors else []
254
265
  )
255
266
  return {
256
- p.product_environment.product.name: get_selected_app_names(
267
+ p.product_environment.product.name: get_selected_app_data(
257
268
  global_selectors, p
258
269
  )
259
270
  for p in sb.products
@@ -287,7 +298,7 @@ class StatusBoardExporterIntegration(QontractReconcileIntegration):
287
298
 
288
299
  @staticmethod
289
300
  def desired_abstract_status_board_map(
290
- desired_product_apps: Mapping[str, set[str]],
301
+ desired_product_apps: Mapping[str, dict[str, dict[str, dict[str, set[str]]]]],
291
302
  slodocs: list[SLODocumentV1],
292
303
  ) -> dict[str, AbstractStatusBoard]:
293
304
  """
@@ -299,7 +310,11 @@ class StatusBoardExporterIntegration(QontractReconcileIntegration):
299
310
  desired_abstract_status_board_map: dict[str, AbstractStatusBoard] = {}
300
311
  for product_name, apps in desired_product_apps.items():
301
312
  product = Product(
302
- id=None, name=product_name, fullname=product_name, applications=[]
313
+ id=None,
314
+ name=product_name,
315
+ fullname=product_name,
316
+ applications=[],
317
+ metadata={},
303
318
  )
304
319
  desired_abstract_status_board_map[product_name] = product
305
320
  for a in apps:
@@ -310,6 +325,7 @@ class StatusBoardExporterIntegration(QontractReconcileIntegration):
310
325
  fullname=key,
311
326
  services=[],
312
327
  product=product,
328
+ metadata=apps[a]["metadata"],
313
329
  )
314
330
  for slodoc in slodocs:
315
331
  products = [
@@ -373,6 +389,99 @@ class StatusBoardExporterIntegration(QontractReconcileIntegration):
373
389
 
374
390
  return return_value
375
391
 
392
+ @staticmethod
393
+ def _compare_metadata(
394
+ current_metadata: dict[str, Any] | ServiceMetadataSpec,
395
+ desired_metadata: dict[str, Any] | ServiceMetadataSpec,
396
+ ) -> bool:
397
+ """
398
+ Compare metadata dictionaries with deep equality checking for nested structures.
399
+
400
+ :param current_metadata: The current metadata dictionary
401
+ :param desired_metadata: The desired metadata dictionary
402
+ :return: True if metadata are equal, False otherwise
403
+ """
404
+ # Convert TypedDict to regular dict to allow variable key access
405
+ current_dict = dict(current_metadata)
406
+ desired_dict = dict(desired_metadata)
407
+
408
+ if current_dict.keys() != desired_dict.keys():
409
+ return False
410
+
411
+ for key, current_value in current_dict.items():
412
+ desired_value = desired_dict[key]
413
+
414
+ # Handle None values
415
+ if current_value is None or desired_value is None:
416
+ if current_value is not desired_value:
417
+ return False
418
+ continue
419
+
420
+ # Handle sets
421
+ if isinstance(current_value, set) and isinstance(desired_value, set):
422
+ if current_value != desired_value:
423
+ return False
424
+ # Handle lists and tuples
425
+ elif isinstance(current_value, (list, tuple)) and isinstance(
426
+ desired_value, (list, tuple)
427
+ ):
428
+ if len(current_value) != len(desired_value):
429
+ return False
430
+
431
+ try:
432
+ sorted_current = sorted(current_value, key=repr)
433
+ sorted_desired = sorted(desired_value, key=repr)
434
+ except Exception:
435
+ # Fallback: compare without sorting
436
+ sorted_current = list(current_value)
437
+ sorted_desired = list(desired_value)
438
+
439
+ for c, d in zip(sorted_current, sorted_desired, strict=True):
440
+ if isinstance(c, dict) and isinstance(d, dict):
441
+ if not StatusBoardExporterIntegration._compare_metadata(c, d):
442
+ return False
443
+ elif isinstance(c, (list, tuple)) and isinstance(d, (list, tuple)):
444
+ if not StatusBoardExporterIntegration._compare_metadata(
445
+ {"x": c}, {"x": d}
446
+ ):
447
+ return False
448
+ elif c != d:
449
+ return False
450
+ # Handle nested dictionaries
451
+ elif isinstance(current_value, dict) and isinstance(desired_value, dict):
452
+ if not StatusBoardExporterIntegration._compare_metadata(
453
+ current_value, desired_value
454
+ ):
455
+ return False
456
+ # Handle primitive types
457
+ elif current_value != desired_value:
458
+ return False
459
+
460
+ return True
461
+
462
+ @staticmethod
463
+ def _status_board_objects_equal(
464
+ current: AbstractStatusBoard, desired: AbstractStatusBoard
465
+ ) -> bool:
466
+ """
467
+ Check if two AbstractStatusBoard objects are equal, including metadata comparison.
468
+
469
+ :param current: The current AbstractStatusBoard object
470
+ :param desired: The desired AbstractStatusBoard object
471
+ :return: True if objects are equal, False otherwise
472
+ """
473
+ # Check basic equality first (name, fullname)
474
+ if current.name != desired.name or current.fullname != desired.fullname:
475
+ return False
476
+
477
+ # Compare metadata with deep equality
478
+ if current.metadata and desired.metadata:
479
+ return StatusBoardExporterIntegration._compare_metadata(
480
+ current.metadata, desired.metadata
481
+ )
482
+ else:
483
+ return True
484
+
376
485
  @staticmethod
377
486
  def get_diff(
378
487
  desired_abstract_status_board_map: Mapping[str, AbstractStatusBoard],
@@ -383,6 +492,7 @@ class StatusBoardExporterIntegration(QontractReconcileIntegration):
383
492
  diff_result = diff_mappings(
384
493
  current_abstract_status_board_map,
385
494
  desired_abstract_status_board_map,
495
+ equal=StatusBoardExporterIntegration._status_board_objects_equal,
386
496
  )
387
497
 
388
498
  for pair in chain(diff_result.identical.values(), diff_result.change.values()):
@@ -398,6 +508,18 @@ class StatusBoardExporterIntegration(QontractReconcileIntegration):
398
508
  for o in diff_result.delete.values()
399
509
  )
400
510
 
511
+ # Handle Applications that need updates (metadata changes)
512
+ applications_to_update = [
513
+ a.desired
514
+ for _, a in diff_result.change.items()
515
+ if isinstance(a.desired, Application)
516
+ ]
517
+
518
+ return_list.extend(
519
+ StatusBoardHandler(action=Action.update, status_board_object=a)
520
+ for a in applications_to_update
521
+ )
522
+
401
523
  services_to_update = [
402
524
  s.desired
403
525
  for _, s in diff_result.change.items()
@@ -445,7 +567,9 @@ class StatusBoardExporterIntegration(QontractReconcileIntegration):
445
567
  ocm_api = init_ocm_base_client(sb.ocm, self.secret_reader)
446
568
 
447
569
  # Desired state
448
- desired_product_apps: dict[str, set[str]] = self.get_product_apps(sb)
570
+ desired_product_apps: dict[
571
+ str, dict[str, dict[str, dict[str, set[str]]]]
572
+ ] = self.get_product_apps(sb)
449
573
  desired_abstract_status_board_map = self.desired_abstract_status_board_map(
450
574
  desired_product_apps, slodocs
451
575
  )
@@ -9,6 +9,10 @@ from reconcile.gql_definitions.status_board.status_board import (
9
9
  query,
10
10
  )
11
11
  from reconcile.utils import gql
12
+ from reconcile.utils.ocm.status_board import (
13
+ METADATA_MANAGED_BY_KEY,
14
+ METADATA_MANAGED_BY_VALUE,
15
+ )
12
16
 
13
17
 
14
18
  def get_status_board(
@@ -19,11 +23,11 @@ def get_status_board(
19
23
  return query(query_func).status_board_v1 or []
20
24
 
21
25
 
22
- def get_selected_app_names(
26
+ def get_selected_app_data(
23
27
  global_selectors: Iterable[str],
24
28
  product: StatusBoardProductV1,
25
- ) -> set[str]:
26
- selected_app_names: set[str] = set()
29
+ ) -> dict[str, dict[str, dict[str, set[str]]]]:
30
+ selected_app_data: dict[str, dict[str, dict[str, Any]]] = {}
27
31
 
28
32
  apps: dict[str, Any] = {"apps": []}
29
33
  for namespace in product.product_environment.namespaces or []:
@@ -31,15 +35,45 @@ def get_selected_app_names(
31
35
  if namespace.app.parent_app:
32
36
  prefix = f"{namespace.app.parent_app.name}-"
33
37
  name = f"{prefix}{namespace.app.name}"
34
- selected_app_names.add(name)
38
+
39
+ deployment_saas_files = set()
40
+ if namespace.app.saas_files:
41
+ deployment_saas_files = {
42
+ saas_file.name
43
+ for saas_file in namespace.app.saas_files
44
+ if "Deployment" in saas_file.managed_resource_types
45
+ or "ClowdApp" in saas_file.managed_resource_types
46
+ }
47
+
48
+ selected_app_data[name] = {
49
+ "metadata": {
50
+ METADATA_MANAGED_BY_KEY: METADATA_MANAGED_BY_VALUE,
51
+ "deploymentSaasFiles": set(deployment_saas_files),
52
+ },
53
+ }
54
+
35
55
  app = namespace.app.dict(by_alias=True)
36
56
  app["name"] = name
37
57
  apps["apps"].append(app)
38
58
 
39
59
  for child in namespace.app.children_apps or []:
40
60
  name = f"{namespace.app.name}-{child.name}"
41
- if name not in selected_app_names:
42
- selected_app_names.add(f"{namespace.app.name}-{child.name}")
61
+ if name not in selected_app_data:
62
+ deployment_saas_files = set()
63
+ if child.saas_files:
64
+ deployment_saas_files = {
65
+ saas_file.name
66
+ for saas_file in child.saas_files
67
+ if "Deployment" in saas_file.managed_resource_types
68
+ }
69
+
70
+ selected_app_data[name] = {
71
+ "metadata": {
72
+ METADATA_MANAGED_BY_KEY: METADATA_MANAGED_BY_VALUE,
73
+ "deploymentSaasFiles": set(deployment_saas_files),
74
+ },
75
+ }
76
+
43
77
  child_dict = child.dict(by_alias=True)
44
78
  child_dict["name"] = name
45
79
  apps["apps"].append(child_dict)
@@ -52,6 +86,7 @@ def get_selected_app_names(
52
86
  apps_to_remove: set[str] = set()
53
87
  results = parser.parse(selector).find(apps)
54
88
  apps_to_remove.update(match.value["name"] for match in results)
55
- selected_app_names -= apps_to_remove
89
+ for app_name in apps_to_remove:
90
+ selected_app_data.pop(app_name, None)
56
91
 
57
- return selected_app_names
92
+ return selected_app_data
@@ -21,6 +21,7 @@ class IDSpec(TypedDict):
21
21
 
22
22
 
23
23
  class ApplicationOCMSpec(BaseOCMSpec):
24
+ metadata: dict[str, Any]
24
25
  product: IDSpec
25
26
 
26
27
 
@@ -117,6 +118,18 @@ def create_service(ocm_api: OCMBaseClient, spec: ServiceOCMSpec) -> str:
117
118
  return resp["id"]
118
119
 
119
120
 
121
+ def update_application(
122
+ ocm_api: OCMBaseClient,
123
+ application_id: str,
124
+ spec: ApplicationOCMSpec,
125
+ ) -> None:
126
+ data = spec | {
127
+ "metadata": spec.get("metadata", {})
128
+ | {METADATA_MANAGED_BY_KEY: METADATA_MANAGED_BY_VALUE}
129
+ }
130
+ ocm_api.patch(f"/api/status-board/v1/applications/{application_id}", data=data)
131
+
132
+
120
133
  def update_service(
121
134
  ocm_api: OCMBaseClient,
122
135
  service_id: str,
@@ -5563,22 +5563,27 @@ class TerrascriptClient:
5563
5563
 
5564
5564
  # mutual authentication section
5565
5565
  if mutual_authentication := resource.get("mutual_authentication"):
5566
- trust_store_values = {
5567
- "ca_certificates_bundle_s3_bucket": mutual_authentication[
5568
- "ca_cert_bundle_s3_bucket_name"
5569
- ],
5570
- "ca_certificates_bundle_s3_key": mutual_authentication[
5571
- "ca_cert_bundle_s3_bucket_key"
5572
- ],
5573
- }
5574
- trust_store = aws_lb_trust_store(
5575
- f"{identifier}-trust-store", **trust_store_values
5576
- )
5577
- tf_resources.append(trust_store)
5578
- values["mutual_authentication"] = {
5579
- "mode": mutual_authentication["mode"],
5580
- "trust_store_arn": f"${{{trust_store.arn}}}",
5581
- }
5566
+ if mutual_authentication["mode"] in {"off", "passthrough"}:
5567
+ values["mutual_authentication"] = {
5568
+ "mode": mutual_authentication["mode"],
5569
+ }
5570
+ else:
5571
+ trust_store_values = {
5572
+ "ca_certificates_bundle_s3_bucket": mutual_authentication[
5573
+ "ca_cert_bundle_s3_bucket_name"
5574
+ ],
5575
+ "ca_certificates_bundle_s3_key": mutual_authentication[
5576
+ "ca_cert_bundle_s3_bucket_key"
5577
+ ],
5578
+ }
5579
+ trust_store = aws_lb_trust_store(
5580
+ f"{identifier}-trust-store", **trust_store_values
5581
+ )
5582
+ tf_resources.append(trust_store)
5583
+ values["mutual_authentication"] = {
5584
+ "mode": mutual_authentication["mode"],
5585
+ "trust_store_arn": f"${{{trust_store.arn}}}",
5586
+ }
5582
5587
 
5583
5588
  forward_identifier = f"{identifier}-forward"
5584
5589
  forward_lbl_tf_resource = aws_lb_listener(forward_identifier, **values)
tools/qontract_cli.py CHANGED
@@ -2772,7 +2772,7 @@ def slo_document_services(ctx: click.Context, status_board_instance: str) -> Non
2772
2772
  print(f"Status-board instance '{status_board_instance}' not found.")
2773
2773
  sys.exit(1)
2774
2774
 
2775
- desired_product_apps: dict[str, set[str]] = (
2775
+ desired_product_apps: dict[str, dict[str, dict[str, dict[str, set[str]]]]] = (
2776
2776
  StatusBoardExporterIntegration.get_product_apps(sb)
2777
2777
  )
2778
2778