File indexing completed on 2026-09-28 09:36:49
0001 """
0002 Offline check of the inline workflow-description submission endpoint.
0003
0004 Stubs the server-side dependencies (task buffer, workflow interface, request validation) so that
0005 pandaserver.api.v1.workflow_api.submit_workflow_description can be exercised without a database or
0006 a running server.
0007
0008 Run from the repository root: python3 pandaserver/workflow/examples/submit_endpoint_test.py
0009 """
0010
0011 import copy
0012 import importlib.abc
0013 import importlib.machinery
0014 import json
0015 import os
0016 import sys
0017 import types
0018 from typing import Any, cast
0019
0020 REPO_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", ".."))
0021 sys.path.insert(0, REPO_ROOT)
0022
0023
0024 AUTO_STUB_ROOTS = ("idds", "pandaclient", "ruamel", "requests", "snakemake")
0025
0026
0027 class AutoStubFinder(importlib.abc.MetaPathFinder, importlib.abc.Loader):
0028 def find_spec(self, name, path=None, target=None):
0029 if name.split(".")[0] in AUTO_STUB_ROOTS:
0030 return importlib.machinery.ModuleSpec(name, self, is_package=True)
0031 return None
0032
0033 def create_module(self, spec):
0034 module = types.ModuleType(spec.name)
0035 module.__path__ = []
0036 return module
0037
0038 def exec_module(self, module):
0039 class Anything:
0040 def __init__(self, *args, **kwargs):
0041 pass
0042
0043 def __call__(self, *args, **kwargs):
0044 return self
0045
0046 module.__getattr__ = lambda name: Anything
0047
0048
0049 sys.meta_path.insert(0, AutoStubFinder())
0050
0051
0052 def stub(name, **attrs):
0053 module = types.ModuleType(name)
0054 for key, value in attrs.items():
0055 setattr(module, key, value)
0056 sys.modules[name] = module
0057 return module
0058
0059
0060 class QuietLogWrapper:
0061 def __init__(self, *args, **kwargs):
0062 self.messages = []
0063
0064 def info(self, message):
0065 pass
0066
0067 def debug(self, message):
0068 pass
0069
0070 def warning(self, message):
0071 self.messages.append(message)
0072
0073 def error(self, message):
0074 self.messages.append(message)
0075
0076
0077 import datetime
0078
0079 stub("pandacommon")
0080 pandautils = stub("pandacommon.pandautils")
0081 pandautils.__path__ = []
0082 stub("pandacommon.pandautils.base", SpecBase=object)
0083 stub(
0084 "pandacommon.pandautils.PandaUtils", naive_utcnow=lambda: datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None), get_sql_IN_bind_variables=None
0085 )
0086 pandalogger = stub("pandacommon.pandalogger")
0087 pandalogger.__path__ = []
0088 stub("pandacommon.pandalogger.LogWrapper", LogWrapper=QuietLogWrapper)
0089 stub("pandacommon.pandalogger.PandaLogger", PandaLogger=lambda: types.SimpleNamespace(getLogger=lambda name: None))
0090 stub("pandaserver.config", panda_config=types.SimpleNamespace(schemaJEDI="ATLAS_PANDA", schemaDEFT="ATLAS_DEFT"))
0091
0092
0093 FAKE_ROLE = {"production": True}
0094 FAKE_FQANS = ["/atlas/Role=production", "/atlas/usatlas"]
0095
0096
0097 def fake_request_validation(logger, secure=False, production=False, request_method=None):
0098 def decorator(func):
0099 return func
0100
0101 return decorator
0102
0103
0104 stub(
0105 "pandaserver.api.v1.common",
0106 MESSAGE_DATABASE="database error",
0107 TIME_OUT="timeout",
0108 TimedMethod=object,
0109 generate_response=lambda success, message="", data=None: {"success": success, "message": message, "data": data},
0110 get_dn=lambda req: "/DC=ch/DC=cern/CN=test user",
0111 get_fqan=lambda req: list(FAKE_FQANS),
0112 has_production_role=lambda req: FAKE_ROLE["production"],
0113 request_validation=fake_request_validation,
0114 )
0115 stub("pandaserver.srvcore")
0116 stub("pandaserver.srvcore.panda_request", PandaRequest=object)
0117 stub("pandaserver.taskbuffer")
0118 stub("pandaserver.taskbuffer.TaskBuffer", TaskBuffer=object)
0119 stub("pandaserver.workflow.workflow_core", WorkflowInterface=lambda *a, **k: None)
0120
0121 from pandaserver.api.v1 import workflow_api
0122
0123
0124
0125 class FakeTaskBuffer:
0126 def __init__(self, existing=None, fail=False):
0127 self.existing = existing or {}
0128 self.fail = fail
0129 self.queries = []
0130
0131 def get_existing_task_names(self, vo, prod_source_label, task_names):
0132 self.queries.append((vo, prod_source_label, sorted(task_names)))
0133 if self.fail:
0134 return None
0135 return {name: info for name, info in self.existing.items() if name in task_names}
0136
0137
0138 class FakeWorkflowInterface:
0139 def __init__(self, workflow_id=4242):
0140 self.workflow_id = workflow_id
0141 self.calls = []
0142 self.step_relations = {"workflow_id": 133, "steps": [{"step_id": 516, "name": "evgen", "parent_step_ids": []}]}
0143 self.task_relations = {"workflow_id": 133, "tasks": [{"key": "133:516", "task_id": 52382519, "name": "evgen", "parents": []}]}
0144
0145 def register_workflow(
0146 self, prodsourcelabel, user_dn, workflow_name=None, workflow_definition=None, raw_request_params=None, prod_role=False, fqans=None, *args, **kwargs
0147 ):
0148 self.calls.append(
0149 {
0150 "prodsourcelabel": prodsourcelabel,
0151 "user_dn": user_dn,
0152 "workflow_name": workflow_name,
0153 "raw_request_params": raw_request_params,
0154 "prod_role": prod_role,
0155 "fqans": fqans,
0156 }
0157 )
0158 return self.workflow_id
0159
0160
0161
0162 def get_step_relations(self, workflow_id):
0163 self.calls.append({"get_step_relations": workflow_id})
0164 return self.step_relations
0165
0166 def get_task_relations(self, workflow_id):
0167 self.calls.append({"get_task_relations": workflow_id})
0168 return self.task_relations
0169
0170 def get_task_relations_of_task(self, task_id):
0171 self.calls.append({"get_task_relations_of_task": task_id})
0172 return self.task_relations
0173
0174
0175 def install(existing=None, fail=False, workflow_id=4242):
0176 tbif = FakeTaskBuffer(existing, fail)
0177 wfif = FakeWorkflowInterface(workflow_id)
0178
0179
0180 workflow_api.global_task_buffer = cast(Any, tbif)
0181 workflow_api.global_wfif = cast(Any, wfif)
0182 return tbif, wfif
0183
0184
0185 def check(label, condition, detail=""):
0186 print(f" {'PASS' if condition else 'FAIL'} {label}{' ' + str(detail) if detail and not condition else ''}")
0187 return condition
0188
0189
0190 def main():
0191 failures = 0
0192 wfd = json.load(open(os.path.join(os.path.dirname(__file__), "production_chain_wfd.json")))
0193 submit = workflow_api.submit_workflow_description
0194
0195 print("\n=== a valid description registers ===")
0196 tbif, wfif = install()
0197 res = submit(None, copy.deepcopy(wfd))
0198 failures += not check("success", res["success"] is True, res["message"])
0199 failures += not check("workflow_id returned", res["data"] == {"workflow_id": 4242}, res["data"])
0200 failures += not check("no warning message", res["message"] == "", res["message"])
0201 failures += not check("registered exactly once", len(wfif.calls) == 1)
0202 call = wfif.calls[0]
0203 failures += not check("workflow_name taken from the description", call["workflow_name"] == wfd["name"], call["workflow_name"])
0204 failures += not check("prodsourcelabel managed for a production role", call["prodsourcelabel"] == "managed", call["prodsourcelabel"])
0205 failures += not check(
0206 "description carried under the inline key",
0207 list(call["raw_request_params"]) == [workflow_api.INLINE_DESCRIPTION_KEY],
0208 list(call["raw_request_params"]),
0209 )
0210 failures += not check("no sandbox keys invented", "sandbox" not in call["raw_request_params"] and "sourceURL" not in call["raw_request_params"])
0211 failures += not check("production role captured from VOMS", call["prod_role"] is True, call["prod_role"])
0212 failures += not check("fqans captured from VOMS", call["fqans"] == FAKE_FQANS, call["fqans"])
0213
0214 print("\n=== credentials come from VOMS, never from the payload ===")
0215 spoofed = copy.deepcopy(wfd)
0216 spoofed["prod_role"] = True
0217 spoofed["fqans"] = ["/atlas/Role=production"]
0218 FAKE_ROLE["production"] = False
0219 FAKE_FQANS_SAVED = list(FAKE_FQANS)
0220 del FAKE_FQANS[:]
0221 FAKE_FQANS.extend(["/atlas"])
0222 tbif_s, wfif_s = install()
0223 submit(None, spoofed)
0224 failures += not check("payload cannot claim a production role", wfif_s.calls[0]["prod_role"] is False, wfif_s.calls[0]["prod_role"])
0225 failures += not check("payload cannot inject fqans", wfif_s.calls[0]["fqans"] == ["/atlas"], wfif_s.calls[0]["fqans"])
0226 failures += not check("payload cannot force prodsourcelabel", wfif_s.calls[0]["prodsourcelabel"] == "user")
0227 FAKE_ROLE["production"] = True
0228 del FAKE_FQANS[:]
0229 FAKE_FQANS.extend(FAKE_FQANS_SAVED)
0230
0231 print("\n=== the description is accepted as a JSON string too ===")
0232 tbif, wfif = install()
0233 res = submit(None, json.dumps(wfd))
0234 failures += not check("success", res["success"] is True, res["message"])
0235 res_bad = submit(None, "{not json")
0236 failures += not check("malformed JSON rejected", res_bad["success"] is False)
0237 failures += not check("malformed JSON explained", "Failed to parse" in res_bad["message"], res_bad["message"])
0238
0239 print("\n=== a non-production submitter gets prodsourcelabel user ===")
0240 FAKE_ROLE["production"] = False
0241 tbif, wfif = install()
0242 submit(None, copy.deepcopy(wfd))
0243 failures += not check("prodsourcelabel user", wfif.calls[0]["prodsourcelabel"] == "user", wfif.calls[0]["prodsourcelabel"])
0244 FAKE_ROLE["production"] = True
0245
0246 print("\n=== an invalid description is rejected before registration ===")
0247 broken = copy.deepcopy(wfd)
0248 for job_param in broken["steps"]["simul"]["task_params"]["jobParameters"]:
0249 if job_param.get("param_type") == "input":
0250 job_param["dataset"] = "{merge_evnt/NOPE}"
0251 tbif, wfif = install()
0252 res = submit(None, broken)
0253 failures += not check("rejected", res["success"] is False)
0254 failures += not check("reason reported", "does not match any step output" in res["message"], res["message"])
0255 failures += not check("nothing registered", wfif.calls == [])
0256 failures += not check("no duplication query wasted", tbif.queries == [])
0257
0258 print("\n=== a duplicated taskName warns but still registers ===")
0259 existing = {
0260 wfd["steps"]["evgen"]["task_params"]["taskName"]: {"jediTaskID": 48810693, "status": "running"},
0261 wfd["steps"]["merge_aod"]["task_params"]["taskName"]: {"jediTaskID": 48810733, "status": None},
0262 }
0263 tbif, wfif = install(existing=existing)
0264 res = submit(None, copy.deepcopy(wfd))
0265 failures += not check("still succeeds", res["success"] is True)
0266 failures += not check("still registers", len(wfif.calls) == 1)
0267 failures += not check("warning names both collisions", res["message"].count("jediTaskID=") == 2, res["message"])
0268 failures += not check("JEDI status reported", "status=running" in res["message"], res["message"])
0269 failures += not check("DEFT-only collision reported", "queued in DEFT" in res["message"], res["message"])
0270 failures += not check("queried once per vo/prodSourceLabel group", len(tbif.queries) == 1, tbif.queries)
0271 failures += not check("queried all 9 taskNames", len(tbif.queries[0][2]) == 9, tbif.queries[0][2])
0272 failures += not check("queried group scoped by vo and label", tbif.queries[0][:2] == ("atlas", "managed"), tbif.queries[0][:2])
0273
0274 print("\n=== a failed duplication lookup does not block submission ===")
0275 tbif, wfif = install(fail=True)
0276 res = submit(None, copy.deepcopy(wfd))
0277 failures += not check("still succeeds", res["success"] is True, res["message"])
0278 failures += not check("still registers", len(wfif.calls) == 1)
0279
0280 print("\n=== registration failure is reported ===")
0281 tbif, wfif = install(workflow_id=None)
0282 res = submit(None, copy.deepcopy(wfd))
0283 failures += not check("failure reported", res["success"] is False)
0284 failures += not check("reason given", "Failed to submit" in res["message"], res["message"])
0285
0286 print("\n=== the deprecated alias delegates to submit_workflow ===")
0287 raw_params = {"sourceURL": "https://example.org", "sandbox": "sandbox.tgz", "language": "yaml", "workflowSpecFile": "wf.yaml", "outDS": "user.me.out"}
0288 tbif, wfif = install()
0289
0290
0291 res_alias = workflow_api.submit_workflow_raw_request(cast(Any, None), copy.deepcopy(raw_params))
0292 tbif2, wfif2 = install()
0293 res_direct = workflow_api.submit_workflow(None, copy.deepcopy(raw_params))
0294 failures += not check("alias succeeds", res_alias["success"] is True, res_alias["message"])
0295 failures += not check("alias result matches submit_workflow", res_alias == res_direct, (res_alias, res_direct))
0296 failures += not check("alias registers the raw request unchanged", wfif.calls[0]["raw_request_params"]["sandbox"] == "sandbox.tgz")
0297 failures += not check("raw request path does not validate a description", "workflow_description" not in wfif.calls[0]["raw_request_params"])
0298
0299 print("\n=== routing ===")
0300 print("\n=== get_step_relations ===")
0301 _, wfif = install()
0302 res = workflow_api.get_step_relations(cast(Any, None), 133)
0303 failures += not check("succeeds", res["success"] is True, res)
0304 failures += not check("the workflow id is passed through", wfif.calls[-1] == {"get_step_relations": 133}, wfif.calls[-1])
0305 failures += not check("the relations are the data", res["data"]["steps"][0]["name"] == "evgen", res["data"])
0306 _, wfif = install()
0307 wfif.step_relations = None
0308 res = workflow_api.get_step_relations(cast(Any, None), 999)
0309 failures += not check("nothing found is a failure with a reason", res["success"] is False and "No step" in res["message"], res)
0310
0311 print("\n=== get_task_relations ===")
0312 _, wfif = install()
0313 res = workflow_api.get_task_relations(cast(Any, None), workflow_id=133)
0314 failures += not check("by workflow id", res["success"] is True and wfif.calls[-1] == {"get_task_relations": 133}, wfif.calls[-1])
0315 _, wfif = install()
0316 res = workflow_api.get_task_relations(cast(Any, None), task_id=52401216)
0317 failures += not check("by task id, through the other method", wfif.calls[-1] == {"get_task_relations_of_task": 52401216}, wfif.calls[-1])
0318 failures += not check("...and succeeds", res["success"] is True, res)
0319 _, wfif = install()
0320 res = workflow_api.get_task_relations(cast(Any, None))
0321 failures += not check("neither given is refused", res["success"] is False and "required" in res["message"], res)
0322 failures += not check("...without reaching the interface", not any("get_task" in str(call) for call in wfif.calls), wfif.calls)
0323 res = workflow_api.get_task_relations(cast(Any, None), workflow_id=133, task_id=52401216)
0324 failures += not check("both given is refused", res["success"] is False and "not both" in res["message"], res)
0325 _, wfif = install()
0326 wfif.task_relations = None
0327 res = workflow_api.get_task_relations(cast(Any, None), task_id=52401216)
0328 failures += not check("a task no workflow runs is a failure with a reason", res["success"] is False and "runs this task" in res["message"], res)
0329
0330
0331
0332 import inspect
0333
0334 exported = [
0335 name
0336 for name, obj in inspect.getmembers(workflow_api, inspect.isfunction)
0337 if obj.__module__ == workflow_api.__name__ and name != "init_task_buffer" and not name.startswith("_")
0338 ]
0339 print(f" exported: {sorted(exported)}")
0340 failures_local = 0
0341 failures_local += not check("the new endpoint is routed", "submit_workflow_description" in exported)
0342 failures_local += not check(
0343 "all four endpoints routed",
0344 {"submit_workflow", "submit_workflow_description", "submit_workflow_definition", "submit_workflow_raw_request"} <= set(exported),
0345 sorted(exported),
0346 )
0347 failures_local += not check("the deprecated alias is still routed", "submit_workflow_raw_request" in exported)
0348 failures_local += not check(
0349 "the relation endpoints are routed",
0350 {"get_step_relations", "get_task_relations"} <= set(exported),
0351 sorted(exported),
0352 )
0353 failures_local += not check("private helpers not routed", not any(n.startswith("_") for n in exported))
0354 failures_local += not check(
0355 "imported helpers not routed",
0356 "validate_workflow_description" not in exported and "substitute_placeholder" not in exported,
0357 )
0358
0359 failures += failures_local
0360 print(f"\n{'ALL CHECKS PASSED' if not failures else f'{failures} CHECK(S) FAILED'}")
0361 return 1 if failures else 0
0362
0363
0364 if __name__ == "__main__":
0365 sys.exit(main())