Skip to content

Commit 084823d

Browse files
COMPUTE-1703_instance_type_selector_validation
(feat) app_builder.py, test_dxpy.py - added validation for the applet build checking mutual exclusivity in the applet spec for selector and instanceType and clusterSpec
1 parent 5353d41 commit 084823d

File tree

2 files changed

+149
-0
lines changed

2 files changed

+149
-0
lines changed

src/python/dxpy/app_builder.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,10 +64,47 @@ class AppBuilderException(Exception):
6464
"""
6565
pass
6666

67+
def _validate_system_requirements(system_requirements):
68+
"""
69+
Validates systemRequirements section of dxapp.json for instanceTypeSelector constraints.
70+
71+
:param system_requirements: The systemRequirements section from runSpec
72+
:type system_requirements: dict
73+
:raises AppBuilderException: If validation fails
74+
"""
75+
if not isinstance(system_requirements, dict):
76+
return
77+
78+
for entry_point, requirements in system_requirements.items():
79+
if not isinstance(requirements, dict):
80+
continue
81+
82+
has_instance_type = 'instanceType' in requirements
83+
has_instance_type_selector = 'instanceTypeSelector' in requirements
84+
has_cluster_spec = 'clusterSpec' in requirements
85+
86+
# Check mutual exclusivity: instanceType and instanceTypeSelector
87+
if has_instance_type and has_instance_type_selector:
88+
raise AppBuilderException(
89+
"instanceType and instanceTypeSelector keywords are mutually exclusive "
90+
"in systemRequirements for entry point '{}'".format(entry_point)
91+
)
92+
93+
# Check mutual exclusivity: instanceTypeSelector and clusterSpec
94+
if has_instance_type_selector and has_cluster_spec:
95+
raise AppBuilderException(
96+
"instanceTypeSelector and clusterSpec keywords are mutually exclusive "
97+
"in systemRequirements for entry point '{}'".format(entry_point)
98+
)
99+
67100
def _validate_applet_spec(applet_spec):
68101
if 'runSpec' not in applet_spec:
69102
raise AppBuilderException("Required field 'runSpec' not found in dxapp.json")
70103

104+
# Validate systemRequirements for instanceTypeSelector constraints
105+
if 'systemRequirements' in applet_spec.get('runSpec', {}):
106+
_validate_system_requirements(applet_spec['runSpec']['systemRequirements'])
107+
71108
def _validate_app_spec(app_spec):
72109
pass
73110

src/python/test/test_dxpy.py

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3191,6 +3191,118 @@ def test_assert_consistent_regions(self):
31913191
with self.assertRaises(app_builder.AppBuilderException):
31923192
assert_consistent_regions({"aws:us-east-1": None}, ["azure:westus"], app_builder.AppBuilderException)
31933193

3194+
def test_validate_system_requirements_instance_type_selector(self):
3195+
"""Test that instanceTypeSelector validation works correctly"""
3196+
validate = app_builder._validate_system_requirements
3197+
3198+
# Valid cases - should not raise exceptions
3199+
3200+
# instanceTypeSelector alone is valid
3201+
validate({
3202+
"main": {
3203+
"instanceTypeSelector": {
3204+
"allowedInstanceTypes": ["mem1_ssd1_x4", "mem1_ssd1_x8"]
3205+
}
3206+
}
3207+
})
3208+
3209+
# instanceType alone is valid
3210+
validate({
3211+
"main": {
3212+
"instanceType": "mem1_ssd1_x4"
3213+
}
3214+
})
3215+
3216+
# clusterSpec alone is valid
3217+
validate({
3218+
"main": {
3219+
"clusterSpec": {
3220+
"type": "generic",
3221+
"numInstances": 3
3222+
}
3223+
}
3224+
})
3225+
3226+
# instanceType and clusterSpec together is valid
3227+
validate({
3228+
"main": {
3229+
"instanceType": "mem1_ssd1_x4",
3230+
"clusterSpec": {
3231+
"type": "generic",
3232+
"numInstances": 3
3233+
}
3234+
}
3235+
})
3236+
3237+
# Empty requirements is valid
3238+
validate({})
3239+
3240+
# None requirements is valid
3241+
validate(None)
3242+
3243+
# Invalid cases - should raise AppBuilderException
3244+
3245+
# instanceType and instanceTypeSelector are mutually exclusive
3246+
with self.assertRaises(app_builder.AppBuilderException) as cm:
3247+
validate({
3248+
"main": {
3249+
"instanceType": "mem1_ssd1_x4",
3250+
"instanceTypeSelector": {
3251+
"allowedInstanceTypes": ["mem1_ssd1_x4", "mem1_ssd1_x8"]
3252+
}
3253+
}
3254+
})
3255+
self.assertIn("mutually exclusive", str(cm.exception))
3256+
self.assertIn("instanceType", str(cm.exception))
3257+
self.assertIn("instanceTypeSelector", str(cm.exception))
3258+
3259+
# instanceTypeSelector and clusterSpec are mutually exclusive
3260+
with self.assertRaises(app_builder.AppBuilderException) as cm:
3261+
validate({
3262+
"main": {
3263+
"instanceTypeSelector": {
3264+
"allowedInstanceTypes": ["mem1_ssd1_x4", "mem1_ssd1_x8"]
3265+
},
3266+
"clusterSpec": {
3267+
"type": "generic",
3268+
"numInstances": 3
3269+
}
3270+
}
3271+
})
3272+
self.assertIn("mutually exclusive", str(cm.exception))
3273+
self.assertIn("instanceTypeSelector", str(cm.exception))
3274+
self.assertIn("clusterSpec", str(cm.exception))
3275+
3276+
# Test with wildcard entry point
3277+
with self.assertRaises(app_builder.AppBuilderException) as cm:
3278+
validate({
3279+
"*": {
3280+
"instanceType": "mem1_ssd1_x4",
3281+
"instanceTypeSelector": {
3282+
"allowedInstanceTypes": ["mem1_ssd1_x4"]
3283+
}
3284+
}
3285+
})
3286+
self.assertIn("mutually exclusive", str(cm.exception))
3287+
3288+
# Test with multiple entry points - one invalid
3289+
with self.assertRaises(app_builder.AppBuilderException) as cm:
3290+
validate({
3291+
"main": {
3292+
"instanceTypeSelector": {
3293+
"allowedInstanceTypes": ["mem1_ssd1_x4"]
3294+
}
3295+
},
3296+
"process": {
3297+
"instanceType": "mem1_ssd1_x4",
3298+
"instanceTypeSelector": {
3299+
"allowedInstanceTypes": ["mem1_ssd1_x8"]
3300+
}
3301+
}
3302+
})
3303+
self.assertIn("mutually exclusive", str(cm.exception))
3304+
self.assertIn("process", str(cm.exception))
3305+
31943306
class TestWorkflowBuilderUtils(testutil.DXTestCaseBuildWorkflows):
31953307
def setUp(self):
31963308
super(TestWorkflowBuilderUtils, self).setUp()

0 commit comments

Comments
 (0)