forked from openshift/release
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathvalidate-prow-job-semantics.py
executable file
·304 lines (240 loc) · 11.5 KB
/
validate-prow-job-semantics.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
#!/usr/bin/env python3
import logging
import os
import re
import sys
from argparse import ArgumentParser
import yaml
JOBS_DIR = 'ci-operator/jobs'
logger = logging.getLogger('validate-prow-job-semantics.py')
def parse_args():
parser = ArgumentParser()
parser.add_argument(
'--log-level',
type=str, default='warning',
choices=('debug', 'info', 'warning', 'error', 'critical'))
parser.add_argument("release_repo_dir", help="release directory")
return parser.parse_args()
def main():
PATH_CHECKS = (
validate_filename,
)
CONTENT_CHECKS = (
validate_file_structure,
validate_job_repo,
validate_names,
validate_sharding,
validate_pod_name,
validate_resources,
)
validate = lambda funcs, *args: all(f(*args) for f in funcs)
failed = False
args = parse_args()
logger.setLevel(getattr(logging, args.log_level.upper()))
sh = logging.StreamHandler()
sh.setFormatter(logging.Formatter('%(levelname)s: %(message)s'))
logger.addHandler(sh)
for root, _, files in os.walk(os.path.join(args.release_repo_dir, JOBS_DIR)):
for filename in files:
if filename.endswith(".yml"):
logger.error("Only .yaml extensions are allowed, not .yml as in %s/%s", root, filename)
failed = True
if not filename.endswith('.yaml'):
continue
if os.path.basename(filename).startswith("infra-"):
continue
path = os.path.join(root, filename)
if not validate(PATH_CHECKS, path):
failed = True
continue
with open(path, encoding='utf-8') as f:
data = yaml.load(f, Loader=yaml.SafeLoader)
if not validate(CONTENT_CHECKS, path, data):
failed = True
if failed:
sys.exit(1)
def parse_org_repo(path):
return os.path.basename(os.path.dirname(os.path.dirname(path))), os.path.basename(os.path.dirname(path))
def validate_filename(path):
org_dir, repo_dir = parse_org_repo(path)
base = os.path.basename(path)
if not base.startswith(f'{org_dir}-{repo_dir}-'):
logger.error("%s: expected filename to start with %s-%s", path, org_dir, repo_dir)
return False
job_type = base[base.rfind("-")+1:-len(".yaml")]
if job_type not in ["periodics", "postsubmits", "presubmits"]:
logger.error("%s: expected filename to end with a job type", path)
return False
if job_type == "periodics":
branch = base[len(f'{org_dir}-{repo_dir}-'):-len(f'-{job_type}.yaml')]
if branch == "":
if base != f'{org_dir}-{repo_dir}-{job_type}.yaml':
logger.error("%s: Invalid formatting in filename: expected filename format $org-$repo-periodics.yaml", path)
return False
else:
branch = base[len(f'{org_dir}-{repo_dir}-'):-len(f'-{job_type}.yaml')]
if branch == "":
logger.error("%s: Invalid formatting in filename: expected filename format org-repo-branch-(pre|post)submits.yaml", path)
return False
return True
def validate_file_structure(path, data):
if len(data) != 1:
logger.error("%s: file contains more than one type of job", path)
return False
if next(iter(data.keys())) == 'periodics':
return True
data = next(iter(data.values()))
if len(data) != 1:
logger.error("%s: file contains jobs for more than one repo", path)
return False
return True
def validate_job_repo(path, data):
org, repo = parse_org_repo(path)
if "presubmits" in data:
for org_repo in data["presubmits"]:
if org_repo != f'{org}/{repo}':
logger.error("%s: file defines jobs for %s, but is only allowed to contain jobs for %s/%s", path, org_repo, org, repo)
return False
if "postsubmits" in data:
for org_repo in data["postsubmits"]:
if org_repo != f'{org}/{repo}':
logger.error("%s: file defines jobs for %s, but is only allowed to contain jobs for %s/%s", path, org_repo, org, repo)
return False
return True
def validate_names(path, data):
out = True
for job_type in data:
if job_type == "periodics":
continue
for repo in data[job_type]:
for job in data[job_type][repo]:
if job["agent"] != "kubernetes":
continue
if (not "command" in job["spec"]["containers"][0].keys()) or (job["spec"]["containers"][0]["command"][0] != "ci-operator"):
continue
if ("labels" in job) and ("ci-operator.openshift.io/semantics-ignored" in job["labels"]) and job["labels"]["ci-operator.openshift.io/semantics-ignored"] == "true":
logger.info("%s: ci-operator job %s is ignored because of a label says so", path, job["name"])
continue
if ("labels" in job) and ("ci.openshift.io/generator" in job["labels"]) and job["labels"]["ci.openshift.io/generator"] == "prowgen":
logger.info("%s: ci-operator job %s is ignored because it's generated and assumed to be right", path, job["name"])
continue
targets = []
for arg in job["spec"]["containers"][0].get("args", []) + job["spec"]["containers"][0]["command"]:
if arg.startswith("--target="):
targets.append(arg[len("--target="):].strip("[]"))
if not targets:
logger.warning("%s: ci-operator job %s should call a target", path, job["name"])
continue
if ("labels" in job) and ("ci.openshift.io/release-type" in job["labels"]):
logger.error("[%s: in job %s \"ci.openshift.io/release-type\" annotation has been deprecated. Specify release-type in release/core-services/testgrid-config-generator/_allow-list.yaml to override defaults", path, job["name"])
out = False
continue
branch = "master"
if "branches" in job:
for branch_name in job["branches"]:
if "_" in branch_name:
logger.error("%s: job %s branches with underscores are not allowed: %s", path, job["name"], branch_name)
branch = make_regex_filename_label(job["branches"][0])
prefixes = ["pull"]
if job_type == "postsubmits":
prefixes = ["branch", "priv"]
variant = job.get("labels", {}).get("ci-operator.openshift.io/variant", "")
filtered_targets = [target for target in targets if target not in ["release:latest"]]
valid_names = {}
for target in filtered_targets:
if variant:
target = variant + "-" + target
for prefix in prefixes:
name = f'{prefix}-ci-{repo.replace("/", "-")}-{branch}-{target}'
valid_names[name] = target
if job["name"] not in valid_names:
logger.error("%s: ci-operator job %s should be named one of %s", path, job["name"], list(valid_names.keys()))
out = False
continue
name_target = valid_names[job["name"]]
if job_type == "presubmits":
valid_context = f'ci/prow/{name_target}'
if job["context"] != valid_context:
logger.error("%s: ci-operator job %s should have context %s", path, job["name"], valid_context)
out = False
valid_rerun_command = f'/test {name_target}'
if job["rerun_command"] != valid_rerun_command:
logger.error("%s: ci-operator job %s should have rerun_command %s", path, job["name"], valid_rerun_command)
out = False
valid_trigger = rf'(?m)^/test( | .* ){name_target},?($|\s.*)'
if job["trigger"] != valid_trigger:
logger.error("%s: ci-operator job %s should have trigger %s", path, job["name"], valid_trigger)
out = False
return out
def make_regex_filename_label(name):
name = re.sub(r"[^\w\-\.]+", "", name)
name = name.strip("-._")
return name
def validate_sharding(path, data):
out = True
for job_type in data:
if job_type == "periodics":
continue
for repo in data[job_type]:
for job in data[job_type][repo]:
branch = "master"
if "branches" in job:
branch = make_regex_filename_label(job["branches"][0])
file_branch = os.path.basename(path)[len(f'{repo.replace("/", "-")}-'):-len(f'-{job_type}.yaml')]
if file_branch != branch:
logger.error("%s: job %s runs on branch %s, not %s so it should be in file %s", path, job["name"], branch, file_branch, path.replace(file_branch, branch))
out = False
return out
def validate_pod_name(path, data):
out = True
for job_type in data:
if job_type == "periodics":
continue
for repo in data[job_type]:
for job in data[job_type][repo]:
if job["agent"] != "kubernetes":
continue
if len(job["spec"]["containers"]) == 1 and job["spec"]["containers"][0]["name"] != "":
logger.error("%s: ci-operator job %s should not set a container name", path, job["name"])
out = False
continue
return out
def validate_image_pull(path, data):
out = True
for job_type in data:
if job_type == "periodics":
continue
for repo in data[job_type]:
for job in data[job_type][repo]:
if job["agent"] != "kubernetes":
continue
if job["spec"]["containers"][0]["imagePullPolicy"] != "Always":
logger.error("%s: ci-operator job %s should set the pod's image pull policy to always", path, job["name"])
out = False
continue
return out
def validate_resources(path, data):
out = True
for job_type in data:
if job_type == "periodics":
continue
for repo in data[job_type]:
for job in data[job_type][repo]:
if job["agent"] != "kubernetes":
continue
if not "command" in job["spec"]["containers"][0].keys():
continue
ci_op_job = job["spec"]["containers"][0]["command"][0] == "ci-operator"
resources = job["spec"]["containers"][0].get("resources", {})
bad_ci_op_resources = resources != {"requests": {"cpu": "10m"}}
null_cpu_request = resources.get("requests", {}).get("cpu", "") == ""
if ci_op_job and bad_ci_op_resources:
logger.error("%s: ci-operator job %s should set the pod's CPU requests and limits to %s", path, job["name"], resources)
out = False
continue
if null_cpu_request:
logger.error("%s: ci-operator job %s should set the pod's CPU requests", path, job["name"])
out = False
continue
return out
main()