test_validation.py 38 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857
  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_TEST_OPERATOR_CATALOG
  18. from conftest import KFP_COMPONENT_CACHE_INSTANCE
  19. import pytest
  20. from elyra.pipeline.pipeline import KubernetesAnnotation
  21. from elyra.pipeline.pipeline import KubernetesSecret
  22. from elyra.pipeline.pipeline import KubernetesToleration
  23. from elyra.pipeline.pipeline import PIPELINE_CURRENT_VERSION
  24. from elyra.pipeline.pipeline import VolumeMount
  25. from elyra.pipeline.pipeline_constants import KUBERNETES_POD_ANNOTATIONS
  26. from elyra.pipeline.pipeline_constants import KUBERNETES_SECRETS
  27. from elyra.pipeline.pipeline_constants import KUBERNETES_TOLERATIONS
  28. from elyra.pipeline.pipeline_constants import MOUNTED_VOLUMES
  29. from elyra.pipeline.pipeline_definition import PipelineDefinition
  30. from elyra.pipeline.validation import PipelineValidationManager
  31. from elyra.pipeline.validation import ValidationResponse
  32. from elyra.tests.pipeline.util import _read_pipeline_resource
  33. @pytest.fixture
  34. def load_pipeline():
  35. def _function(pipeline_filepath):
  36. response = ValidationResponse()
  37. pipeline = _read_pipeline_resource(f"resources/validation_pipelines/{pipeline_filepath}")
  38. return pipeline, response
  39. yield _function
  40. @pytest.fixture
  41. def validation_manager(setup_factory_data, component_cache):
  42. root = os.path.realpath(os.path.join(os.getcwd(), os.path.dirname(__file__), "resources/validation_pipelines"))
  43. yield PipelineValidationManager.instance(root_dir=root)
  44. PipelineValidationManager.clear_instance()
  45. async def test_invalid_lower_pipeline_version(validation_manager, load_pipeline):
  46. pipeline, response = load_pipeline("generic_basic_pipeline_only_notebook.pipeline")
  47. pipeline_version = PIPELINE_CURRENT_VERSION - 1
  48. pipeline["pipelines"][0]["app_data"]["version"] = pipeline_version
  49. pipeline_definition = PipelineDefinition(pipeline_definition=pipeline)
  50. validation_manager._validate_pipeline_structure(pipeline_definition=pipeline_definition, response=response)
  51. issues = response.to_json().get("issues")
  52. assert len(issues) == 1
  53. assert issues[0]["severity"] == 1
  54. assert issues[0]["type"] == "invalidPipeline"
  55. assert (
  56. issues[0]["message"] == f"Pipeline version {pipeline_version} is out of date "
  57. "and needs to be migrated using the Elyra pipeline editor."
  58. )
  59. def test_invalid_upper_pipeline_version(validation_manager, load_pipeline):
  60. pipeline, response = load_pipeline("generic_basic_pipeline_only_notebook.pipeline")
  61. pipeline_version = PIPELINE_CURRENT_VERSION + 1
  62. pipeline["pipelines"][0]["app_data"]["version"] = pipeline_version
  63. pipeline_definition = PipelineDefinition(pipeline_definition=pipeline)
  64. validation_manager._validate_pipeline_structure(pipeline_definition=pipeline_definition, response=response)
  65. issues = response.to_json().get("issues")
  66. assert len(issues) == 1
  67. assert issues[0]["severity"] == 1
  68. assert issues[0]["type"] == "invalidPipeline"
  69. assert (
  70. issues[0]["message"] == "Pipeline was last edited in a newer version of Elyra. "
  71. "Update Elyra to use this pipeline."
  72. )
  73. def test_invalid_pipeline_version_that_needs_migration(validation_manager, load_pipeline):
  74. pipeline, response = load_pipeline("generic_basic_pipeline_only_notebook.pipeline")
  75. pipeline["pipelines"][0]["app_data"]["version"] = 3
  76. pipeline_definition = PipelineDefinition(pipeline_definition=pipeline)
  77. validation_manager._validate_pipeline_structure(pipeline_definition=pipeline_definition, response=response)
  78. issues = response.to_json().get("issues")
  79. assert len(issues) == 1
  80. assert issues[0]["severity"] == 1
  81. assert issues[0]["type"] == "invalidPipeline"
  82. assert "needs to be migrated" in issues[0]["message"]
  83. def test_basic_pipeline_structure(validation_manager, load_pipeline):
  84. pipeline, response = load_pipeline("generic_basic_pipeline_only_notebook.pipeline")
  85. pipeline_definition = PipelineDefinition(pipeline_definition=pipeline)
  86. validation_manager._validate_pipeline_structure(pipeline_definition=pipeline_definition, response=response)
  87. assert not response.has_fatal
  88. assert not response.to_json().get("issues")
  89. def test_basic_pipeline_structure_with_scripts(validation_manager, load_pipeline):
  90. pipeline, response = load_pipeline("generic_basic_pipeline_with_scripts.pipeline")
  91. pipeline_definition = PipelineDefinition(pipeline_definition=pipeline)
  92. validation_manager._validate_pipeline_structure(pipeline_definition=pipeline_definition, response=response)
  93. assert not response.has_fatal
  94. assert not response.to_json().get("issues")
  95. @pytest.mark.parametrize("catalog_instance", [KFP_COMPONENT_CACHE_INSTANCE], indirect=True)
  96. async def test_invalid_runtime_node_kubeflow(validation_manager, load_pipeline, catalog_instance):
  97. pipeline, response = load_pipeline("kf_invalid_node_op.pipeline")
  98. node_id = "eace43f8-c4b1-4a25-b331-d57d4fc29426"
  99. pipeline_definition = PipelineDefinition(pipeline_definition=pipeline)
  100. await validation_manager._validate_compatibility(
  101. pipeline_definition=pipeline_definition,
  102. response=response,
  103. pipeline_type="KUBEFLOW_PIPELINES",
  104. pipeline_runtime="kfp",
  105. )
  106. issues = response.to_json().get("issues")
  107. print(issues)
  108. assert len(issues) == 1
  109. assert issues[0]["severity"] == 1
  110. assert issues[0]["type"] == "invalidNodeType"
  111. assert issues[0]["data"]["nodeID"] == node_id
  112. @pytest.mark.parametrize("catalog_instance", [KFP_COMPONENT_CACHE_INSTANCE], indirect=True)
  113. async def test_invalid_runtime_node_kubeflow_with_supernode(validation_manager, load_pipeline, catalog_instance):
  114. pipeline, response = load_pipeline("kf_invalid_node_op_with_supernode.pipeline")
  115. node_id = "98aa7270-639b-42a4-9a07-b31cd0fa3205"
  116. pipeline_id = "00304a2b-dec4-4a73-ab4a-6830f97d7855"
  117. pipeline_definition = PipelineDefinition(pipeline_definition=pipeline)
  118. await validation_manager._validate_compatibility(
  119. pipeline_definition=pipeline_definition,
  120. response=response,
  121. pipeline_type="KUBEFLOW_PIPELINES",
  122. pipeline_runtime="kfp",
  123. )
  124. issues = response.to_json().get("issues")
  125. print(issues)
  126. assert len(issues) == 1
  127. assert issues[0]["severity"] == 1
  128. assert issues[0]["type"] == "invalidNodeType"
  129. assert issues[0]["data"]["pipelineId"] == pipeline_id
  130. assert issues[0]["data"]["nodeID"] == node_id
  131. async def test_invalid_pipeline_runtime_with_kubeflow_execution(validation_manager, load_pipeline):
  132. pipeline, response = load_pipeline("generic_basic_pipeline_with_scripts.pipeline")
  133. pipeline_definition = PipelineDefinition(pipeline_definition=pipeline)
  134. await validation_manager._validate_compatibility(
  135. pipeline_definition=pipeline_definition,
  136. response=response,
  137. pipeline_type="APACHE_AIRFLOW",
  138. pipeline_runtime="kfp",
  139. )
  140. issues = response.to_json().get("issues")
  141. assert len(issues) == 1
  142. assert issues[0]["severity"] == 1
  143. assert issues[0]["type"] == "invalidRuntime"
  144. async def test_invalid_pipeline_runtime_with_local_execution(validation_manager, load_pipeline):
  145. pipeline, response = load_pipeline("generic_basic_pipeline_with_scripts.pipeline")
  146. pipeline_definition = PipelineDefinition(pipeline_definition=pipeline)
  147. await validation_manager._validate_compatibility(
  148. pipeline_definition=pipeline_definition,
  149. response=response,
  150. pipeline_type="APACHE_AIRFLOW",
  151. pipeline_runtime="local",
  152. )
  153. issues = response.to_json().get("issues")
  154. assert len(issues) == 1
  155. assert issues[0]["severity"] == 1
  156. assert issues[0]["type"] == "invalidRuntime"
  157. assert issues[0]["data"]["pipelineType"] == "APACHE_AIRFLOW"
  158. async def test_invalid_node_op_with_airflow(validation_manager, load_pipeline):
  159. pipeline, response = load_pipeline("aa_invalid_node_op.pipeline")
  160. node_id = "749d4641-cee8-4a50-a0ed-30c07439908f"
  161. pipeline_definition = PipelineDefinition(pipeline_definition=pipeline)
  162. await validation_manager._validate_compatibility(
  163. pipeline_definition=pipeline_definition,
  164. response=response,
  165. pipeline_type="APACHE_AIRFLOW",
  166. pipeline_runtime="airflow",
  167. )
  168. issues = response.to_json().get("issues")
  169. assert len(issues) == 1
  170. assert issues[0]["severity"] == 1
  171. assert issues[0]["type"] == "invalidNodeType"
  172. assert issues[0]["data"]["nodeID"] == node_id
  173. async def test_invalid_node_property_structure(validation_manager, monkeypatch, load_pipeline):
  174. pipeline, response = load_pipeline("generic_invalid_node_property_structure.pipeline")
  175. node_id = "88ab83dc-d5f0-443a-8837-788ed16851b7"
  176. node_property = "runtime_image"
  177. pvm = validation_manager
  178. monkeypatch.setattr(pvm, "_validate_filepath", lambda node_id, node_label, property_name, filename, response: True)
  179. monkeypatch.setattr(pvm, "_validate_label", lambda node_id, node_label, response: True)
  180. pipeline_definition = PipelineDefinition(pipeline_definition=pipeline)
  181. await pvm._validate_node_properties(
  182. pipeline_definition=pipeline_definition, response=response, pipeline_type="GENERIC", pipeline_runtime="kfp"
  183. )
  184. issues = response.to_json().get("issues")
  185. assert len(issues) == 1
  186. assert issues[0]["severity"] == 1
  187. assert issues[0]["type"] == "invalidNodeProperty"
  188. assert issues[0]["data"]["propertyName"] == node_property
  189. assert issues[0]["data"]["nodeID"] == node_id
  190. @pytest.mark.parametrize("catalog_instance", [KFP_COMPONENT_CACHE_INSTANCE], indirect=True)
  191. async def test_missing_node_property_for_kubeflow_pipeline(
  192. validation_manager, monkeypatch, load_pipeline, catalog_instance
  193. ):
  194. pipeline, response = load_pipeline("kf_invalid_node_property_in_component.pipeline")
  195. node_id = "fe08b42d-bd8c-4e97-8010-0503a3185427"
  196. node_property = "notebook"
  197. pvm = validation_manager
  198. monkeypatch.setattr(pvm, "_validate_filepath", lambda node_id, file_dir, property_name, filename, response: True)
  199. pipeline_definition = PipelineDefinition(pipeline_definition=pipeline)
  200. await pvm._validate_node_properties(
  201. pipeline_definition=pipeline_definition,
  202. response=response,
  203. pipeline_type="KUBEFLOW_PIPELINES",
  204. pipeline_runtime="kfp",
  205. )
  206. issues = response.to_json().get("issues")
  207. assert len(issues) == 1
  208. assert issues[0]["severity"] == 1
  209. assert issues[0]["type"] == "invalidNodeProperty"
  210. assert issues[0]["data"]["propertyName"] == node_property
  211. assert issues[0]["data"]["nodeID"] == node_id
  212. def test_invalid_node_property_image_name(validation_manager, load_pipeline):
  213. pipeline, response = load_pipeline("generic_invalid_node_property_image_name.pipeline")
  214. node_ids = ["88ab83dc-d5f0-443a-8837-788ed16851b7", "7ae74ba6-d49f-48ea-9e4f-e44d13594b2f"]
  215. node_property = "runtime_image"
  216. for i, node_id in enumerate(node_ids):
  217. node = pipeline["pipelines"][0]["nodes"][i]
  218. node_label = node["app_data"].get("label")
  219. image_name = node["app_data"]["component_parameters"].get("runtime_image")
  220. validation_manager._validate_container_image_name(node["id"], node_label, image_name, response)
  221. issues = response.to_json().get("issues")
  222. assert len(issues) == 2
  223. # Test missing runtime image in node 0
  224. assert issues[0]["severity"] == 1
  225. assert issues[0]["type"] == "invalidNodeProperty"
  226. assert issues[0]["data"]["propertyName"] == node_property
  227. assert issues[0]["data"]["nodeID"] == node_ids[0]
  228. assert issues[0]["message"] == "Required property value is missing."
  229. # Test invalid format for runtime image in node 1
  230. assert issues[1]["severity"] == 1
  231. assert issues[1]["type"] == "invalidNodeProperty"
  232. assert issues[1]["data"]["propertyName"] == node_property
  233. assert issues[1]["data"]["nodeID"] == node_ids[1]
  234. assert (
  235. issues[1]["message"] == "Node contains an invalid runtime image. Runtime image "
  236. "must conform to the format [registry/]owner/image:tag"
  237. )
  238. def test_invalid_node_property_image_name_list(validation_manager):
  239. response = ValidationResponse()
  240. node_label = "test_label"
  241. node_id = "test-id"
  242. failing_image_names = [
  243. "12345566:one-two-three",
  244. "someregistry.io/some_org/some_tag/something/",
  245. "docker.io//missing_org_name:test",
  246. ]
  247. for image_name in failing_image_names:
  248. validation_manager._validate_container_image_name(node_id, node_label, image_name, response)
  249. issues = response.to_json().get("issues")
  250. assert len(issues) == len(failing_image_names)
  251. def test_invalid_node_property_dependency_filepath_workspace(validation_manager):
  252. response = ValidationResponse()
  253. node = {"id": "test-id", "app_data": {"label": "test"}}
  254. property_name = "test-property"
  255. validation_manager._validate_filepath(
  256. node_id=node["id"],
  257. file_dir=os.getcwd(),
  258. property_name=property_name,
  259. node_label=node["app_data"]["label"],
  260. filename="../invalid_filepath/to/file.ipynb",
  261. response=response,
  262. )
  263. issues = response.to_json().get("issues")
  264. assert issues[0]["severity"] == 1
  265. assert issues[0]["type"] == "invalidFilePath"
  266. assert issues[0]["data"]["propertyName"] == property_name
  267. assert issues[0]["data"]["nodeID"] == node["id"]
  268. def test_invalid_node_property_dependency_filepath_non_existent(validation_manager):
  269. response = ValidationResponse()
  270. node = {"id": "test-id", "app_data": {"label": "test"}}
  271. property_name = "test-property"
  272. validation_manager._validate_filepath(
  273. node_id=node["id"],
  274. file_dir=os.getcwd(),
  275. property_name=property_name,
  276. node_label=node["app_data"]["label"],
  277. filename="invalid_filepath/to/file.ipynb",
  278. response=response,
  279. )
  280. issues = response.to_json().get("issues")
  281. assert issues[0]["severity"] == 1
  282. assert issues[0]["type"] == "invalidFilePath"
  283. assert issues[0]["data"]["propertyName"] == property_name
  284. assert issues[0]["data"]["nodeID"] == node["id"]
  285. def test_valid_node_property_dependency_filepath(validation_manager):
  286. response = ValidationResponse()
  287. valid_filename = os.path.join(
  288. os.path.dirname(__file__), "resources/validation_pipelines/generic_single_cycle.pipeline"
  289. )
  290. node = {"id": "test-id", "app_data": {"label": "test"}}
  291. property_name = "test-property"
  292. validation_manager._validate_filepath(
  293. node_id=node["id"],
  294. file_dir=os.getcwd(),
  295. property_name=property_name,
  296. node_label=node["app_data"]["label"],
  297. filename=valid_filename,
  298. response=response,
  299. )
  300. assert not response.has_fatal
  301. assert not response.to_json().get("issues")
  302. async def test_valid_node_property_pipeline_filepath(monkeypatch, validation_manager, load_pipeline):
  303. pipeline, response = load_pipeline("generic_basic_filepath_check.pipeline")
  304. monkeypatch.setattr(validation_manager, "_validate_label", lambda node_id, node_label, response: True)
  305. pipeline_definition = PipelineDefinition(pipeline_definition=pipeline)
  306. await validation_manager._validate_node_properties(
  307. pipeline_definition=pipeline_definition, response=response, pipeline_type="GENERIC", pipeline_runtime="kfp"
  308. )
  309. assert not response.has_fatal
  310. assert not response.to_json().get("issues")
  311. def test_invalid_node_property_resource_value(validation_manager, load_pipeline):
  312. pipeline, response = load_pipeline("generic_invalid_node_property_hardware_resources.pipeline")
  313. node_id = "88ab83dc-d5f0-443a-8837-788ed16851b7"
  314. node = pipeline["pipelines"][0]["nodes"][0]
  315. validation_manager._validate_resource_value(
  316. node["id"],
  317. node["app_data"]["label"],
  318. resource_name="memory",
  319. resource_value=node["app_data"]["component_parameters"]["memory"],
  320. response=response,
  321. )
  322. issues = response.to_json().get("issues")
  323. assert len(issues) == 1
  324. assert issues[0]["severity"] == 1
  325. assert issues[0]["type"] == "invalidNodeProperty"
  326. assert issues[0]["data"]["propertyName"] == "memory"
  327. assert issues[0]["data"]["nodeID"] == node_id
  328. def test_invalid_node_property_env_var(validation_manager):
  329. response = ValidationResponse()
  330. node = {"id": "test-id", "app_data": {"label": "test"}}
  331. invalid_env_var = 'TEST_ENV_ONE"test_one"'
  332. validation_manager._validate_environmental_variables(
  333. node_id=node["id"], node_label=node["app_data"]["label"], env_var=invalid_env_var, response=response
  334. )
  335. issues = response.to_json().get("issues")
  336. assert issues[0]["severity"] == 1
  337. assert issues[0]["type"] == "invalidEnvPair"
  338. assert issues[0]["data"]["propertyName"] == "env_vars"
  339. assert issues[0]["data"]["nodeID"] == "test-id"
  340. def test_invalid_node_property_volumes(validation_manager):
  341. response = ValidationResponse()
  342. node = {"id": "test-id", "app_data": {"label": "test"}}
  343. volumes = [
  344. VolumeMount("/mount/test", "rwx-test-claim"), # valid
  345. VolumeMount("/mount/test_two", "second-claim"), # valid
  346. VolumeMount("/mount/test_four", "second#claim"), # invalid pvc name
  347. ]
  348. validation_manager._validate_mounted_volumes(
  349. node_id=node["id"], node_label=node["app_data"]["label"], volumes=volumes, response=response
  350. )
  351. issues = response.to_json().get("issues")
  352. assert issues[0]["severity"] == 1
  353. assert issues[0]["type"] == "invalidVolumeMount"
  354. assert issues[0]["data"]["propertyName"] == MOUNTED_VOLUMES
  355. assert issues[0]["data"]["nodeID"] == "test-id"
  356. assert "not a valid Kubernetes resource name" in issues[0]["message"]
  357. def test_valid_node_property_kubernetes_toleration(validation_manager):
  358. """
  359. Validate that valid kubernetes toleration definitions are not flagged as invalid.
  360. Constraints are documented in
  361. https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.23/#toleration-v1-core
  362. """
  363. response = ValidationResponse()
  364. node = {"id": "test-id", "app_data": {"label": "test"}}
  365. # The following tolerations are valid
  366. tolerations = [
  367. # parameters are key, operator, value, effect
  368. KubernetesToleration("", "Exists", "", "NoExecute"),
  369. KubernetesToleration("key0", "Exists", "", ""),
  370. KubernetesToleration("key1", "Exists", "", "NoSchedule"),
  371. KubernetesToleration("key2", "Equal", "value2", "NoExecute"),
  372. KubernetesToleration("key3", "Equal", "value3", "PreferNoSchedule"),
  373. ]
  374. validation_manager._validate_kubernetes_tolerations(
  375. node_id=node["id"], node_label=node["app_data"]["label"], tolerations=tolerations, response=response
  376. )
  377. issues = response.to_json().get("issues")
  378. assert len(issues) == 0, response.to_json()
  379. def test_valid_node_property_kubernetes_pod_annotation(validation_manager):
  380. """
  381. Validate that valid kubernetes pod annotation definitions are not flagged as invalid.
  382. Constraints are documented in
  383. https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/#syntax-and-character-set
  384. """
  385. response = ValidationResponse()
  386. node = {"id": "test-id", "app_data": {"label": "test"}}
  387. # The following annotations are valid
  388. annotations = [
  389. # parameters are key and value
  390. KubernetesAnnotation("k", ""),
  391. KubernetesAnnotation("key", "value"),
  392. KubernetesAnnotation("n-a-m-e", "value"),
  393. KubernetesAnnotation("n.a.m.e", "value"),
  394. KubernetesAnnotation("n_a_m_e", "value"),
  395. KubernetesAnnotation("n-a.m_e", "value"),
  396. KubernetesAnnotation("prefix/name", "value"),
  397. KubernetesAnnotation("abc.def/name", "value"),
  398. KubernetesAnnotation("abc.def.ghi/n-a-m-e", "value"),
  399. KubernetesAnnotation("abc.def.ghi.jkl/n.a.m.e", "value"),
  400. KubernetesAnnotation("abc.def.ghi.jkl.mno/n_a_m_e", "value"),
  401. KubernetesAnnotation("abc.def.ghijklmno.pqr/n-a.m_e", "value"),
  402. ]
  403. validation_manager._validate_kubernetes_pod_annotations(
  404. node_id=node["id"], node_label=node["app_data"]["label"], annotations=annotations, response=response
  405. )
  406. issues = response.to_json().get("issues")
  407. assert len(issues) == 0, response.to_json()
  408. def test_invalid_node_property_kubernetes_toleration(validation_manager):
  409. """
  410. Validate that invalid kubernetes toleration definitions are properly detected.
  411. Constraints are documented in
  412. https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.23/#toleration-v1-core
  413. """
  414. response = ValidationResponse()
  415. node = {"id": "test-id", "app_data": {"label": "test"}}
  416. # The following tolerations are invalid
  417. invalid_tolerations = [
  418. # parameters are key, operator, value, effect
  419. KubernetesToleration("", "", "", ""), # cannot be all empty
  420. # invalid values for 'operator'
  421. KubernetesToleration("", "Equal", "value", ""), # empty key requires 'Exists'
  422. KubernetesToleration("key0", "exists", "", ""), # wrong case
  423. KubernetesToleration("key1", "Exist", "", ""), # wrong keyword
  424. KubernetesToleration("key2", "", "", ""), # wrong keyword (technically valid but enforced)
  425. # invalid values for 'value'
  426. KubernetesToleration("key3", "Exists", "value3", ""), # 'Exists' -> no value
  427. # invalid values for 'effect'
  428. KubernetesToleration("key4", "Exists", "", "noschedule"), # wrong case
  429. KubernetesToleration("key5", "Exists", "", "no-such-effect"), # wrong keyword
  430. ]
  431. expected_error_messages = [
  432. "'' is not a valid operator. The value must be one of 'Exists' or 'Equal'.",
  433. "'Equal' is not a valid operator. Operator must be 'Exists' if no key is specified.",
  434. "'exists' is not a valid operator. The value must be one of 'Exists' or 'Equal'.",
  435. "'Exist' is not a valid operator. The value must be one of 'Exists' or 'Equal'.",
  436. "'' is not a valid operator. The value must be one of 'Exists' or 'Equal'.",
  437. "'value3' is not a valid value. It should be empty if operator is 'Exists'.",
  438. "'noschedule' is not a valid effect. Effect must be one of 'NoExecute', 'NoSchedule', or 'PreferNoSchedule'.",
  439. "'no-such-effect' is not a valid effect. Effect must be one of 'NoExecute', "
  440. "'NoSchedule', or 'PreferNoSchedule'.",
  441. ]
  442. # verify that the number of tolerations in this test matches the number of error messages
  443. assert len(invalid_tolerations) == len(expected_error_messages), "Test setup error. "
  444. validation_manager._validate_kubernetes_tolerations(
  445. node_id=node["id"], node_label=node["app_data"]["label"], tolerations=invalid_tolerations, response=response
  446. )
  447. issues = response.to_json().get("issues")
  448. assert len(issues) == len(invalid_tolerations), response.to_json()
  449. index = 0
  450. for issue in issues:
  451. assert issue["type"] == "invalidKubernetesToleration"
  452. assert issue["data"]["propertyName"] == KUBERNETES_TOLERATIONS
  453. assert issue["data"]["nodeID"] == "test-id"
  454. assert issue["message"] == expected_error_messages[index], f"Index is {index}"
  455. index = index + 1
  456. def test_invalid_node_property_kubernetes_pod_annotation(validation_manager):
  457. """
  458. Validate that valid kubernetes pod annotation definitions are not flagged as invalid.
  459. Constraints are documented in
  460. https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/#syntax-and-character-set
  461. """
  462. response = ValidationResponse()
  463. node = {"id": "test-id", "app_data": {"label": "test"}}
  464. TOO_SHORT_LENGTH = 0
  465. MAX_PREFIX_LENGTH = 253
  466. MAX_NAME_LENGTH = 63
  467. TOO_LONG_LENGTH = MAX_PREFIX_LENGTH + 1 + MAX_NAME_LENGTH + 1 # prefix + '/' + name
  468. # The following annotations are invalid
  469. invalid_annotations = [
  470. # parameters are key and value
  471. # test length violations (key name and prefix)
  472. KubernetesAnnotation("a" * (TOO_SHORT_LENGTH), ""), # empty key (min 1)
  473. KubernetesAnnotation("a" * (TOO_LONG_LENGTH), ""), # key too long
  474. KubernetesAnnotation(f"{'a' * (MAX_PREFIX_LENGTH + 1)}/b", ""), # key prefix too long
  475. KubernetesAnnotation(f"{'a' * (MAX_NAME_LENGTH + 1)}", ""), # key name too long
  476. KubernetesAnnotation(f"prefix/{'a' * (MAX_NAME_LENGTH + 1)}", ""), # key name too long
  477. KubernetesAnnotation(f"{'a' * (MAX_PREFIX_LENGTH + 1)}/name", ""), # key prefix too long
  478. # test character violations (key name)
  479. KubernetesAnnotation("-", ""), # name must start and end with alphanum
  480. KubernetesAnnotation("-a", ""), # name must start with alphanum
  481. KubernetesAnnotation("a-", ""), # name must start with alphanum
  482. KubernetesAnnotation("prefix/-b", ""), # name start with alphanum
  483. KubernetesAnnotation("prefix/b-", ""), # name must end with alphanum
  484. # test character violations (key prefix)
  485. KubernetesAnnotation("PREFIX/name", ""), # prefix must be lowercase
  486. KubernetesAnnotation("pref!x/name", ""), # prefix must contain alnum, '-' or '.'
  487. KubernetesAnnotation("pre.fx./name", ""), # prefix must contain alnum, '-' or '.'
  488. KubernetesAnnotation("-pre.fx.com/name", ""), # prefix must contain alnum, '-' or '.'
  489. KubernetesAnnotation("pre.fx-./name", ""), # prefix must contain alnum, '-' or '.'
  490. KubernetesAnnotation("a/b/c", ""), # only one separator char
  491. ]
  492. expected_error_messages = [
  493. "'' is not a valid Kubernetes annotation key.",
  494. f"'{'a' * (TOO_LONG_LENGTH)}' is not a valid Kubernetes annotation key.",
  495. f"'{'a' * (MAX_PREFIX_LENGTH + 1)}/b' is not a valid Kubernetes annotation key.",
  496. f"'{'a' * (MAX_NAME_LENGTH + 1)}' is not a valid Kubernetes annotation key.",
  497. f"'prefix/{'a' * (MAX_NAME_LENGTH + 1)}' is not a valid Kubernetes annotation key.",
  498. f"'{'a' * (MAX_PREFIX_LENGTH + 1)}/name' is not a valid Kubernetes annotation key.",
  499. "'-' is not a valid Kubernetes annotation key.",
  500. "'-a' is not a valid Kubernetes annotation key.",
  501. "'a-' is not a valid Kubernetes annotation key.",
  502. "'prefix/-b' is not a valid Kubernetes annotation key.",
  503. "'prefix/b-' is not a valid Kubernetes annotation key.",
  504. "'PREFIX/name' is not a valid Kubernetes annotation key.",
  505. "'pref!x/name' is not a valid Kubernetes annotation key.",
  506. "'pre.fx./name' is not a valid Kubernetes annotation key.",
  507. "'-pre.fx.com/name' is not a valid Kubernetes annotation key.",
  508. "'pre.fx-./name' is not a valid Kubernetes annotation key.",
  509. "'a/b/c' is not a valid Kubernetes annotation key.",
  510. ]
  511. # verify that the number of annotations in this test matches the number of error messages
  512. assert len(invalid_annotations) == len(expected_error_messages), "Test implementation error. "
  513. validation_manager._validate_kubernetes_pod_annotations(
  514. node_id=node["id"], node_label=node["app_data"]["label"], annotations=invalid_annotations, response=response
  515. )
  516. issues = response.to_json().get("issues")
  517. assert len(issues) == len(
  518. invalid_annotations
  519. ), f"validation returned unexpected results: {response.to_json()['issues']}"
  520. index = 0
  521. for issue in issues:
  522. assert issue["type"] == "invalidKubernetesAnnotation"
  523. assert issue["data"]["propertyName"] == KUBERNETES_POD_ANNOTATIONS
  524. assert issue["data"]["nodeID"] == "test-id"
  525. assert issue["message"] == expected_error_messages[index], f"Index is {index}"
  526. index = index + 1
  527. def test_invalid_node_property_secrets(validation_manager):
  528. response = ValidationResponse()
  529. node = {"id": "test-id", "app_data": {"label": "test"}}
  530. secrets = [
  531. KubernetesSecret("ENV_VAR1", "test-secret", "test-key1"), # valid
  532. KubernetesSecret("ENV_VAR2", "test-secret", "test-key2"), # valid
  533. KubernetesSecret("ENV_VAR3", "test-secret", ""), # invalid: improper format of secret name/key
  534. KubernetesSecret("ENV_VAR5", "test%secret", "test-key"), # invalid: not a valid Kubernetes resource name
  535. KubernetesSecret("ENV_VAR6", "test-secret", "test$key2"), # invalid: not a valid Kubernetes secret key
  536. ]
  537. validation_manager._validate_kubernetes_secrets(
  538. node_id=node["id"], node_label=node["app_data"]["label"], secrets=secrets, response=response
  539. )
  540. issues = response.to_json().get("issues")
  541. assert issues[0]["severity"] == 1
  542. assert issues[0]["type"] == "invalidKubernetesSecret"
  543. assert issues[0]["data"]["propertyName"] == KUBERNETES_SECRETS
  544. assert issues[0]["data"]["nodeID"] == "test-id"
  545. assert "improperly formatted representation of secret name and key" in issues[0]["message"]
  546. assert "not a valid Kubernetes resource name" in issues[1]["message"]
  547. assert "not a valid Kubernetes secret key" in issues[2]["message"]
  548. def test_valid_node_property_label(validation_manager):
  549. response = ValidationResponse()
  550. node = {"id": "test-id"}
  551. valid_label_name = "dead-bread-dead-bread-dead-bread-dead-bread-dead-bread-dead-bre"
  552. validation_manager._validate_label(node_id=node["id"], node_label=valid_label_name, response=response)
  553. issues = response.to_json().get("issues")
  554. assert len(issues) == 0
  555. def test_valid_node_property_label_min_length(validation_manager):
  556. response = ValidationResponse()
  557. node = {"id": "test-id", "app_data": {"label": "test"}}
  558. valid_label_name = "d"
  559. validation_manager._validate_label(node_id=node["id"], node_label=valid_label_name, response=response)
  560. issues = response.to_json().get("issues")
  561. assert len(issues) == 0
  562. def test_invalid_node_property_label_filename_exceeds_max_length(validation_manager):
  563. response = ValidationResponse()
  564. node = {"id": "test-id", "app_data": {"label": "test"}}
  565. valid_label_name = "deadbread-deadbread-deadbread-deadbread-deadbread-deadbread-de.py"
  566. validation_manager._validate_label(node_id=node["id"], node_label=valid_label_name, response=response)
  567. issues = response.to_json().get("issues")
  568. assert len(issues) == 2
  569. def test_invalid_node_property_label_max_length(validation_manager):
  570. response = ValidationResponse()
  571. node = {"id": "test-id", "app_data": {"label": "test"}}
  572. invalid_label_name = "dead-bread-dead-bread-dead-bread-dead-bread-dead-bread-dead-bred"
  573. validation_manager._validate_label(node_id=node["id"], node_label=invalid_label_name, response=response)
  574. issues = response.to_json().get("issues")
  575. assert len(issues) == 1
  576. assert issues[0]["severity"] == 2
  577. assert issues[0]["type"] == "invalidNodeLabel"
  578. assert issues[0]["data"]["propertyName"] == "label"
  579. assert issues[0]["data"]["nodeID"] == "test-id"
  580. def test_valid_node_property_label_filename_has_relative_path(validation_manager):
  581. response = ValidationResponse()
  582. node = {"id": "test-id", "app_data": {"label": "test"}}
  583. valid_label_name = "deadbread.py"
  584. validation_manager._validate_label(node_id=node["id"], node_label=valid_label_name, response=response)
  585. issues = response.to_json().get("issues")
  586. assert len(issues) == 0
  587. def test_invalid_node_property_label_bad_characters(validation_manager):
  588. response = ValidationResponse()
  589. node = {"id": "test-id"}
  590. invalid_label_name = "bad_label_*&^&$"
  591. validation_manager._validate_label(node_id=node["id"], node_label=invalid_label_name, response=response)
  592. issues = response.to_json().get("issues")
  593. assert len(issues) == 1
  594. assert issues[0]["severity"] == 2
  595. assert issues[0]["type"] == "invalidNodeLabel"
  596. assert issues[0]["data"]["propertyName"] == "label"
  597. assert issues[0]["data"]["nodeID"] == "test-id"
  598. def test_pipeline_graph_single_cycle(validation_manager, load_pipeline):
  599. pipeline, response = load_pipeline("generic_single_cycle.pipeline")
  600. # cycle_ID = ['c309f6dd-b022-4b1c-b2b0-b6449bb26e8f', '8cb986cb-4fc9-4b1d-864d-0ec64b7ac13c']
  601. validation_manager._validate_pipeline_graph(pipeline=pipeline, response=response)
  602. issues = response.to_json().get("issues")
  603. assert len(issues) == 1
  604. assert issues[0]["severity"] == 1
  605. assert issues[0]["type"] == "circularReference"
  606. # assert issues[0]['data']['linkIDList'].sort() == cycle_ID.sort()
  607. def test_pipeline_graph_double_cycle(validation_manager, load_pipeline):
  608. pipeline, response = load_pipeline("generic_double_cycle.pipeline")
  609. # cycle_ID = ['597b2971-b95d-4df7-a36d-9d93b0345298', 'b63378e4-9085-4a33-9330-6f86054681f4']
  610. # cycle_two_ID = ['c309f6dd-b022-4b1c-b2b0-b6449bb26e8f', '8cb986cb-4fc9-4b1d-864d-0ec64b7ac13c']
  611. validation_manager._validate_pipeline_graph(pipeline=pipeline, response=response)
  612. issues = response.to_json().get("issues")
  613. assert len(issues) == 1
  614. assert issues[0]["severity"] == 1
  615. assert issues[0]["type"] == "circularReference"
  616. # assert issues[0]['data']['linkIDList'].sort() == cycle_ID.sort()
  617. # assert issues[1]['severity'] == 1
  618. # assert issues[1]['type'] == 'circularReference'
  619. # assert issues[1]['data']['linkIDList'].sort() == cycle_two_ID.sort()
  620. def test_pipeline_graph_singleton(validation_manager, load_pipeline):
  621. pipeline, response = load_pipeline("generic_singleton.pipeline")
  622. node_id = "0195fefd-3ceb-4a90-a12c-3958ef0ff42e"
  623. validation_manager._validate_pipeline_graph(pipeline=pipeline, response=response)
  624. issues = response.to_json().get("issues")
  625. assert len(issues) == 1
  626. assert not response.has_fatal
  627. assert issues[0]["severity"] == 2
  628. assert issues[0]["type"] == "singletonReference"
  629. assert issues[0]["data"]["nodeID"] == node_id
  630. def test_pipeline_valid_kfp_with_supernode(validation_manager, load_pipeline):
  631. pipeline, response = load_pipeline("kf_supernode_valid.pipeline")
  632. validation_manager._validate_pipeline_graph(pipeline=pipeline, response=response)
  633. issues = response.to_json().get("issues")
  634. assert len(issues) == 0
  635. assert not response.has_fatal
  636. def test_pipeline_invalid_single_cycle_kfp_with_supernode(validation_manager, load_pipeline):
  637. pipeline, response = load_pipeline("kf_supernode_invalid_single_cycle.pipeline")
  638. validation_manager._validate_pipeline_graph(pipeline=pipeline, response=response)
  639. issues = response.to_json().get("issues")
  640. assert len(issues) == 1
  641. assert response.has_fatal
  642. assert issues[0]["severity"] == 1
  643. assert issues[0]["type"] == "circularReference"
  644. @pytest.mark.parametrize("catalog_instance", [KFP_COMPONENT_CACHE_INSTANCE], indirect=True)
  645. async def test_pipeline_kfp_inputpath_parameter(validation_manager, load_pipeline, catalog_instance, component_cache):
  646. pipeline, response = load_pipeline("kf_inputpath_parameter.pipeline")
  647. pipeline_definition = PipelineDefinition(pipeline_definition=pipeline)
  648. await validation_manager._validate_node_properties(
  649. pipeline_definition=pipeline_definition,
  650. response=response,
  651. pipeline_type="KUBEFLOW_PIPELINES",
  652. pipeline_runtime="kfp",
  653. )
  654. issues = response.to_json().get("issues")
  655. assert len(issues) == 0
  656. @pytest.mark.parametrize("catalog_instance", [KFP_COMPONENT_CACHE_INSTANCE], indirect=True)
  657. async def test_pipeline_invalid_kfp_inputpath_parameter(
  658. validation_manager, load_pipeline, catalog_instance, component_cache
  659. ):
  660. invalid_key_node_id = "089a12df-fe2f-4fcb-ae37-a1f8a6259ca1"
  661. missing_param_node_id = "e8820c55-dc79-46d1-b32e-924fa5d70d2a"
  662. pipeline, response = load_pipeline("kf_invalid_inputpath_parameter.pipeline")
  663. pipeline_definition = PipelineDefinition(pipeline_definition=pipeline)
  664. await validation_manager._validate_node_properties(
  665. pipeline_definition=pipeline_definition,
  666. response=response,
  667. pipeline_type="KUBEFLOW_PIPELINES",
  668. pipeline_runtime="kfp",
  669. )
  670. issues = response.to_json().get("issues")
  671. assert len(issues) == 2
  672. assert response.has_fatal
  673. assert issues[0]["severity"] == 1
  674. assert issues[0]["type"] == "invalidNodeProperty"
  675. assert issues[0]["data"]["nodeID"] == invalid_key_node_id
  676. assert issues[1]["severity"] == 1
  677. assert issues[1]["type"] == "invalidNodeProperty"
  678. assert issues[1]["data"]["nodeID"] == missing_param_node_id
  679. @pytest.mark.parametrize("catalog_instance", [KFP_COMPONENT_CACHE_INSTANCE], indirect=True)
  680. async def test_pipeline_invalid_kfp_inputpath_missing_connection(
  681. validation_manager, load_pipeline, catalog_instance, component_cache
  682. ):
  683. invalid_node_id = "5b78ea0a-e5fc-4022-94d4-7b9dc170d794"
  684. pipeline, response = load_pipeline("kf_invalid_inputpath_missing_connection.pipeline")
  685. pipeline_definition = PipelineDefinition(pipeline_definition=pipeline)
  686. await validation_manager._validate_node_properties(
  687. pipeline_definition=pipeline_definition,
  688. response=response,
  689. pipeline_type="KUBEFLOW_PIPELINES",
  690. pipeline_runtime="kfp",
  691. )
  692. issues = response.to_json().get("issues")
  693. assert len(issues) == 1
  694. assert response.has_fatal
  695. assert issues[0]["severity"] == 1
  696. assert issues[0]["type"] == "invalidNodeProperty"
  697. assert issues[0]["data"]["nodeID"] == invalid_node_id
  698. @pytest.mark.parametrize("catalog_instance", [AIRFLOW_TEST_OPERATOR_CATALOG], indirect=True)
  699. async def test_pipeline_aa_parent_node_missing_xcom_push(
  700. validation_manager, load_pipeline, catalog_instance, component_cache
  701. ):
  702. invalid_node_id = "b863d458-21b5-4a46-8420-5a814b7bd525"
  703. invalid_operator = "TestOperator"
  704. pipeline, response = load_pipeline("aa_parent_node_missing_xcom.pipeline")
  705. pipeline_definition = PipelineDefinition(pipeline_definition=pipeline)
  706. await validation_manager._validate_node_properties(
  707. pipeline_definition=pipeline_definition,
  708. response=response,
  709. pipeline_type="APACHE_AIRFLOW",
  710. pipeline_runtime="airflow",
  711. )
  712. issues = response.to_json().get("issues")
  713. assert len(issues) == 1
  714. assert response.has_fatal
  715. assert issues[0]["severity"] == 1
  716. assert issues[0]["type"] == "invalidNodeProperty"
  717. assert issues[0]["data"]["nodeID"] == invalid_node_id
  718. assert issues[0]["data"]["parentNodeID"] == invalid_operator