qontract-reconcile 0.10.2.dev292__py3-none-any.whl → 0.10.2.dev293__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.dev292
3
+ Version: 0.10.2.dev293
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
@@ -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=3-WcCYBV8GP1OH1_NLgu9yyXNSNZDJ5_1dZIiTCWhwM,2346833
216
+ reconcile/gql_definitions/introspection.json,sha256=CQAoUjEbdjRlHyYeLjs1PTMiP_RQrPzD0yCj08aYawA,2348381
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
@@ -409,8 +409,8 @@ reconcile/gql_definitions/status_board/status_board.py,sha256=BYq-1g_-AjNKHIKmY2
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
412
- reconcile/gql_definitions/templating/template_collection.py,sha256=I9razKZ9BThTn1HZW1AamwF0EKXxLACL30DxGxev-rA,4045
413
- reconcile/gql_definitions/templating/templates.py,sha256=bV1JWwxDW8opARBOrZ_h5NBsJfM-9aL-povmTZnpTOk,3296
412
+ reconcile/gql_definitions/templating/template_collection.py,sha256=alQA1qLhL0nOFcnA0O2Z7HCuuAviZXM66t6yeur9XWo,4123
413
+ reconcile/gql_definitions/templating/templates.py,sha256=rQ14gs5dMttxVuW5q26p23Y8c5vCNh8dSec9ucTGlKc,3372
414
414
  reconcile/gql_definitions/terraform_cloudflare_dns/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
415
415
  reconcile/gql_definitions/terraform_cloudflare_dns/app_interface_cloudflare_dns_settings.py,sha256=eyGX9HcTF6MZbOYZ6Kl6Mg3k6nJTUtwqs9gDxBP_8Dk,1920
416
416
  reconcile/gql_definitions/terraform_cloudflare_dns/terraform_cloudflare_zones.py,sha256=dBQ2tyAp-eRZs59mguaTc6-x67JUoSxtZ8mOjbRqDuc,5832
@@ -507,12 +507,12 @@ reconcile/templates/jira-checkpoint-missinginfo.j2,sha256=c_Vvg-lEENsB3tgxm9B6Y9
507
507
  reconcile/templates/rosa-classic-cluster-creation.sh.j2,sha256=M-nzp-GtkQNRe8rdoDAndSKJSJhcJwNFKeql-JP2W7M,2094
508
508
  reconcile/templates/rosa-hcp-cluster-creation.sh.j2,sha256=eeT7xUhmz7Q_HtJOQALF5snZE8cUPGkIh6WUlcBHhhs,2349
509
509
  reconcile/templating/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
510
- reconcile/templating/renderer.py,sha256=VA_lMMtsyEFc0Cwf0jehQdv3tMEJtfyZMzBgA5us5ME,14856
510
+ reconcile/templating/renderer.py,sha256=YVKhk1piAnk3faT81oeZeMHnFV78Mt-_wgtIC693vtE,14907
511
511
  reconcile/templating/validator.py,sha256=5f9f35PCHOOdjb7KZquL2YdabyuAUokPDa4xutSEHIQ,5360
512
512
  reconcile/templating/lib/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
513
513
  reconcile/templating/lib/merge_request_manager.py,sha256=XwpOR4rVS9ZiJ_Mn8qfCXPZ7CRLMGSJgeIkqD-8Jhgc,5228
514
514
  reconcile/templating/lib/model.py,sha256=YVUIXuPny3_kpFgBMSud8q_ndY5o882wKiX0l0A14L4,481
515
- reconcile/templating/lib/rendering.py,sha256=IzTbXJ5cO0c9mV6P9HrstOo79ovOcoNVtmyc6RgfMe0,6241
515
+ reconcile/templating/lib/rendering.py,sha256=qjs7WAVSvWH4edbBk6Aaql4ZT_OG9W7L8qi_YeJkHVI,6973
516
516
  reconcile/terraform_init/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
517
517
  reconcile/terraform_init/integration.py,sha256=pPi4YAjbEE8vDaaRizGf-d-PewqqSJmjcLgAsWFS7G0,6236
518
518
  reconcile/terraform_init/merge_request.py,sha256=3CYtgSd7Q9zjKg4wsDz437EPCRfGeZZ8fZ0Y-ChKXJY,1475
@@ -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.dev292.dist-info/METADATA,sha256=ihGz58VKwtQQvhSNIkYsi1RwyscQpXOvPCSEIw9DpfM,24916
800
- qontract_reconcile-0.10.2.dev292.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
801
- qontract_reconcile-0.10.2.dev292.dist-info/entry_points.txt,sha256=5i9l54La3vQrDLAdwDKQWC0iG4sV9RRfOb1BpvzOWLc,698
802
- qontract_reconcile-0.10.2.dev292.dist-info/RECORD,,
799
+ qontract_reconcile-0.10.2.dev293.dist-info/METADATA,sha256=1dwm4wJoCGKDdjglqMJw3fdWAvuquaPeE2BIpH7wkUc,24916
800
+ qontract_reconcile-0.10.2.dev293.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
801
+ qontract_reconcile-0.10.2.dev293.dist-info/entry_points.txt,sha256=5i9l54La3vQrDLAdwDKQWC0iG4sV9RRfOb1BpvzOWLc,698
802
+ qontract_reconcile-0.10.2.dev293.dist-info/RECORD,,
@@ -6451,6 +6451,18 @@
6451
6451
  "isDeprecated": false,
6452
6452
  "deprecationReason": null
6453
6453
  },
6454
+ {
6455
+ "name": "externalOrgId",
6456
+ "description": null,
6457
+ "args": [],
6458
+ "type": {
6459
+ "kind": "SCALAR",
6460
+ "name": "String",
6461
+ "ofType": null
6462
+ },
6463
+ "isDeprecated": false,
6464
+ "deprecationReason": null
6465
+ },
6454
6466
  {
6455
6467
  "name": "environment",
6456
6468
  "description": null,
@@ -34178,6 +34190,18 @@
34178
34190
  "isDeprecated": false,
34179
34191
  "deprecationReason": null
34180
34192
  },
34193
+ {
34194
+ "name": "overwrite",
34195
+ "description": null,
34196
+ "args": [],
34197
+ "type": {
34198
+ "kind": "SCALAR",
34199
+ "name": "Boolean",
34200
+ "ofType": null
34201
+ },
34202
+ "isDeprecated": false,
34203
+ "deprecationReason": null
34204
+ },
34181
34205
  {
34182
34206
  "name": "patch",
34183
34207
  "description": null,
@@ -41014,6 +41038,18 @@
41014
41038
  },
41015
41039
  "isDeprecated": false,
41016
41040
  "deprecationReason": null
41041
+ },
41042
+ {
41043
+ "name": "promtool_version",
41044
+ "description": null,
41045
+ "args": [],
41046
+ "type": {
41047
+ "kind": "SCALAR",
41048
+ "name": "String",
41049
+ "ofType": null
41050
+ },
41051
+ "isDeprecated": false,
41052
+ "deprecationReason": null
41017
41053
  }
41018
41054
  ],
41019
41055
  "inputFields": null,
@@ -40,6 +40,7 @@ query TemplateCollection_v1($name: String) {
40
40
  autoApproved
41
41
  condition
42
42
  targetPath
43
+ overwrite
43
44
  patch {
44
45
  path
45
46
  identifier
@@ -92,6 +93,7 @@ class TemplateV1(ConfiguredBaseModel):
92
93
  auto_approved: Optional[bool] = Field(..., alias="autoApproved")
93
94
  condition: Optional[str] = Field(..., alias="condition")
94
95
  target_path: str = Field(..., alias="targetPath")
96
+ overwrite: Optional[bool] = Field(..., alias="overwrite")
95
97
  patch: Optional[TemplatePatchV1] = Field(..., alias="patch")
96
98
  template: str = Field(..., alias="template")
97
99
  template_render_options: Optional[TemplateRenderOptionsV1] = Field(..., alias="templateRenderOptions")
@@ -24,6 +24,7 @@ query Templatev1 {
24
24
  name
25
25
  autoApproved
26
26
  condition
27
+ overwrite
27
28
  patch {
28
29
  path
29
30
  identifier
@@ -78,6 +79,7 @@ class TemplateV1(ConfiguredBaseModel):
78
79
  name: str = Field(..., alias="name")
79
80
  auto_approved: Optional[bool] = Field(..., alias="autoApproved")
80
81
  condition: Optional[str] = Field(..., alias="condition")
82
+ overwrite: Optional[bool] = Field(..., alias="overwrite")
81
83
  patch: Optional[TemplatePatchV1] = Field(..., alias="patch")
82
84
  target_path: str = Field(..., alias="targetPath")
83
85
  template: str = Field(..., alias="template")
@@ -1,5 +1,6 @@
1
1
  import logging
2
2
  from abc import ABC, abstractmethod
3
+ from functools import cached_property
3
4
  from io import StringIO
4
5
  from typing import Any, Protocol
5
6
 
@@ -33,6 +34,7 @@ class Template(Protocol):
33
34
  condition: str | None
34
35
  target_path: str
35
36
  template: str
37
+ overwrite: bool | None
36
38
 
37
39
  def dict(self) -> dict[str, str]: ...
38
40
 
@@ -77,14 +79,28 @@ class Renderer(ABC):
77
79
  """
78
80
  pass
79
81
 
82
+ @abstractmethod
83
+ def target_exist(self) -> bool:
84
+ """
85
+ if target (file or patch block) already exists.
86
+ """
87
+ pass
88
+
80
89
  def render_target_path(self) -> str:
81
90
  return self._render_template(self.template.target_path).strip()
82
91
 
83
92
  def render_condition(self) -> bool:
84
- return self._render_template(self.template.condition or "True") == "True"
93
+ if self._render_template(self.template.condition or "True") != "True":
94
+ return False
95
+ if self.template.overwrite:
96
+ return True
97
+ return not self.target_exist()
85
98
 
86
99
 
87
100
  class FullRenderer(Renderer):
101
+ def target_exist(self) -> bool:
102
+ return self.data.current is not None
103
+
88
104
  def render_output(self) -> str:
89
105
  """
90
106
  Take the variables from Template Data and render the template with it.
@@ -95,6 +111,12 @@ class FullRenderer(Renderer):
95
111
 
96
112
 
97
113
  class PatchRenderer(Renderer):
114
+ def target_exist(self) -> bool:
115
+ if isinstance(self._matched_value, list):
116
+ dta_identifier = self._get_identifier(self._data_to_add)
117
+ return self._find_index(self._matched_value, dta_identifier) is not None
118
+ return True
119
+
98
120
  def render_output(self) -> str:
99
121
  """
100
122
  Takes the variables from Template Data and render the template with it.
@@ -105,70 +127,69 @@ class PatchRenderer(Renderer):
105
127
 
106
128
  This method returns the entire file as a string.
107
129
  """
108
- if self.template.patch is None: # here to satisfy mypy
109
- raise ValueError("PatchRenderer requires a patch")
110
-
111
- p = parse_jsonpath(self._render_template(self.template.patch.path))
112
-
113
- matched_values = [match.value for match in p.find(self.data.current)]
114
-
115
- if len(matched_values) != 1:
116
- raise ValueError(
117
- f"Expected exactly one match for {self.template.patch.path}, got {len(matched_values)}"
118
- )
119
- matched_value = matched_values[0]
120
-
121
- data_to_add = self.ruamel_instance.load(
122
- self._render_template(self.template.template)
123
- )
124
-
125
- if isinstance(matched_value, list):
126
-
127
- def get_identifier(data: dict[str, Any]) -> Any:
128
- assert self.template.patch is not None # mypy
129
- if not self.template.patch.identifier:
130
- raise ValueError(
131
- f"Expected identifier in patch for list at {self.template}"
132
- )
133
- if self.template.patch.identifier.startswith(
134
- "$"
135
- ) and not self.template.patch.identifier.startswith("$ref"):
136
- # jsonpath and list of strings support
137
- if matches := [
138
- match.value
139
- for match in parse_jsonpath(
140
- self.template.patch.identifier
141
- ).find(data)
142
- ]:
143
- return matches[0]
144
- return None
145
- return data.get(self.template.patch.identifier)
146
-
147
- dta_identifier = get_identifier(data_to_add)
130
+ if isinstance(self._matched_value, list):
131
+ dta_identifier = self._get_identifier(self._data_to_add)
148
132
  if not dta_identifier:
133
+ assert self.template.patch is not None # mypy
149
134
  raise ValueError(
150
135
  f"Expected identifier {self.template.patch.identifier} in data to add"
151
136
  )
152
-
153
- index = next(
154
- (
155
- index
156
- for index, data in enumerate(matched_value)
157
- if get_identifier(data) == dta_identifier
158
- ),
159
- None,
160
- )
161
- if index is None:
162
- matched_value.append(data_to_add)
137
+ if (
138
+ index := self._find_index(self._matched_value, dta_identifier)
139
+ ) is not None:
140
+ self._matched_value[index] = self._data_to_add
163
141
  else:
164
- matched_value[index] = data_to_add
142
+ self._matched_value.append(self._data_to_add)
165
143
  else:
166
- matched_value.update(data_to_add)
144
+ self._matched_value.update(self._data_to_add)
167
145
 
168
146
  with StringIO() as s:
169
147
  self.ruamel_instance.dump(self.data.current, s)
170
148
  return s.getvalue()
171
149
 
150
+ @cached_property
151
+ def _matched_value(self) -> Any:
152
+ assert self.template.patch is not None # mypy
153
+ p = parse_jsonpath(self._render_template(self.template.patch.path))
154
+ matched_values = [match.value for match in p.find(self.data.current)]
155
+ if len(matched_values) != 1:
156
+ raise ValueError(
157
+ f"Expected exactly one match for {self.template.patch.path}, got {len(matched_values)}"
158
+ )
159
+ return matched_values[0]
160
+
161
+ @cached_property
162
+ def _data_to_add(self) -> Any:
163
+ return self.ruamel_instance.load(self._render_template(self.template.template))
164
+
165
+ def _get_identifier(self, data: dict[str, Any]) -> Any:
166
+ assert self.template.patch is not None # mypy
167
+ if not self.template.patch.identifier:
168
+ raise ValueError(
169
+ f"Expected identifier in patch for list at {self.template}"
170
+ )
171
+ if self.template.patch.identifier.startswith(
172
+ "$"
173
+ ) and not self.template.patch.identifier.startswith("$ref"):
174
+ # jsonpath and list of strings support
175
+ if matches := [
176
+ match.value
177
+ for match in parse_jsonpath(self.template.patch.identifier).find(data)
178
+ ]:
179
+ return matches[0]
180
+ return None
181
+ return data.get(self.template.patch.identifier)
182
+
183
+ def _find_index(
184
+ self,
185
+ matched_value: list,
186
+ dta_identifier: Any,
187
+ ) -> int | None:
188
+ for index, data in enumerate(matched_value):
189
+ if self._get_identifier(data) == dta_identifier:
190
+ return index
191
+ return None
192
+
172
193
 
173
194
  def create_renderer(
174
195
  template: Template,
@@ -104,14 +104,17 @@ class LocalFilePersistence(FilePersistence):
104
104
  def read(self, path: str) -> str | None:
105
105
  return self._read_local_file(join_path(self.app_interface_data_path, path))
106
106
 
107
- def __exit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> None:
108
- if self.dry_run:
109
- return
107
+ def flush(self) -> None:
110
108
  for output in self.outputs:
111
109
  filepath = Path(join_path(self.app_interface_data_path, output.path))
112
110
  filepath.parent.mkdir(parents=True, exist_ok=True)
113
111
  filepath.write_text(output.content, encoding="utf-8")
114
112
 
113
+ def __exit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> None:
114
+ if self.dry_run:
115
+ return
116
+ self.flush()
117
+
115
118
 
116
119
  class PersistenceTransaction(FilePersistence):
117
120
  """