qontract-reconcile 0.10.2.dev244__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.
- {qontract_reconcile-0.10.2.dev244.dist-info → qontract_reconcile-0.10.2.dev246.dist-info}/METADATA +1 -3
- {qontract_reconcile-0.10.2.dev244.dist-info → qontract_reconcile-0.10.2.dev246.dist-info}/RECORD +9 -10
- reconcile/cli.py +0 -8
- reconcile/utils/jenkins_api.py +68 -48
- reconcile/utils/saasherder/interfaces.py +6 -0
- reconcile/utils/saasherder/models.py +50 -1
- reconcile/utils/saasherder/saasherder.py +106 -77
- reconcile/jenkins_job_cleaner.py +0 -51
- {qontract_reconcile-0.10.2.dev244.dist-info → qontract_reconcile-0.10.2.dev246.dist-info}/WHEEL +0 -0
- {qontract_reconcile-0.10.2.dev244.dist-info → qontract_reconcile-0.10.2.dev246.dist-info}/entry_points.txt +0 -0
{qontract_reconcile-0.10.2.dev244.dist-info → qontract_reconcile-0.10.2.dev246.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.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
|
@@ -181,8 +181,6 @@ OpenShift templates can be found [here](/openshift/qontract-reconcile.yaml). In
|
|
181
181
|
jenkins-job-builder Manage Jenkins jobs configurations using
|
182
182
|
jenkins-jobs.
|
183
183
|
jenkins-job-builds-cleaner Clean up jenkins job history.
|
184
|
-
jenkins-job-cleaner Delete Jenkins jobs in multiple tenant
|
185
|
-
instances.
|
186
184
|
jenkins-roles Manage Jenkins roles association via REST
|
187
185
|
API.
|
188
186
|
jenkins-webhooks Manage web hooks to Jenkins jobs.
|
{qontract_reconcile-0.10.2.dev244.dist-info → qontract_reconcile-0.10.2.dev246.dist-info}/RECORD
RENAMED
@@ -8,7 +8,7 @@ reconcile/aws_iam_password_reset.py,sha256=O0JX2N5kNRKs3u2xzu4NNrI6p0ag5JWy3MTsv
|
|
8
8
|
reconcile/aws_support_cases_sos.py,sha256=PDhilxQ4TBxVnxUPIUdTbKEaNUI0wzPiEsB91oHT2fY,3384
|
9
9
|
reconcile/blackbox_exporter_endpoint_monitoring.py,sha256=O1wFp52EyF538c6txaWBs8eMtUIy19gyHZ6VzJ6QXS8,3512
|
10
10
|
reconcile/checkpoint.py,sha256=gjtS8g6KIyKFYlHMSZjAqDUOlVh83nh4go-9yNrhWZU,5016
|
11
|
-
reconcile/cli.py,sha256=
|
11
|
+
reconcile/cli.py,sha256=mewdtUiv_j2OYNnjIMsc4BTbgDk6WTIotsyD-E4LwT4,112803
|
12
12
|
reconcile/closedbox_endpoint_monitoring_base.py,sha256=al7m8EgnnYx90rY1REryW3byN_ItfJfAzEeLtjbCfi0,4921
|
13
13
|
reconcile/cluster_deployment_mapper.py,sha256=5gumAaRCcFXsabUJ1dnuUy9WrP_FEEM5JnOnE8ch9sE,2326
|
14
14
|
reconcile/dashdotdb_base.py,sha256=83ZWIf5JJk3P_D69y2TmXRcQr6ELJGlv10OM0h7fJVs,4767
|
@@ -38,7 +38,6 @@ reconcile/integrations_manager.py,sha256=KKMui_zXHNbMT0o5jnW2JpDzIKuMPqBvamit3I8
|
|
38
38
|
reconcile/jenkins_base.py,sha256=0Gocu3fU2YTltaxBlbDQOUvP-7CP2OSQV1ZRwtWeVXw,875
|
39
39
|
reconcile/jenkins_job_builder.py,sha256=SAcoJUSs2BgxEzfuRfJ4Poik2OSg_B8LaT5VQISocPs,3474
|
40
40
|
reconcile/jenkins_job_builds_cleaner.py,sha256=l9eLyvdgv1sN2tAlkGx3T8g6Db9kIfWW3LJh5H6dV9A,4080
|
41
|
-
reconcile/jenkins_job_cleaner.py,sha256=IJs-YqtoPN9BpdimYegfU2ObYr4g6qGvbFoU-gnX05Q,1874
|
42
41
|
reconcile/jenkins_roles.py,sha256=zdGavYJJNmbOdu_pr-NrNPl_Tj3j8oin14qvydKxXZw,4916
|
43
42
|
reconcile/jenkins_webhooks.py,sha256=KANd1ExYw6jp6F-d-QEbX1L1CNbRF270yL2ppDCAjio,2324
|
44
43
|
reconcile/jenkins_webhooks_cleaner.py,sha256=tFbAzsFGvJ6UrHRZFdIuLdqG-Ocd5_OknfsbtN-eyFY,1619
|
@@ -617,7 +616,7 @@ reconcile/utils/helm.py,sha256=wC1h0GylhDFeZ6hZEtYy2giAGIIQroaQhkAtURoSlI8,3893
|
|
617
616
|
reconcile/utils/helpers.py,sha256=koyAtYnxsUVx-HIn6GpedcUE-ekz_VtoYDkiZ0iv8ik,1795
|
618
617
|
reconcile/utils/imap_client.py,sha256=h8YDiCSCvroErhpH_-KGYI7Y2WU2Q2oSpuxDFbOkSbY,1989
|
619
618
|
reconcile/utils/instrumented_wrappers.py,sha256=VqT4s0Bdicv224-uSeSaugtHXm-xJ3oSeBiqj0QQRiU,1942
|
620
|
-
reconcile/utils/jenkins_api.py,sha256=
|
619
|
+
reconcile/utils/jenkins_api.py,sha256=jNwdtBtO8DgMW_H8XfqkQs2r4JsLovHe03t5_F3M1xg,7961
|
621
620
|
reconcile/utils/jira_client.py,sha256=jj7E58PGssoCUEhUZpXoVYeyuChjrvjcg1AdNsONXO0,10545
|
622
621
|
reconcile/utils/jjb_client.py,sha256=e5cDeNAeJMGz3sZMJ1KUIMFyLdRet0YnC0Qgj1vTPHc,15239
|
623
622
|
reconcile/utils/jsonpath.py,sha256=wdxOMqR-GMpQf5vRPWRMqAF7bCiXDBkkcFfY2U4j_tk,5536
|
@@ -751,9 +750,9 @@ reconcile/utils/runtime/meta.py,sha256=dWdKS9eHVuowFkTK4lgXJ723vS1y9giOMzePUKnHn
|
|
751
750
|
reconcile/utils/runtime/runner.py,sha256=I30KRrX1UQbHc_Ir1cIZX3OfNSdoHKdnDSPAEB69Ilk,7944
|
752
751
|
reconcile/utils/runtime/sharding.py,sha256=r0ieUtNed7NvknSw6qQrCkKpVXE1shuHGnfFcnpA_k4,16142
|
753
752
|
reconcile/utils/saasherder/__init__.py,sha256=3U8plqMAPRE1kjwZ5YnIsYsggTf4_gS7flRUEuXVBAs,343
|
754
|
-
reconcile/utils/saasherder/interfaces.py,sha256=
|
755
|
-
reconcile/utils/saasherder/models.py,sha256=
|
756
|
-
reconcile/utils/saasherder/saasherder.py,sha256=
|
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
|
757
756
|
reconcile/utils/terraform/__init__.py,sha256=zNbiyTWo35AT1sFTElL2j_AA0jJ_yWE_bfFn-nD2xik,250
|
758
757
|
reconcile/utils/terraform/config.py,sha256=5UVrd563TMcvi4ooa5JvWVDW1I3bIWg484u79evfV_8,164
|
759
758
|
reconcile/utils/terraform/config_client.py,sha256=gRL1rQ0AqvShei_rcGqC3HDYGskOFKE1nPrJyJE9yno,4676
|
@@ -799,7 +798,7 @@ tools/saas_promotion_state/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJ
|
|
799
798
|
tools/saas_promotion_state/saas_promotion_state.py,sha256=UfwwRLS5Ya4_Nh1w5n1dvoYtchQvYE9yj1VANt2IKqI,3925
|
800
799
|
tools/sre_checkpoints/__init__.py,sha256=CDaDaywJnmRCLyl_NCcvxi-Zc0hTi_3OdwKiFOyS39I,145
|
801
800
|
tools/sre_checkpoints/util.py,sha256=zEDbGr18ZeHNQwW8pUsr2JRjuXIPz--WAGJxZo9sv_Y,894
|
802
|
-
qontract_reconcile-0.10.2.
|
803
|
-
qontract_reconcile-0.10.2.
|
804
|
-
qontract_reconcile-0.10.2.
|
805
|
-
qontract_reconcile-0.10.2.
|
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,,
|
reconcile/cli.py
CHANGED
@@ -1103,14 +1103,6 @@ def jenkins_job_builds_cleaner(ctx: click.Context) -> None:
|
|
1103
1103
|
run_integration(reconcile.jenkins_job_builds_cleaner, ctx)
|
1104
1104
|
|
1105
1105
|
|
1106
|
-
@integration.command(short_help="Delete Jenkins jobs in multiple tenant instances.")
|
1107
|
-
@click.pass_context
|
1108
|
-
def jenkins_job_cleaner(ctx: click.Context) -> None:
|
1109
|
-
import reconcile.jenkins_job_cleaner
|
1110
|
-
|
1111
|
-
run_integration(reconcile.jenkins_job_cleaner, ctx)
|
1112
|
-
|
1113
|
-
|
1114
1106
|
@integration.command(short_help="Manage web hooks to Jenkins jobs.")
|
1115
1107
|
@click.pass_context
|
1116
1108
|
def jenkins_webhooks(ctx: click.Context) -> None:
|
reconcile/utils/jenkins_api.py
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
import logging
|
2
|
-
from
|
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
|
-
@
|
24
|
+
@classmethod
|
16
25
|
def init_jenkins_from_secret(
|
17
|
-
|
18
|
-
|
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
|
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__(
|
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,50 +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
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
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
|
97
|
-
kwargs = self.get_crumb_kwargs()
|
98
|
-
|
99
|
-
url = f"{self.url}/job/{job_name}/doDelete"
|
100
|
-
res = requests.post(
|
101
|
-
url,
|
102
|
-
verify=self.ssl_verify,
|
103
|
-
auth=(self.user, self.password),
|
104
|
-
timeout=60,
|
105
|
-
**kwargs,
|
106
|
-
)
|
107
|
-
|
108
|
-
res.raise_for_status()
|
109
|
-
|
110
|
-
def get_all_roles(self):
|
130
|
+
def get_all_roles(self) -> dict[str, Any]:
|
111
131
|
url = f"{self.url}/role-strategy/strategy/getAllRoles"
|
112
132
|
res = requests.get(
|
113
133
|
url, verify=self.ssl_verify, auth=(self.user, self.password), timeout=60
|
@@ -116,7 +136,7 @@ class JenkinsApi:
|
|
116
136
|
res.raise_for_status()
|
117
137
|
return res.json()
|
118
138
|
|
119
|
-
def assign_role_to_user(self, role, user):
|
139
|
+
def assign_role_to_user(self, role: str, user: str) -> None:
|
120
140
|
url = f"{self.url}/role-strategy/strategy/assignRole"
|
121
141
|
data = {"type": "globalRoles", "roleName": role, "sid": user}
|
122
142
|
res = requests.post(
|
@@ -129,7 +149,7 @@ class JenkinsApi:
|
|
129
149
|
|
130
150
|
res.raise_for_status()
|
131
151
|
|
132
|
-
def unassign_role_from_user(self, role, user):
|
152
|
+
def unassign_role_from_user(self, role: str, user: str) -> None:
|
133
153
|
url = f"{self.url}/role-strategy/strategy/unassignRole"
|
134
154
|
data = {"type": "globalRoles", "roleName": role, "sid": user}
|
135
155
|
res = requests.post(
|
@@ -142,7 +162,7 @@ class JenkinsApi:
|
|
142
162
|
|
143
163
|
res.raise_for_status()
|
144
164
|
|
145
|
-
def safe_restart(self, force_restart=False):
|
165
|
+
def safe_restart(self, force_restart: bool = False) -> None:
|
146
166
|
url = f"{self.url}/safeRestart"
|
147
167
|
if self.should_restart or force_restart:
|
148
168
|
logging.debug(
|
@@ -156,7 +176,7 @@ class JenkinsApi:
|
|
156
176
|
|
157
177
|
res.raise_for_status()
|
158
178
|
|
159
|
-
def get_builds(self, job_name):
|
179
|
+
def get_builds(self, job_name: str) -> list[dict[str, Any]]:
|
160
180
|
url = (
|
161
181
|
f"{self.url}/job/{job_name}/api/json"
|
162
182
|
+ "?tree=allBuilds[timestamp,result,id]"
|
@@ -171,14 +191,14 @@ class JenkinsApi:
|
|
171
191
|
except KeyError:
|
172
192
|
return []
|
173
193
|
|
174
|
-
def get_build_history(self, job_name, time_limit):
|
194
|
+
def get_build_history(self, job_name: str, time_limit: int) -> list[str]:
|
175
195
|
return [
|
176
196
|
b["result"]
|
177
197
|
for b in self.get_builds(job_name)
|
178
198
|
if time_limit < self.timestamp_seconds(b["timestamp"])
|
179
199
|
]
|
180
200
|
|
181
|
-
def is_job_running(self, job_name):
|
201
|
+
def is_job_running(self, job_name: str) -> bool:
|
182
202
|
url = f"{self.url}/job/{job_name}/lastBuild/api/json"
|
183
203
|
res = requests.get(
|
184
204
|
url, verify=self.ssl_verify, auth=(self.user, self.password), timeout=60
|
@@ -192,7 +212,7 @@ class JenkinsApi:
|
|
192
212
|
res.raise_for_status()
|
193
213
|
return res.json()["building"] is True
|
194
214
|
|
195
|
-
def get_crumb_kwargs(self):
|
215
|
+
def get_crumb_kwargs(self) -> dict[str, Any]:
|
196
216
|
try:
|
197
217
|
crumb_url = f"{self.url}/crumbIssuer/api/json"
|
198
218
|
res = requests.get(
|
@@ -211,7 +231,7 @@ class JenkinsApi:
|
|
211
231
|
|
212
232
|
return kwargs
|
213
233
|
|
214
|
-
def trigger_job(self, job_name):
|
234
|
+
def trigger_job(self, job_name: str) -> None:
|
215
235
|
kwargs = self.get_crumb_kwargs()
|
216
236
|
|
217
237
|
url = f"{self.url}/job/{job_name}/build"
|
@@ -226,5 +246,5 @@ class JenkinsApi:
|
|
226
246
|
res.raise_for_status()
|
227
247
|
|
228
248
|
@staticmethod
|
229
|
-
def timestamp_seconds(timestamp):
|
249
|
+
def timestamp_seconds(timestamp: float) -> int:
|
230
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(
|
1520
|
-
|
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:
|
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,
|
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:
|
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:
|
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
|
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
|
-
|
1884
|
-
|
1885
|
-
|
1886
|
-
|
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=
|
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=
|
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
|
-
) ->
|
1993
|
+
) -> TriggerSpecConfigStateContentNamespace:
|
1967
1994
|
"""Only keep fields that should trigger a new job."""
|
1968
|
-
return
|
1969
|
-
|
1970
|
-
|
1971
|
-
|
1972
|
-
|
1973
|
-
|
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
|
|
reconcile/jenkins_job_cleaner.py
DELETED
@@ -1,51 +0,0 @@
|
|
1
|
-
import logging
|
2
|
-
from collections.abc import Iterable
|
3
|
-
|
4
|
-
from reconcile import queries
|
5
|
-
from reconcile.jenkins_job_builder import init_jjb
|
6
|
-
from reconcile.utils.jenkins_api import JenkinsApi
|
7
|
-
from reconcile.utils.secret_reader import SecretReader
|
8
|
-
|
9
|
-
QONTRACT_INTEGRATION = "jenkins-job-cleaner"
|
10
|
-
|
11
|
-
|
12
|
-
def get_managed_job_names(
|
13
|
-
job_names: Iterable[str], managed_projects: Iterable[str]
|
14
|
-
) -> list[str]:
|
15
|
-
managed_jobs = set()
|
16
|
-
for job_name in job_names:
|
17
|
-
for managed_project in managed_projects:
|
18
|
-
if job_name.startswith(managed_project):
|
19
|
-
managed_jobs.add(job_name)
|
20
|
-
|
21
|
-
return list(managed_jobs)
|
22
|
-
|
23
|
-
|
24
|
-
def get_desired_job_names(instance_name: str, secret_reader: SecretReader) -> list[str]:
|
25
|
-
jjb = init_jjb(secret_reader)
|
26
|
-
desired_jobs = jjb.get_all_jobs(instance_name=instance_name)[instance_name]
|
27
|
-
return [j["name"] for j in desired_jobs]
|
28
|
-
|
29
|
-
|
30
|
-
def run(dry_run: bool) -> None:
|
31
|
-
jenkins_instances = queries.get_jenkins_instances()
|
32
|
-
secret_reader = SecretReader(queries.get_secret_reader_settings())
|
33
|
-
|
34
|
-
for instance in jenkins_instances:
|
35
|
-
if instance.get("deleteMethod") != "manual":
|
36
|
-
continue
|
37
|
-
managed_projects = instance.get("managedProjects")
|
38
|
-
if not managed_projects:
|
39
|
-
continue
|
40
|
-
|
41
|
-
instance_name = instance["name"]
|
42
|
-
jenkins = JenkinsApi.init_jenkins_from_secret(secret_reader, instance["token"])
|
43
|
-
all_job_names = jenkins.get_job_names()
|
44
|
-
managed_job_names = get_managed_job_names(all_job_names, managed_projects)
|
45
|
-
desired_job_names = get_desired_job_names(instance_name, secret_reader)
|
46
|
-
delete_job_names = [j for j in managed_job_names if j not in desired_job_names]
|
47
|
-
|
48
|
-
for job_name in delete_job_names:
|
49
|
-
logging.info(["delete_job", instance_name, job_name])
|
50
|
-
if not dry_run:
|
51
|
-
jenkins.delete_job(job_name)
|
{qontract_reconcile-0.10.2.dev244.dist-info → qontract_reconcile-0.10.2.dev246.dist-info}/WHEEL
RENAMED
File without changes
|
File without changes
|