test_validation.py 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670
  1. #
  2. # Copyright 2018-2022 Elyra Authors
  3. #
  4. # Licensed under the Apache License, Version 2.0 (the "License");
  5. # you may not use this file except in compliance with the License.
  6. # You may obtain a copy of the License at
  7. #
  8. # http://www.apache.org/licenses/LICENSE-2.0
  9. #
  10. # Unless required by applicable law or agreed to in writing, software
  11. # distributed under the License is distributed on an "AS IS" BASIS,
  12. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. # See the License for the specific language governing permissions and
  14. # limitations under the License.
  15. #
  16. import os
  17. from conftest import AIRFLOW_COMPONENT_CACHE_INSTANCE
  18. from conftest import KFP_COMPONENT_CACHE_INSTANCE
  19. import pytest
  20. from elyra.pipeline.pipeline import KubernetesSecret
  21. from elyra.pipeline.pipeline import PIPELINE_CURRENT_VERSION
  22. from elyra.pipeline.pipeline import VolumeMount
  23. from elyra.pipeline.pipeline_constants import KUBERNETES_SECRETS
  24. from elyra.pipeline.pipeline_constants import MOUNTED_VOLUMES
  25. from elyra.pipeline.pipeline_definition import PipelineDefinition
  26. from elyra.pipeline.validation import PipelineValidationManager
  27. from elyra.pipeline.validation import ValidationResponse
  28. from elyra.tests.pipeline.util import _read_pipeline_resource
  29. @pytest.fixture
  30. def load_pipeline():
  31. def _function(pipeline_filepath):
  32. response = ValidationResponse()
  33. pipeline = _read_pipeline_resource(f"resources/validation_pipelines/{pipeline_filepath}")
  34. return pipeline, response
  35. yield _function
  36. @pytest.fixture
  37. def validation_manager(setup_factory_data, component_cache):
  38. root = os.path.realpath(os.path.join(os.getcwd(), os.path.dirname(__file__), "resources/validation_pipelines"))
  39. yield PipelineValidationManager.instance(root_dir=root)
  40. PipelineValidationManager.clear_instance()
  41. async def test_invalid_lower_pipeline_version(validation_manager, load_pipeline):
  42. pipeline, response = load_pipeline("generic_basic_pipeline_only_notebook.pipeline")
  43. pipeline_version = PIPELINE_CURRENT_VERSION - 1
  44. pipeline["pipelines"][0]["app_data"]["version"] = pipeline_version
  45. pipeline_definition = PipelineDefinition(pipeline_definition=pipeline)
  46. validation_manager._validate_pipeline_structure(pipeline_definition=pipeline_definition, response=response)
  47. issues = response.to_json().get("issues")
  48. assert len(issues) == 1
  49. assert issues[0]["severity"] == 1
  50. assert issues[0]["type"] == "invalidPipeline"
  51. assert (
  52. issues[0]["message"] == f"Pipeline version {pipeline_version} is out of date "
  53. "and needs to be migrated using the Elyra pipeline editor."
  54. )
  55. def test_invalid_upper_pipeline_version(validation_manager, load_pipeline):
  56. pipeline, response = load_pipeline("generic_basic_pipeline_only_notebook.pipeline")
  57. pipeline_version = PIPELINE_CURRENT_VERSION + 1
  58. pipeline["pipelines"][0]["app_data"]["version"] = pipeline_version
  59. pipeline_definition = PipelineDefinition(pipeline_definition=pipeline)
  60. validation_manager._validate_pipeline_structure(pipeline_definition=pipeline_definition, response=response)
  61. issues = response.to_json().get("issues")
  62. assert len(issues) == 1
  63. assert issues[0]["severity"] == 1
  64. assert issues[0]["type"] == "invalidPipeline"
  65. assert (
  66. issues[0]["message"] == "Pipeline was last edited in a newer version of Elyra. "
  67. "Update Elyra to use this pipeline."
  68. )
  69. def test_invalid_pipeline_version_that_needs_migration(validation_manager, load_pipeline):
  70. pipeline, response = load_pipeline("generic_basic_pipeline_only_notebook.pipeline")
  71. pipeline["pipelines"][0]["app_data"]["version"] = 3
  72. pipeline_definition = PipelineDefinition(pipeline_definition=pipeline)
  73. validation_manager._validate_pipeline_structure(pipeline_definition=pipeline_definition, response=response)
  74. issues = response.to_json().get("issues")
  75. assert len(issues) == 1
  76. assert issues[0]["severity"] == 1
  77. assert issues[0]["type"] == "invalidPipeline"
  78. assert "needs to be migrated" in issues[0]["message"]
  79. def test_basic_pipeline_structure(validation_manager, load_pipeline):
  80. pipeline, response = load_pipeline("generic_basic_pipeline_only_notebook.pipeline")
  81. pipeline_definition = PipelineDefinition(pipeline_definition=pipeline)
  82. validation_manager._validate_pipeline_structure(pipeline_definition=pipeline_definition, response=response)
  83. assert not response.has_fatal
  84. assert not response.to_json().get("issues")
  85. def test_basic_pipeline_structure_with_scripts(validation_manager, load_pipeline):
  86. pipeline, response = load_pipeline("generic_basic_pipeline_with_scripts.pipeline")
  87. pipeline_definition = PipelineDefinition(pipeline_definition=pipeline)
  88. validation_manager._validate_pipeline_structure(pipeline_definition=pipeline_definition, response=response)
  89. assert not response.has_fatal
  90. assert not response.to_json().get("issues")
  91. @pytest.mark.parametrize("catalog_instance", [KFP_COMPONENT_CACHE_INSTANCE], indirect=True)
  92. async def test_invalid_runtime_node_kubeflow(validation_manager, load_pipeline, catalog_instance):
  93. pipeline, response = load_pipeline("kf_invalid_node_op.pipeline")
  94. node_id = "eace43f8-c4b1-4a25-b331-d57d4fc29426"
  95. pipeline_definition = PipelineDefinition(pipeline_definition=pipeline)
  96. await validation_manager._validate_compatibility(
  97. pipeline_definition=pipeline_definition,
  98. response=response,
  99. pipeline_type="KUBEFLOW_PIPELINES",
  100. pipeline_runtime="kfp",
  101. )
  102. issues = response.to_json().get("issues")
  103. print(issues)
  104. assert len(issues) == 1
  105. assert issues[0]["severity"] == 1
  106. assert issues[0]["type"] == "invalidNodeType"
  107. assert issues[0]["data"]["nodeID"] == node_id
  108. @pytest.mark.parametrize("catalog_instance", [KFP_COMPONENT_CACHE_INSTANCE], indirect=True)
  109. async def test_invalid_runtime_node_kubeflow_with_supernode(validation_manager, load_pipeline, catalog_instance):
  110. pipeline, response = load_pipeline("kf_invalid_node_op_with_supernode.pipeline")
  111. node_id = "98aa7270-639b-42a4-9a07-b31cd0fa3205"
  112. pipeline_id = "00304a2b-dec4-4a73-ab4a-6830f97d7855"
  113. pipeline_definition = PipelineDefinition(pipeline_definition=pipeline)
  114. await validation_manager._validate_compatibility(
  115. pipeline_definition=pipeline_definition,
  116. response=response,
  117. pipeline_type="KUBEFLOW_PIPELINES",
  118. pipeline_runtime="kfp",
  119. )
  120. issues = response.to_json().get("issues")
  121. print(issues)
  122. assert len(issues) == 1
  123. assert issues[0]["severity"] == 1
  124. assert issues[0]["type"] == "invalidNodeType"
  125. assert issues[0]["data"]["pipelineId"] == pipeline_id
  126. assert issues[0]["data"]["nodeID"] == node_id
  127. async def test_invalid_pipeline_runtime_with_kubeflow_execution(validation_manager, load_pipeline):
  128. pipeline, response = load_pipeline("generic_basic_pipeline_with_scripts.pipeline")
  129. pipeline_definition = PipelineDefinition(pipeline_definition=pipeline)
  130. await validation_manager._validate_compatibility(
  131. pipeline_definition=pipeline_definition,
  132. response=response,
  133. pipeline_type="APACHE_AIRFLOW",
  134. pipeline_runtime="kfp",
  135. )
  136. issues = response.to_json().get("issues")
  137. assert len(issues) == 1
  138. assert issues[0]["severity"] == 1
  139. assert issues[0]["type"] == "invalidRuntime"
  140. async def test_invalid_pipeline_runtime_with_local_execution(validation_manager, load_pipeline):
  141. pipeline, response = load_pipeline("generic_basic_pipeline_with_scripts.pipeline")
  142. pipeline_definition = PipelineDefinition(pipeline_definition=pipeline)
  143. await validation_manager._validate_compatibility(
  144. pipeline_definition=pipeline_definition,
  145. response=response,
  146. pipeline_type="APACHE_AIRFLOW",
  147. pipeline_runtime="local",
  148. )
  149. issues = response.to_json().get("issues")
  150. assert len(issues) == 1
  151. assert issues[0]["severity"] == 1
  152. assert issues[0]["type"] == "invalidRuntime"
  153. assert issues[0]["data"]["pipelineType"] == "APACHE_AIRFLOW"
  154. async def test_invalid_node_op_with_airflow(validation_manager, load_pipeline):
  155. pipeline, response = load_pipeline("aa_invalid_node_op.pipeline")
  156. node_id = "749d4641-cee8-4a50-a0ed-30c07439908f"
  157. pipeline_definition = PipelineDefinition(pipeline_definition=pipeline)
  158. await validation_manager._validate_compatibility(
  159. pipeline_definition=pipeline_definition,
  160. response=response,
  161. pipeline_type="APACHE_AIRFLOW",
  162. pipeline_runtime="airflow",
  163. )
  164. issues = response.to_json().get("issues")
  165. assert len(issues) == 1
  166. assert issues[0]["severity"] == 1
  167. assert issues[0]["type"] == "invalidNodeType"
  168. assert issues[0]["data"]["nodeID"] == node_id
  169. async def test_invalid_node_property_structure(validation_manager, monkeypatch, load_pipeline):
  170. pipeline, response = load_pipeline("generic_invalid_node_property_structure.pipeline")
  171. node_id = "88ab83dc-d5f0-443a-8837-788ed16851b7"
  172. node_property = "runtime_image"
  173. pvm = validation_manager
  174. monkeypatch.setattr(pvm, "_validate_filepath", lambda node_id, node_label, property_name, filename, response: True)
  175. monkeypatch.setattr(pvm, "_validate_label", lambda node_id, node_label, response: True)
  176. pipeline_definition = PipelineDefinition(pipeline_definition=pipeline)
  177. await pvm._validate_node_properties(
  178. pipeline_definition=pipeline_definition, response=response, pipeline_type="GENERIC", pipeline_runtime="kfp"
  179. )
  180. issues = response.to_json().get("issues")
  181. assert len(issues) == 1
  182. assert issues[0]["severity"] == 1
  183. assert issues[0]["type"] == "invalidNodeProperty"
  184. assert issues[0]["data"]["propertyName"] == node_property
  185. assert issues[0]["data"]["nodeID"] == node_id
  186. @pytest.mark.parametrize("catalog_instance", [KFP_COMPONENT_CACHE_INSTANCE], indirect=True)
  187. async def test_missing_node_property_for_kubeflow_pipeline(
  188. validation_manager, monkeypatch, load_pipeline, catalog_instance
  189. ):
  190. pipeline, response = load_pipeline("kf_invalid_node_property_in_component.pipeline")
  191. node_id = "fe08b42d-bd8c-4e97-8010-0503a3185427"
  192. node_property = "notebook"
  193. pvm = validation_manager
  194. monkeypatch.setattr(pvm, "_validate_filepath", lambda node_id, file_dir, property_name, filename, response: True)
  195. pipeline_definition = PipelineDefinition(pipeline_definition=pipeline)
  196. await pvm._validate_node_properties(
  197. pipeline_definition=pipeline_definition,
  198. response=response,
  199. pipeline_type="KUBEFLOW_PIPELINES",
  200. pipeline_runtime="kfp",
  201. )
  202. issues = response.to_json().get("issues")
  203. assert len(issues) == 1
  204. assert issues[0]["severity"] == 1
  205. assert issues[0]["type"] == "invalidNodeProperty"
  206. assert issues[0]["data"]["propertyName"] == node_property
  207. assert issues[0]["data"]["nodeID"] == node_id
  208. def test_invalid_node_property_image_name(validation_manager, load_pipeline):
  209. pipeline, response = load_pipeline("generic_invalid_node_property_image_name.pipeline")
  210. node_ids = ["88ab83dc-d5f0-443a-8837-788ed16851b7", "7ae74ba6-d49f-48ea-9e4f-e44d13594b2f"]
  211. node_property = "runtime_image"
  212. for i, node_id in enumerate(node_ids):
  213. node = pipeline["pipelines"][0]["nodes"][i]
  214. node_label = node["app_data"].get("label")
  215. image_name = node["app_data"]["component_parameters"].get("runtime_image")
  216. validation_manager._validate_container_image_name(node["id"], node_label, image_name, response)
  217. issues = response.to_json().get("issues")
  218. assert len(issues) == 2
  219. # Test missing runtime image in node 0
  220. assert issues[0]["severity"] == 1
  221. assert issues[0]["type"] == "invalidNodeProperty"
  222. assert issues[0]["data"]["propertyName"] == node_property
  223. assert issues[0]["data"]["nodeID"] == node_ids[0]
  224. assert issues[0]["message"] == "Required property value is missing."
  225. # Test invalid format for runtime image in node 1
  226. assert issues[1]["severity"] == 1
  227. assert issues[1]["type"] == "invalidNodeProperty"
  228. assert issues[1]["data"]["propertyName"] == node_property
  229. assert issues[1]["data"]["nodeID"] == node_ids[1]
  230. assert (
  231. issues[1]["message"] == "Node contains an invalid runtime image. Runtime image "
  232. "must conform to the format [registry/]owner/image:tag"
  233. )
  234. def test_invalid_node_property_image_name_list(validation_manager):
  235. response = ValidationResponse()
  236. node_label = "test_label"
  237. node_id = "test-id"
  238. failing_image_names = [
  239. "12345566:one-two-three",
  240. "someregistry.io/some_org/some_tag/something/",
  241. "docker.io//missing_org_name:test",
  242. ]
  243. for image_name in failing_image_names:
  244. validation_manager._validate_container_image_name(node_id, node_label, image_name, response)
  245. issues = response.to_json().get("issues")
  246. assert len(issues) == len(failing_image_names)
  247. def test_invalid_node_property_dependency_filepath_workspace(validation_manager):
  248. response = ValidationResponse()
  249. node = {"id": "test-id", "app_data": {"label": "test"}}
  250. property_name = "test-property"
  251. validation_manager._validate_filepath(
  252. node_id=node["id"],
  253. file_dir=os.getcwd(),
  254. property_name=property_name,
  255. node_label=node["app_data"]["label"],
  256. filename="../invalid_filepath/to/file.ipynb",
  257. response=response,
  258. )
  259. issues = response.to_json().get("issues")
  260. assert issues[0]["severity"] == 1
  261. assert issues[0]["type"] == "invalidFilePath"
  262. assert issues[0]["data"]["propertyName"] == property_name
  263. assert issues[0]["data"]["nodeID"] == node["id"]
  264. def test_invalid_node_property_dependency_filepath_non_existent(validation_manager):
  265. response = ValidationResponse()
  266. node = {"id": "test-id", "app_data": {"label": "test"}}
  267. property_name = "test-property"
  268. validation_manager._validate_filepath(
  269. node_id=node["id"],
  270. file_dir=os.getcwd(),
  271. property_name=property_name,
  272. node_label=node["app_data"]["label"],
  273. filename="invalid_filepath/to/file.ipynb",
  274. response=response,
  275. )
  276. issues = response.to_json().get("issues")
  277. assert issues[0]["severity"] == 1
  278. assert issues[0]["type"] == "invalidFilePath"
  279. assert issues[0]["data"]["propertyName"] == property_name
  280. assert issues[0]["data"]["nodeID"] == node["id"]
  281. def test_valid_node_property_dependency_filepath(validation_manager):
  282. response = ValidationResponse()
  283. valid_filename = os.path.join(
  284. os.path.dirname(__file__), "resources/validation_pipelines/generic_single_cycle.pipeline"
  285. )
  286. node = {"id": "test-id", "app_data": {"label": "test"}}
  287. property_name = "test-property"
  288. validation_manager._validate_filepath(
  289. node_id=node["id"],
  290. file_dir=os.getcwd(),
  291. property_name=property_name,
  292. node_label=node["app_data"]["label"],
  293. filename=valid_filename,
  294. response=response,
  295. )
  296. assert not response.has_fatal
  297. assert not response.to_json().get("issues")
  298. async def test_valid_node_property_pipeline_filepath(monkeypatch, validation_manager, load_pipeline):
  299. pipeline, response = load_pipeline("generic_basic_filepath_check.pipeline")
  300. monkeypatch.setattr(validation_manager, "_validate_label", lambda node_id, node_label, response: True)
  301. pipeline_definition = PipelineDefinition(pipeline_definition=pipeline)
  302. await validation_manager._validate_node_properties(
  303. pipeline_definition=pipeline_definition, response=response, pipeline_type="GENERIC", pipeline_runtime="kfp"
  304. )
  305. assert not response.has_fatal
  306. assert not response.to_json().get("issues")
  307. def test_invalid_node_property_resource_value(validation_manager, load_pipeline):
  308. pipeline, response = load_pipeline("generic_invalid_node_property_hardware_resources.pipeline")
  309. node_id = "88ab83dc-d5f0-443a-8837-788ed16851b7"
  310. node = pipeline["pipelines"][0]["nodes"][0]
  311. validation_manager._validate_resource_value(
  312. node["id"],
  313. node["app_data"]["label"],
  314. resource_name="memory",
  315. resource_value=node["app_data"]["component_parameters"]["memory"],
  316. response=response,
  317. )
  318. issues = response.to_json().get("issues")
  319. assert len(issues) == 1
  320. assert issues[0]["severity"] == 1
  321. assert issues[0]["type"] == "invalidNodeProperty"
  322. assert issues[0]["data"]["propertyName"] == "memory"
  323. assert issues[0]["data"]["nodeID"] == node_id
  324. def test_invalid_node_property_env_var(validation_manager):
  325. response = ValidationResponse()
  326. node = {"id": "test-id", "app_data": {"label": "test"}}
  327. invalid_env_var = 'TEST_ENV_ONE"test_one"'
  328. validation_manager._validate_environmental_variables(
  329. node_id=node["id"], node_label=node["app_data"]["label"], env_var=invalid_env_var, response=response
  330. )
  331. issues = response.to_json().get("issues")
  332. assert issues[0]["severity"] == 1
  333. assert issues[0]["type"] == "invalidEnvPair"
  334. assert issues[0]["data"]["propertyName"] == "env_vars"
  335. assert issues[0]["data"]["nodeID"] == "test-id"
  336. def test_invalid_node_property_volumes(validation_manager):
  337. response = ValidationResponse()
  338. node = {"id": "test-id", "app_data": {"label": "test"}}
  339. volumes = [
  340. VolumeMount("/mount/test", "rwx-test-claim"), # valid
  341. VolumeMount("/mount/test_two", "second-claim"), # valid
  342. VolumeMount("/mount/test_four", "second#claim"), # invalid pvc name
  343. ]
  344. validation_manager._validate_mounted_volumes(
  345. node_id=node["id"], node_label=node["app_data"]["label"], volumes=volumes, response=response
  346. )
  347. issues = response.to_json().get("issues")
  348. assert issues[0]["severity"] == 1
  349. assert issues[0]["type"] == "invalidVolumeMount"
  350. assert issues[0]["data"]["propertyName"] == MOUNTED_VOLUMES
  351. assert issues[0]["data"]["nodeID"] == "test-id"
  352. assert "not a valid Kubernetes resource name" in issues[0]["message"]
  353. def test_invalid_node_property_secrets(validation_manager):
  354. response = ValidationResponse()
  355. node = {"id": "test-id", "app_data": {"label": "test"}}
  356. secrets = [
  357. KubernetesSecret("ENV_VAR1", "test-secret", "test-key1"), # valid
  358. KubernetesSecret("ENV_VAR2", "test-secret", "test-key2"), # valid
  359. KubernetesSecret("ENV_VAR3", "test-secret", ""), # invalid: improper format of secret name/key
  360. KubernetesSecret("ENV_VAR5", "test%secret", "test-key"), # invalid: not a valid Kubernetes resource name
  361. KubernetesSecret("ENV_VAR6", "test-secret", "test$key2"), # invalid: not a valid Kubernetes secret key
  362. ]
  363. validation_manager._validate_kubernetes_secrets(
  364. node_id=node["id"], node_label=node["app_data"]["label"], secrets=secrets, response=response
  365. )
  366. issues = response.to_json().get("issues")
  367. assert issues[0]["severity"] == 1
  368. assert issues[0]["type"] == "invalidKubernetesSecret"
  369. assert issues[0]["data"]["propertyName"] == KUBERNETES_SECRETS
  370. assert issues[0]["data"]["nodeID"] == "test-id"
  371. assert "improperly formatted representation of secret name and key" in issues[0]["message"]
  372. assert "not a valid Kubernetes resource name" in issues[1]["message"]
  373. assert "not a valid Kubernetes secret key" in issues[2]["message"]
  374. def test_valid_node_property_label(validation_manager):
  375. response = ValidationResponse()
  376. node = {"id": "test-id"}
  377. valid_label_name = "dead-bread-dead-bread-dead-bread-dead-bread-dead-bread-dead-bre"
  378. validation_manager._validate_label(node_id=node["id"], node_label=valid_label_name, response=response)
  379. issues = response.to_json().get("issues")
  380. assert len(issues) == 0
  381. def test_valid_node_property_label_min_length(validation_manager):
  382. response = ValidationResponse()
  383. node = {"id": "test-id", "app_data": {"label": "test"}}
  384. valid_label_name = "d"
  385. validation_manager._validate_label(node_id=node["id"], node_label=valid_label_name, response=response)
  386. issues = response.to_json().get("issues")
  387. assert len(issues) == 0
  388. def test_invalid_node_property_label_filename_exceeds_max_length(validation_manager):
  389. response = ValidationResponse()
  390. node = {"id": "test-id", "app_data": {"label": "test"}}
  391. valid_label_name = "deadbread-deadbread-deadbread-deadbread-deadbread-deadbread-de.py"
  392. validation_manager._validate_label(node_id=node["id"], node_label=valid_label_name, response=response)
  393. issues = response.to_json().get("issues")
  394. assert len(issues) == 2
  395. def test_invalid_node_property_label_max_length(validation_manager):
  396. response = ValidationResponse()
  397. node = {"id": "test-id", "app_data": {"label": "test"}}
  398. invalid_label_name = "dead-bread-dead-bread-dead-bread-dead-bread-dead-bread-dead-bred"
  399. validation_manager._validate_label(node_id=node["id"], node_label=invalid_label_name, response=response)
  400. issues = response.to_json().get("issues")
  401. assert len(issues) == 1
  402. assert issues[0]["severity"] == 2
  403. assert issues[0]["type"] == "invalidNodeLabel"
  404. assert issues[0]["data"]["propertyName"] == "label"
  405. assert issues[0]["data"]["nodeID"] == "test-id"
  406. def test_valid_node_property_label_filename_has_relative_path(validation_manager):
  407. response = ValidationResponse()
  408. node = {"id": "test-id", "app_data": {"label": "test"}}
  409. valid_label_name = "deadbread.py"
  410. validation_manager._validate_label(node_id=node["id"], node_label=valid_label_name, response=response)
  411. issues = response.to_json().get("issues")
  412. assert len(issues) == 0
  413. def test_invalid_node_property_label_bad_characters(validation_manager):
  414. response = ValidationResponse()
  415. node = {"id": "test-id"}
  416. invalid_label_name = "bad_label_*&^&$"
  417. validation_manager._validate_label(node_id=node["id"], node_label=invalid_label_name, response=response)
  418. issues = response.to_json().get("issues")
  419. assert len(issues) == 1
  420. assert issues[0]["severity"] == 2
  421. assert issues[0]["type"] == "invalidNodeLabel"
  422. assert issues[0]["data"]["propertyName"] == "label"
  423. assert issues[0]["data"]["nodeID"] == "test-id"
  424. def test_pipeline_graph_single_cycle(validation_manager, load_pipeline):
  425. pipeline, response = load_pipeline("generic_single_cycle.pipeline")
  426. # cycle_ID = ['c309f6dd-b022-4b1c-b2b0-b6449bb26e8f', '8cb986cb-4fc9-4b1d-864d-0ec64b7ac13c']
  427. validation_manager._validate_pipeline_graph(pipeline=pipeline, response=response)
  428. issues = response.to_json().get("issues")
  429. assert len(issues) == 1
  430. assert issues[0]["severity"] == 1
  431. assert issues[0]["type"] == "circularReference"
  432. # assert issues[0]['data']['linkIDList'].sort() == cycle_ID.sort()
  433. def test_pipeline_graph_double_cycle(validation_manager, load_pipeline):
  434. pipeline, response = load_pipeline("generic_double_cycle.pipeline")
  435. # cycle_ID = ['597b2971-b95d-4df7-a36d-9d93b0345298', 'b63378e4-9085-4a33-9330-6f86054681f4']
  436. # cycle_two_ID = ['c309f6dd-b022-4b1c-b2b0-b6449bb26e8f', '8cb986cb-4fc9-4b1d-864d-0ec64b7ac13c']
  437. validation_manager._validate_pipeline_graph(pipeline=pipeline, response=response)
  438. issues = response.to_json().get("issues")
  439. assert len(issues) == 1
  440. assert issues[0]["severity"] == 1
  441. assert issues[0]["type"] == "circularReference"
  442. # assert issues[0]['data']['linkIDList'].sort() == cycle_ID.sort()
  443. # assert issues[1]['severity'] == 1
  444. # assert issues[1]['type'] == 'circularReference'
  445. # assert issues[1]['data']['linkIDList'].sort() == cycle_two_ID.sort()
  446. def test_pipeline_graph_singleton(validation_manager, load_pipeline):
  447. pipeline, response = load_pipeline("generic_singleton.pipeline")
  448. node_id = "0195fefd-3ceb-4a90-a12c-3958ef0ff42e"
  449. validation_manager._validate_pipeline_graph(pipeline=pipeline, response=response)
  450. issues = response.to_json().get("issues")
  451. assert len(issues) == 1
  452. assert not response.has_fatal
  453. assert issues[0]["severity"] == 2
  454. assert issues[0]["type"] == "singletonReference"
  455. assert issues[0]["data"]["nodeID"] == node_id
  456. def test_pipeline_valid_kfp_with_supernode(validation_manager, load_pipeline):
  457. pipeline, response = load_pipeline("kf_supernode_valid.pipeline")
  458. validation_manager._validate_pipeline_graph(pipeline=pipeline, response=response)
  459. issues = response.to_json().get("issues")
  460. assert len(issues) == 0
  461. assert not response.has_fatal
  462. def test_pipeline_invalid_single_cycle_kfp_with_supernode(validation_manager, load_pipeline):
  463. pipeline, response = load_pipeline("kf_supernode_invalid_single_cycle.pipeline")
  464. validation_manager._validate_pipeline_graph(pipeline=pipeline, response=response)
  465. issues = response.to_json().get("issues")
  466. assert len(issues) == 1
  467. assert response.has_fatal
  468. assert issues[0]["severity"] == 1
  469. assert issues[0]["type"] == "circularReference"
  470. @pytest.mark.parametrize("catalog_instance", [KFP_COMPONENT_CACHE_INSTANCE], indirect=True)
  471. async def test_pipeline_kfp_inputpath_parameter(validation_manager, load_pipeline, catalog_instance, component_cache):
  472. pipeline, response = load_pipeline("kf_inputpath_parameter.pipeline")
  473. pipeline_definition = PipelineDefinition(pipeline_definition=pipeline)
  474. await validation_manager._validate_node_properties(
  475. pipeline_definition=pipeline_definition,
  476. response=response,
  477. pipeline_type="KUBEFLOW_PIPELINES",
  478. pipeline_runtime="kfp",
  479. )
  480. issues = response.to_json().get("issues")
  481. assert len(issues) == 0
  482. @pytest.mark.parametrize("catalog_instance", [KFP_COMPONENT_CACHE_INSTANCE], indirect=True)
  483. async def test_pipeline_invalid_kfp_inputpath_parameter(
  484. validation_manager, load_pipeline, catalog_instance, component_cache
  485. ):
  486. invalid_key_node_id = "089a12df-fe2f-4fcb-ae37-a1f8a6259ca1"
  487. missing_param_node_id = "e8820c55-dc79-46d1-b32e-924fa5d70d2a"
  488. pipeline, response = load_pipeline("kf_invalid_inputpath_parameter.pipeline")
  489. pipeline_definition = PipelineDefinition(pipeline_definition=pipeline)
  490. await validation_manager._validate_node_properties(
  491. pipeline_definition=pipeline_definition,
  492. response=response,
  493. pipeline_type="KUBEFLOW_PIPELINES",
  494. pipeline_runtime="kfp",
  495. )
  496. issues = response.to_json().get("issues")
  497. assert len(issues) == 2
  498. assert response.has_fatal
  499. assert issues[0]["severity"] == 1
  500. assert issues[0]["type"] == "invalidNodeProperty"
  501. assert issues[0]["data"]["nodeID"] == invalid_key_node_id
  502. assert issues[1]["severity"] == 1
  503. assert issues[1]["type"] == "invalidNodeProperty"
  504. assert issues[1]["data"]["nodeID"] == missing_param_node_id
  505. @pytest.mark.parametrize("catalog_instance", [KFP_COMPONENT_CACHE_INSTANCE], indirect=True)
  506. async def test_pipeline_invalid_kfp_inputpath_missing_connection(
  507. validation_manager, load_pipeline, catalog_instance, component_cache
  508. ):
  509. invalid_node_id = "5b78ea0a-e5fc-4022-94d4-7b9dc170d794"
  510. pipeline, response = load_pipeline("kf_invalid_inputpath_missing_connection.pipeline")
  511. pipeline_definition = PipelineDefinition(pipeline_definition=pipeline)
  512. await validation_manager._validate_node_properties(
  513. pipeline_definition=pipeline_definition,
  514. response=response,
  515. pipeline_type="KUBEFLOW_PIPELINES",
  516. pipeline_runtime="kfp",
  517. )
  518. issues = response.to_json().get("issues")
  519. assert len(issues) == 1
  520. assert response.has_fatal
  521. assert issues[0]["severity"] == 1
  522. assert issues[0]["type"] == "invalidNodeProperty"
  523. assert issues[0]["data"]["nodeID"] == invalid_node_id
  524. @pytest.mark.parametrize("catalog_instance", [AIRFLOW_COMPONENT_CACHE_INSTANCE], indirect=True)
  525. async def test_pipeline_aa_parent_node_missing_xcom_push(
  526. validation_manager, load_pipeline, catalog_instance, component_cache
  527. ):
  528. invalid_node_id = "b863d458-21b5-4a46-8420-5a814b7bd525"
  529. invalid_operator = "BashOperator"
  530. pipeline, response = load_pipeline("aa_parent_node_missing_xcom.pipeline")
  531. pipeline_definition = PipelineDefinition(pipeline_definition=pipeline)
  532. await validation_manager._validate_node_properties(
  533. pipeline_definition=pipeline_definition,
  534. response=response,
  535. pipeline_type="APACHE_AIRFLOW",
  536. pipeline_runtime="airflow",
  537. )
  538. issues = response.to_json().get("issues")
  539. assert len(issues) == 1
  540. assert response.has_fatal
  541. assert issues[0]["severity"] == 1
  542. assert issues[0]["type"] == "invalidNodeProperty"
  543. assert issues[0]["data"]["nodeID"] == invalid_node_id
  544. assert issues[0]["data"]["parentNodeID"] == invalid_operator