qontract-reconcile 0.10.2.dev245__py3-none-any.whl → 0.10.2.dev246__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.dev245
3
+ Version: 0.10.2.dev246
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
@@ -616,7 +616,7 @@ reconcile/utils/helm.py,sha256=wC1h0GylhDFeZ6hZEtYy2giAGIIQroaQhkAtURoSlI8,3893
616
616
  reconcile/utils/helpers.py,sha256=koyAtYnxsUVx-HIn6GpedcUE-ekz_VtoYDkiZ0iv8ik,1795
617
617
  reconcile/utils/imap_client.py,sha256=h8YDiCSCvroErhpH_-KGYI7Y2WU2Q2oSpuxDFbOkSbY,1989
618
618
  reconcile/utils/instrumented_wrappers.py,sha256=VqT4s0Bdicv224-uSeSaugtHXm-xJ3oSeBiqj0QQRiU,1942
619
- reconcile/utils/jenkins_api.py,sha256=3wm29isLhhK0E9MB3v25SciC9OxmOl1Omtv0s3cfspI,6735
619
+ reconcile/utils/jenkins_api.py,sha256=jNwdtBtO8DgMW_H8XfqkQs2r4JsLovHe03t5_F3M1xg,7961
620
620
  reconcile/utils/jira_client.py,sha256=jj7E58PGssoCUEhUZpXoVYeyuChjrvjcg1AdNsONXO0,10545
621
621
  reconcile/utils/jjb_client.py,sha256=e5cDeNAeJMGz3sZMJ1KUIMFyLdRet0YnC0Qgj1vTPHc,15239
622
622
  reconcile/utils/jsonpath.py,sha256=wdxOMqR-GMpQf5vRPWRMqAF7bCiXDBkkcFfY2U4j_tk,5536
@@ -750,9 +750,9 @@ reconcile/utils/runtime/meta.py,sha256=dWdKS9eHVuowFkTK4lgXJ723vS1y9giOMzePUKnHn
750
750
  reconcile/utils/runtime/runner.py,sha256=I30KRrX1UQbHc_Ir1cIZX3OfNSdoHKdnDSPAEB69Ilk,7944
751
751
  reconcile/utils/runtime/sharding.py,sha256=r0ieUtNed7NvknSw6qQrCkKpVXE1shuHGnfFcnpA_k4,16142
752
752
  reconcile/utils/saasherder/__init__.py,sha256=3U8plqMAPRE1kjwZ5YnIsYsggTf4_gS7flRUEuXVBAs,343
753
- reconcile/utils/saasherder/interfaces.py,sha256=nbGVLiIXJvOtd5ZfKsP3bfrFbMpdQ02D0cTTM9rrED0,9286
754
- reconcile/utils/saasherder/models.py,sha256=qMYY3SBOEnQlaOqn3bQhV33LDIMLcPjWbtfU9Li8-f0,10986
755
- reconcile/utils/saasherder/saasherder.py,sha256=ucDakXbUCRfyiEbdTOw8y1-qE_6bpBdts6EBjk9PRUE,91789
753
+ reconcile/utils/saasherder/interfaces.py,sha256=JEgR9Co2RxCEJZBBzIrcOURHeq1j-QO6rAUY5yNtsRA,9496
754
+ reconcile/utils/saasherder/models.py,sha256=uaurkt150llOAPvuZUBoG4VonOt94Ep1BO1AA6Tmjbc,12405
755
+ reconcile/utils/saasherder/saasherder.py,sha256=p87JKuQYzqjMQg62GJzqT9S0909XTWtmM7naFNXiG1w,92546
756
756
  reconcile/utils/terraform/__init__.py,sha256=zNbiyTWo35AT1sFTElL2j_AA0jJ_yWE_bfFn-nD2xik,250
757
757
  reconcile/utils/terraform/config.py,sha256=5UVrd563TMcvi4ooa5JvWVDW1I3bIWg484u79evfV_8,164
758
758
  reconcile/utils/terraform/config_client.py,sha256=gRL1rQ0AqvShei_rcGqC3HDYGskOFKE1nPrJyJE9yno,4676
@@ -798,7 +798,7 @@ tools/saas_promotion_state/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJ
798
798
  tools/saas_promotion_state/saas_promotion_state.py,sha256=UfwwRLS5Ya4_Nh1w5n1dvoYtchQvYE9yj1VANt2IKqI,3925
799
799
  tools/sre_checkpoints/__init__.py,sha256=CDaDaywJnmRCLyl_NCcvxi-Zc0hTi_3OdwKiFOyS39I,145
800
800
  tools/sre_checkpoints/util.py,sha256=zEDbGr18ZeHNQwW8pUsr2JRjuXIPz--WAGJxZo9sv_Y,894
801
- qontract_reconcile-0.10.2.dev245.dist-info/METADATA,sha256=xxZiW8qJRkrhvX6d89G8EIDt9m9ZIQ4xrQEzSTd_LdQ,24049
802
- qontract_reconcile-0.10.2.dev245.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
803
- qontract_reconcile-0.10.2.dev245.dist-info/entry_points.txt,sha256=5i9l54La3vQrDLAdwDKQWC0iG4sV9RRfOb1BpvzOWLc,698
804
- qontract_reconcile-0.10.2.dev245.dist-info/RECORD,,
801
+ qontract_reconcile-0.10.2.dev246.dist-info/METADATA,sha256=SMoEdXG2JlaoCPrpZYIYSDQW951JukmA_fJxy99bD6g,24049
802
+ qontract_reconcile-0.10.2.dev246.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
803
+ qontract_reconcile-0.10.2.dev246.dist-info/entry_points.txt,sha256=5i9l54La3vQrDLAdwDKQWC0iG4sV9RRfOb1BpvzOWLc,698
804
+ qontract_reconcile-0.10.2.dev246.dist-info/RECORD,,
@@ -1,5 +1,6 @@
1
1
  import logging
2
- from typing import Any
2
+ from collections.abc import Mapping
3
+ from typing import Any, NotRequired, Self, TypedDict
3
4
 
4
5
  import requests
5
6
  import toml
@@ -9,23 +10,40 @@ from sretoolbox.utils import retry
9
10
  from reconcile.utils.secret_reader import SecretReaderBase
10
11
 
11
12
 
13
+ class JobBuildState(TypedDict):
14
+ _class: NotRequired[str]
15
+ number: int
16
+ result: NotRequired[str | None]
17
+ actions: NotRequired[list]
18
+ commit_sha: NotRequired[str]
19
+
20
+
12
21
  class JenkinsApi:
13
22
  """Wrapper around Jenkins API calls"""
14
23
 
15
- @staticmethod
24
+ @classmethod
16
25
  def init_jenkins_from_secret(
17
- secret_reader: SecretReaderBase, secret, ssl_verify=True
18
- ) -> "JenkinsApi":
26
+ cls,
27
+ secret_reader: SecretReaderBase,
28
+ secret: Mapping[str, Any],
29
+ ssl_verify: bool = True,
30
+ ) -> Self:
19
31
  token_config = secret_reader.read(secret)
20
32
  config = toml.loads(token_config)
21
- return JenkinsApi(
33
+ return cls(
22
34
  config["jenkins"]["url"],
23
35
  config["jenkins"]["user"],
24
36
  config["jenkins"]["password"],
25
37
  ssl_verify=ssl_verify,
26
38
  )
27
39
 
28
- def __init__(self, url: str, user: str, password: str, ssl_verify=True):
40
+ def __init__(
41
+ self,
42
+ url: str,
43
+ user: str,
44
+ password: str,
45
+ ssl_verify: bool = True,
46
+ ):
29
47
  self.url = url
30
48
  self.user = user
31
49
  self.password = password
@@ -43,7 +61,7 @@ class JenkinsApi:
43
61
  res.raise_for_status()
44
62
  return yaml.safe_load(res.text)
45
63
 
46
- def apply_jcasc_config(self, config: dict[str, Any]):
64
+ def apply_jcasc_config(self, config: dict[str, Any]) -> None:
47
65
  url = f"{self.url}/manage/configuration-as-code/apply"
48
66
  res = requests.post(
49
67
  url,
@@ -54,7 +72,7 @@ class JenkinsApi:
54
72
  )
55
73
  res.raise_for_status()
56
74
 
57
- def get_job_names(self):
75
+ def get_job_names(self) -> list[str]:
58
76
  url = f"{self.url}/api/json?tree=jobs[name]"
59
77
  res = requests.get(
60
78
  url, verify=self.ssl_verify, auth=(self.user, self.password), timeout=60
@@ -64,36 +82,52 @@ class JenkinsApi:
64
82
  job_names = [r["name"] for r in res.json()["jobs"]]
65
83
  return job_names
66
84
 
85
+ @staticmethod
86
+ def _get_commit_sha_from_build(build: Mapping[str, Any]) -> str | None:
87
+ for action in reversed(build.get("actions", [])):
88
+ if revision := action.get("lastBuiltRevision"):
89
+ return revision["SHA1"]
90
+ return None
91
+
92
+ def _build_job_build_state(self, build: Mapping) -> JobBuildState:
93
+ job_build_state = JobBuildState(number=build["number"])
94
+ if "_class" in build:
95
+ job_build_state["_class"] = build["_class"]
96
+ if "actions" in build:
97
+ job_build_state["actions"] = build["actions"]
98
+ if "result" in build:
99
+ job_build_state["result"] = build["result"]
100
+ if commit_sha := self._get_commit_sha_from_build(build):
101
+ job_build_state["commit_sha"] = commit_sha
102
+ return job_build_state
103
+
67
104
  @retry()
68
- def get_jobs_state(self):
105
+ def get_jobs_state(self) -> dict[str, list[JobBuildState]]:
69
106
  url = f"{self.url}/api/json?tree=jobs[name,builds[number,result,actions[lastBuiltRevision[SHA1]]]]"
70
107
  res = requests.get(
71
108
  url, verify=self.ssl_verify, auth=(self.user, self.password), timeout=60
72
109
  )
73
110
 
74
111
  res.raise_for_status()
75
- jobs_state = {}
76
- for r in res.json()["jobs"]:
77
- job_name = r["name"]
78
- builds = r.get("builds", [])
79
- for b in builds:
80
- actions = b.get("actions", [])
81
- for a in actions:
82
- revision = a.get("lastBuiltRevision")
83
- if revision:
84
- b["commit_sha"] = revision["SHA1"]
85
- jobs_state[job_name] = builds
86
-
87
- return jobs_state
88
-
89
- def delete_build(self, job_name, build_id):
112
+ jobs = res.json().get("jobs") or []
113
+ return {
114
+ job["name"]: list(
115
+ map(
116
+ self._build_job_build_state,
117
+ job.get("builds", []),
118
+ )
119
+ )
120
+ for job in jobs
121
+ }
122
+
123
+ def delete_build(self, job_name: str, build_id: str) -> None:
90
124
  url = f"{self.url}/job/{job_name}/{build_id}/doDelete"
91
125
  res = requests.post(
92
126
  url, verify=self.ssl_verify, auth=(self.user, self.password), timeout=60
93
127
  )
94
128
  res.raise_for_status()
95
129
 
96
- def get_all_roles(self):
130
+ def get_all_roles(self) -> dict[str, Any]:
97
131
  url = f"{self.url}/role-strategy/strategy/getAllRoles"
98
132
  res = requests.get(
99
133
  url, verify=self.ssl_verify, auth=(self.user, self.password), timeout=60
@@ -102,7 +136,7 @@ class JenkinsApi:
102
136
  res.raise_for_status()
103
137
  return res.json()
104
138
 
105
- def assign_role_to_user(self, role, user):
139
+ def assign_role_to_user(self, role: str, user: str) -> None:
106
140
  url = f"{self.url}/role-strategy/strategy/assignRole"
107
141
  data = {"type": "globalRoles", "roleName": role, "sid": user}
108
142
  res = requests.post(
@@ -115,7 +149,7 @@ class JenkinsApi:
115
149
 
116
150
  res.raise_for_status()
117
151
 
118
- def unassign_role_from_user(self, role, user):
152
+ def unassign_role_from_user(self, role: str, user: str) -> None:
119
153
  url = f"{self.url}/role-strategy/strategy/unassignRole"
120
154
  data = {"type": "globalRoles", "roleName": role, "sid": user}
121
155
  res = requests.post(
@@ -128,7 +162,7 @@ class JenkinsApi:
128
162
 
129
163
  res.raise_for_status()
130
164
 
131
- def safe_restart(self, force_restart=False):
165
+ def safe_restart(self, force_restart: bool = False) -> None:
132
166
  url = f"{self.url}/safeRestart"
133
167
  if self.should_restart or force_restart:
134
168
  logging.debug(
@@ -142,7 +176,7 @@ class JenkinsApi:
142
176
 
143
177
  res.raise_for_status()
144
178
 
145
- def get_builds(self, job_name):
179
+ def get_builds(self, job_name: str) -> list[dict[str, Any]]:
146
180
  url = (
147
181
  f"{self.url}/job/{job_name}/api/json"
148
182
  + "?tree=allBuilds[timestamp,result,id]"
@@ -157,14 +191,14 @@ class JenkinsApi:
157
191
  except KeyError:
158
192
  return []
159
193
 
160
- def get_build_history(self, job_name, time_limit):
194
+ def get_build_history(self, job_name: str, time_limit: int) -> list[str]:
161
195
  return [
162
196
  b["result"]
163
197
  for b in self.get_builds(job_name)
164
198
  if time_limit < self.timestamp_seconds(b["timestamp"])
165
199
  ]
166
200
 
167
- def is_job_running(self, job_name):
201
+ def is_job_running(self, job_name: str) -> bool:
168
202
  url = f"{self.url}/job/{job_name}/lastBuild/api/json"
169
203
  res = requests.get(
170
204
  url, verify=self.ssl_verify, auth=(self.user, self.password), timeout=60
@@ -178,7 +212,7 @@ class JenkinsApi:
178
212
  res.raise_for_status()
179
213
  return res.json()["building"] is True
180
214
 
181
- def get_crumb_kwargs(self):
215
+ def get_crumb_kwargs(self) -> dict[str, Any]:
182
216
  try:
183
217
  crumb_url = f"{self.url}/crumbIssuer/api/json"
184
218
  res = requests.get(
@@ -197,7 +231,7 @@ class JenkinsApi:
197
231
 
198
232
  return kwargs
199
233
 
200
- def trigger_job(self, job_name):
234
+ def trigger_job(self, job_name: str) -> None:
201
235
  kwargs = self.get_crumb_kwargs()
202
236
 
203
237
  url = f"{self.url}/job/{job_name}/build"
@@ -212,5 +246,5 @@ class JenkinsApi:
212
246
  res.raise_for_status()
213
247
 
214
248
  @staticmethod
215
- def timestamp_seconds(timestamp):
249
+ def timestamp_seconds(timestamp: float) -> int:
216
250
  return int(timestamp / 1000)
@@ -245,6 +245,8 @@ class SaasResourceTemplateTargetPromotion(Protocol):
245
245
  @property
246
246
  def promotion_data(self) -> Sequence[SaasPromotionData] | None: ...
247
247
 
248
+ def dict(self, *, by_alias: bool = False) -> dict[str, Any]: ...
249
+
248
250
 
249
251
  class Channel(Protocol):
250
252
  name: str
@@ -289,6 +291,8 @@ class SaasResourceTemplateTargetUpstream(Protocol):
289
291
  @property
290
292
  def instance(self) -> SaasJenkinsInstance: ...
291
293
 
294
+ def dict(self, *, by_alias: bool = False) -> dict[str, Any]: ...
295
+
292
296
 
293
297
  class SaasQuayInstance(Protocol):
294
298
  url: str
@@ -307,6 +311,8 @@ class SaasResourceTemplateTargetImage(Protocol):
307
311
  @property
308
312
  def org(self) -> SaasQuayOrg: ...
309
313
 
314
+ def dict(self, *, by_alias: bool = False) -> dict[str, Any]: ...
315
+
310
316
 
311
317
  class SaasResourceTemplateTarget(HasParameters, HasSecretParameters, Protocol):
312
318
  path: str | None
@@ -4,7 +4,7 @@ import logging
4
4
  from collections.abc import Iterable, Sequence
5
5
  from dataclasses import dataclass
6
6
  from enum import Enum
7
- from typing import Any
7
+ from typing import Any, NotRequired, TypedDict
8
8
 
9
9
  from github import Github
10
10
  from pydantic import (
@@ -15,6 +15,7 @@ from pydantic import (
15
15
  from reconcile.gql_definitions.fragments.saas_slo_document import (
16
16
  SLODocument,
17
17
  )
18
+ from reconcile.utils.jenkins_api import JobBuildState
18
19
  from reconcile.utils.oc_connection_parameters import Cluster
19
20
  from reconcile.utils.saasherder.interfaces import (
20
21
  HasParameters,
@@ -78,8 +79,53 @@ class SLOKey:
78
79
  cluster_name: str
79
80
 
80
81
 
82
+ class TriggerSpecConfigStateContentNamespaceApp(TypedDict):
83
+ name: str
84
+
85
+
86
+ class TriggerSpecConfigStateContentNamespaceCluster(TypedDict):
87
+ name: str
88
+ serverUrl: str
89
+
90
+
91
+ class TriggerSpecConfigStateContentNamespace(TypedDict):
92
+ name: str
93
+ cluster: TriggerSpecConfigStateContentNamespaceCluster
94
+ app: TriggerSpecConfigStateContentNamespaceApp
95
+
96
+
97
+ class TriggerSpecConfigStateContent(TypedDict):
98
+ """
99
+ dict representation of reconcile.typed_queries.saas_files.SaasResourceTemplateTarget
100
+ with some additional fields.
101
+ """
102
+
103
+ path: str | None
104
+ name: str | None
105
+ namespace: TriggerSpecConfigStateContentNamespace
106
+ ref: str | None
107
+ promotion: dict | None
108
+ parameters: str | None
109
+ secretParameters: list[dict] | None
110
+ slos: list[Any] | None
111
+ upstream: Any | None
112
+ images: list[Any] | None
113
+ disable: bool | None
114
+ delete: bool | None
115
+
116
+ # additional fields
117
+ saas_file_parameters: str | None
118
+ saas_file_managed_resource_types: list[str]
119
+ saas_file_managed_resource_names: NotRequired[list[Any]]
120
+ url: str
121
+ rt_parameters: str | None
122
+ rt_secretparameters: NotRequired[list[dict]]
123
+ saas_file_secretparameters: NotRequired[list[dict]]
124
+
125
+
81
126
  @dataclass(frozen=True)
82
127
  class TriggerSpecConfig(TriggerSpecBase):
128
+ state_content: TriggerSpecConfigStateContent | None
83
129
  resource_template_url: str
84
130
  slos: list[SLODocument] | None = None
85
131
  target_name: str | None = None
@@ -110,6 +156,7 @@ class TriggerSpecConfig(TriggerSpecBase):
110
156
 
111
157
  @dataclass(frozen=True)
112
158
  class TriggerSpecMovingCommit(TriggerSpecBase):
159
+ state_content: str
113
160
  ref: str
114
161
 
115
162
  @property
@@ -123,6 +170,7 @@ class TriggerSpecMovingCommit(TriggerSpecBase):
123
170
 
124
171
  @dataclass(frozen=True)
125
172
  class TriggerSpecUpstreamJob(TriggerSpecBase):
173
+ state_content: JobBuildState
126
174
  instance_name: str
127
175
  job_name: str
128
176
 
@@ -137,6 +185,7 @@ class TriggerSpecUpstreamJob(TriggerSpecBase):
137
185
 
138
186
  @dataclass(frozen=True)
139
187
  class TriggerSpecContainerImage(TriggerSpecBase):
188
+ state_content: str
140
189
  images: Sequence[str]
141
190
 
142
191
  @property
@@ -6,7 +6,6 @@ import logging
6
6
  import os
7
7
  import re
8
8
  from collections import (
9
- ChainMap,
10
9
  defaultdict,
11
10
  )
12
11
  from collections.abc import (
@@ -40,7 +39,7 @@ from reconcile.status import RunningState
40
39
  from reconcile.utils import helm
41
40
  from reconcile.utils.github_api import GithubRepositoryApi
42
41
  from reconcile.utils.gitlab_api import GitLabApi
43
- from reconcile.utils.jenkins_api import JenkinsApi
42
+ from reconcile.utils.jenkins_api import JenkinsApi, JobBuildState
44
43
  from reconcile.utils.jjb_client import JJB
45
44
  from reconcile.utils.oc import (
46
45
  OCLocal,
@@ -74,6 +73,10 @@ from reconcile.utils.saasherder.models import (
74
73
  SLOKey,
75
74
  TargetSpec,
76
75
  TriggerSpecConfig,
76
+ TriggerSpecConfigStateContent,
77
+ TriggerSpecConfigStateContentNamespace,
78
+ TriggerSpecConfigStateContentNamespaceApp,
79
+ TriggerSpecConfigStateContentNamespaceCluster,
77
80
  TriggerSpecContainerImage,
78
81
  TriggerSpecMovingCommit,
79
82
  TriggerSpecUnion,
@@ -1516,8 +1519,10 @@ class SaasHerder: # pylint: disable=too-many-public-methods
1516
1519
  )
1517
1520
  return list(itertools.chain.from_iterable(results)), error
1518
1521
 
1519
- def _get_upstream_jobs_current_state(self) -> tuple[dict[str, Any], bool]:
1520
- current_state: dict[str, Any] = {}
1522
+ def _get_upstream_jobs_current_state(
1523
+ self,
1524
+ ) -> tuple[dict[str, dict[str, list[JobBuildState]]], bool]:
1525
+ current_state: dict[str, dict[str, list[JobBuildState]]] = {}
1521
1526
  error = False
1522
1527
  if not self.jenkins_map:
1523
1528
  raise Exception("jenkins_map is not initialized")
@@ -1534,7 +1539,7 @@ class SaasHerder: # pylint: disable=too-many-public-methods
1534
1539
 
1535
1540
  def _build_trigger_spec_upstream_job_reason(
1536
1541
  self,
1537
- last_build_result: Any,
1542
+ last_build_result: JobBuildState,
1538
1543
  server_url: str,
1539
1544
  job_name: str,
1540
1545
  url: str,
@@ -1551,7 +1556,10 @@ class SaasHerder: # pylint: disable=too-many-public-methods
1551
1556
  return f"{prefix}{server_url}/job/{job_name}/{last_build_result_number}"
1552
1557
 
1553
1558
  def get_upstream_jobs_diff_saas_file(
1554
- self, saas_file: SaasFile, dry_run: bool, current_state: dict[str, Any]
1559
+ self,
1560
+ saas_file: SaasFile,
1561
+ dry_run: bool,
1562
+ current_state: dict[str, dict[str, list[JobBuildState]]],
1555
1563
  ) -> list[TriggerSpecUpstreamJob]:
1556
1564
  trigger_specs = []
1557
1565
  for rt in saas_file.resource_templates:
@@ -1816,7 +1824,7 @@ class SaasHerder: # pylint: disable=too-many-public-methods
1816
1824
  return True
1817
1825
 
1818
1826
  @staticmethod
1819
- def remove_none_values(d: dict[Any, Any] | None) -> dict[Any, Any]:
1827
+ def remove_none_values(d: Mapping[Any, Any] | None) -> dict[Any, Any]:
1820
1828
  if d is None:
1821
1829
  return {}
1822
1830
  new = {}
@@ -1859,7 +1867,7 @@ class SaasHerder: # pylint: disable=too-many-public-methods
1859
1867
 
1860
1868
  def _build_trigger_spec_config_reason(
1861
1869
  self,
1862
- state_content: dict,
1870
+ state_content: TriggerSpecConfigStateContent,
1863
1871
  ) -> str | None:
1864
1872
  if not self.include_trigger_trace:
1865
1873
  return None
@@ -1869,76 +1877,95 @@ class SaasHerder: # pylint: disable=too-many-public-methods
1869
1877
  # to reduce false-positives.
1870
1878
  auto_promotion_suffix = (
1871
1879
  " [auto-promotion]"
1872
- if (state_content.get("promotion") or {}).get("auto", False)
1880
+ if (state_content["promotion"] or {}).get("auto", False)
1873
1881
  else ""
1874
1882
  )
1875
1883
  return f"{self.repo_url}/commit/{RunningState().commit}{auto_promotion_suffix}"
1876
1884
 
1885
+ def _build_trigger_spec_config_state_content(
1886
+ self,
1887
+ target: SaasResourceTemplateTarget,
1888
+ saas_file: SaasFile,
1889
+ resource_template: SaasResourceTemplate,
1890
+ ) -> TriggerSpecConfigStateContent:
1891
+ state_content = TriggerSpecConfigStateContent(
1892
+ name=target.name,
1893
+ ref=target.ref,
1894
+ promotion=(
1895
+ target.promotion.dict(by_alias=True) if target.promotion else None
1896
+ ),
1897
+ secretParameters=(
1898
+ [p.dict(by_alias=True) for p in target.secret_parameters]
1899
+ if target.secret_parameters
1900
+ else None
1901
+ ),
1902
+ slos=(
1903
+ [slo.dict(by_alias=True) for slo in target.slos]
1904
+ if target.slos
1905
+ else None
1906
+ ),
1907
+ upstream=(target.upstream.dict(by_alias=True) if target.upstream else None),
1908
+ images=(
1909
+ [i.dict(by_alias=True) for i in target.images]
1910
+ if target.images
1911
+ else None
1912
+ ),
1913
+ disable=target.disable,
1914
+ delete=target.delete,
1915
+ namespace=self.sanitize_namespace(target.namespace),
1916
+ # add parent parameters to target config
1917
+ # before the GQL classes are introduced, the parameters attribute
1918
+ # was a json string. Keep it that way to be backwards compatible.
1919
+ saas_file_parameters=(
1920
+ json.dumps(saas_file.parameters, separators=(",", ":"))
1921
+ if saas_file.parameters is not None
1922
+ else None
1923
+ ),
1924
+ # before the GQL classes are introduced, the parameters attribute
1925
+ # was a json string. Keep it that way to be backwards compatible.
1926
+ parameters=(
1927
+ json.dumps(target.parameters, separators=(",", ":"))
1928
+ if target.parameters is not None
1929
+ else None
1930
+ ),
1931
+ # add managed resource types to target config
1932
+ saas_file_managed_resource_types=saas_file.managed_resource_types,
1933
+ url=resource_template.url,
1934
+ path=resource_template.path,
1935
+ # before the GQL classes are introduced, the parameters attribute
1936
+ # was a json string. Keep it that way to be backwards compatible.
1937
+ rt_parameters=(
1938
+ json.dumps(resource_template.parameters, separators=(",", ":"))
1939
+ if resource_template.parameters is not None
1940
+ else None
1941
+ ),
1942
+ )
1943
+ if saas_file.managed_resource_names:
1944
+ state_content["saas_file_managed_resource_names"] = [
1945
+ m.dict() for m in saas_file.managed_resource_names
1946
+ ]
1947
+ # include secret parameters from resource template and saas file
1948
+ if resource_template.secret_parameters:
1949
+ state_content["rt_secretparameters"] = [
1950
+ p.dict() for p in resource_template.secret_parameters
1951
+ ]
1952
+ if saas_file.secret_parameters:
1953
+ state_content["saas_file_secretparameters"] = [
1954
+ p.dict() for p in saas_file.secret_parameters
1955
+ ]
1956
+ return state_content
1957
+
1877
1958
  def get_saas_targets_config_trigger_specs(
1878
1959
  self, saas_file: SaasFile
1879
1960
  ) -> dict[str, TriggerSpecConfig]:
1880
1961
  configs = {}
1881
1962
  for rt in saas_file.resource_templates:
1882
1963
  for target in rt.targets:
1883
- # ChainMap will store modifications avoiding a deep copy
1884
- desired_target_config = ChainMap(target.dict(by_alias=True))
1885
- # This will add the namespace key/value to the chainMap, but
1886
- # the target will remain with the original value
1887
- # When the namespace key is looked up, the chainmap will
1888
- # return the modified attribute (set in the first mapping)
1889
- desired_target_config["namespace"] = self.sanitize_namespace(
1890
- target.namespace
1891
- )
1892
- # add parent parameters to target config
1893
- # before the GQL classes are introduced, the parameters attribute
1894
- # was a json string. Keep it that way to be backwards compatible.
1895
- desired_target_config["saas_file_parameters"] = (
1896
- json.dumps(saas_file.parameters, separators=(",", ":"))
1897
- if saas_file.parameters is not None
1898
- else None
1964
+ desired_target_config = self._build_trigger_spec_config_state_content(
1965
+ target=target,
1966
+ saas_file=saas_file,
1967
+ resource_template=rt,
1899
1968
  )
1900
-
1901
- # before the GQL classes are introduced, the parameters attribute
1902
- # was a json string. Keep it that way to be backwards compatible.
1903
- desired_target_config["parameters"] = (
1904
- json.dumps(target.parameters, separators=(",", ":"))
1905
- if target.parameters is not None
1906
- else None
1907
- )
1908
-
1909
- # add managed resource types to target config
1910
- desired_target_config["saas_file_managed_resource_types"] = (
1911
- saas_file.managed_resource_types
1912
- )
1913
- if saas_file.managed_resource_names:
1914
- desired_target_config["saas_file_managed_resource_names"] = [
1915
- m.dict() for m in saas_file.managed_resource_names
1916
- ]
1917
-
1918
- desired_target_config["url"] = rt.url
1919
- desired_target_config["path"] = rt.path
1920
- # before the GQL classes are introduced, the parameters attribute
1921
- # was a json string. Keep it that way to be backwards compatible.
1922
- desired_target_config["rt_parameters"] = (
1923
- json.dumps(rt.parameters, separators=(",", ":"))
1924
- if rt.parameters is not None
1925
- else None
1926
- )
1927
-
1928
- # include secret parameters from resource template and saas file
1929
- if rt.secret_parameters:
1930
- desired_target_config["rt_secretparameters"] = [
1931
- p.dict() for p in rt.secret_parameters
1932
- ]
1933
- if saas_file.secret_parameters:
1934
- desired_target_config["saas_file_secretparameters"] = [
1935
- p.dict() for p in saas_file.secret_parameters
1936
- ]
1937
-
1938
- # Convert to dict, ChainMap is not JSON serializable
1939
- # desired_target_config needs to be serialized to generate
1940
- # its config hash and to be stored in S3
1941
- serializable_target_config = dict(desired_target_config)
1942
1969
  trigger_spec = TriggerSpecConfig(
1943
1970
  saas_file_name=saas_file.name,
1944
1971
  env_name=target.namespace.environment.name,
@@ -1948,12 +1975,12 @@ class SaasHerder: # pylint: disable=too-many-public-methods
1948
1975
  cluster_name=target.namespace.cluster.name,
1949
1976
  namespace_name=target.namespace.name,
1950
1977
  target_name=target.name,
1951
- state_content=serializable_target_config,
1978
+ state_content=desired_target_config,
1952
1979
  resource_template_url=rt.url,
1953
1980
  target_ref=target.ref,
1954
1981
  slos=target.slos or None,
1955
1982
  reason=self._build_trigger_spec_config_reason(
1956
- state_content=serializable_target_config
1983
+ state_content=desired_target_config
1957
1984
  ),
1958
1985
  )
1959
1986
  configs[trigger_spec.state_key] = trigger_spec
@@ -1963,15 +1990,17 @@ class SaasHerder: # pylint: disable=too-many-public-methods
1963
1990
  @staticmethod
1964
1991
  def sanitize_namespace(
1965
1992
  namespace: SaasResourceTemplateTargetNamespace,
1966
- ) -> dict[str, dict[str, str]]:
1993
+ ) -> TriggerSpecConfigStateContentNamespace:
1967
1994
  """Only keep fields that should trigger a new job."""
1968
- return namespace.dict(
1969
- by_alias=True,
1970
- include={
1971
- "name": True,
1972
- "cluster": {"name": True, "server_url": True},
1973
- "app": {"name": True},
1974
- },
1995
+ return TriggerSpecConfigStateContentNamespace(
1996
+ name=namespace.name,
1997
+ cluster=TriggerSpecConfigStateContentNamespaceCluster(
1998
+ name=namespace.cluster.name,
1999
+ serverUrl=namespace.cluster.server_url,
2000
+ ),
2001
+ app=TriggerSpecConfigStateContentNamespaceApp(
2002
+ name=namespace.app.name,
2003
+ ),
1975
2004
  # TODO: add environment.parameters to the include list!?!?
1976
2005
  )
1977
2006