예제 #1
0
def _validate_string_from_list(key, value, validation_rule):
    if not isinstance(value, str):
        return ValidationResult(False, f'"{key}" input must be a string', None)
    if value not in validation_rule.options:
        formatted_options = ", ".join([f'"{option}"' for option in validation_rule.options])
        return ValidationResult(False, f'"{key}" input must be one of: {formatted_options}', None)
    return ValidationResult(True, None, value)
예제 #2
0
def _validate_byte_quantity(key, value, validation_rule):
    wrong_format_result = ValidationResult(
        False,
        f'"{key}" input must be a string integer value ending in "m" (e.g. "512m" for 512 megabytes)',
        None
    )
    if not isinstance(value, str):
        return wrong_format_result
    if is_str_empty(value):
        return wrong_format_result
    if value[-1] != "m":
        return wrong_format_result
    try:
        numerical_value = int(value[:-1])
    except ValueError:
        return wrong_format_result
    except TypeError:
        return wrong_format_result
    return ValidationResult(True, None, value) \
        if validation_rule.minimum <= numerical_value <= validation_rule.maximum \
        else ValidationResult(
            False,
            f'"{key}" input must be in range [{validation_rule.minimum}m, {validation_rule.maximum}m]',
            None
        )
예제 #3
0
def _validate_label(value):
    validation_result = _validate_non_empty_string("label", value)
    if not validation_result.is_valid:
        return validation_result
    if len(value) > 63:
        # Kubernetes spark operator restriction
        return ValidationResult(False, '"label" input has a maximum length of 63 characters', None)
    match = re.match('^[0-9A-Za-z]([0-9A-Za-z\\-]*)?[0-9A-Za-z]$|^[0-9A-Za-z]$', value)
    if match is None:
        return ValidationResult(False, f'"label" input must obey naming convention: '
                                       f'see https://github.com/ukaea/piezo/wiki/WebAppUserGuide#submit-a-job', None)
    return ValidationResult(True, None, value)
 def test_submit_job_returns_ui_url_as_unavailable_if_failure_in_setup(
         self):
     # Arrange
     body = {'name': 'test-spark-job', 'language': 'example-language'}
     self.mock_validation_service.validate_request_keys.return_value = ValidationResult(
         True, "", None)
     self.mock_validation_service.validate_request_values.return_value = ValidationResult(
         True, "", body)
     self.mock_spark_job_customiser.rename_job.return_value = 'test-spark-job-abcd1234'
     self.mock_spark_ui_service.expose_spark_ui.return_value = "Unavailable"
     # Act
     response = self.test_service.submit_job(body)
     # Assert)
     assert response['spark_ui'] == 'Unavailable'
예제 #5
0
def _validate_integer(key, value, validation_rule):
    try:
        numerical_value = int(value)
    except ValueError:
        return ValidationResult(False, f'"{key}" input must be an integer', None)
    except TypeError:
        return ValidationResult(False, f'"{key}" input must be an integer', None)

    return ValidationResult(True, None, numerical_value) \
        if validation_rule.minimum <= numerical_value <= validation_rule.maximum \
        else ValidationResult(
            False,
            f'"{key}" input must be in range [{validation_rule.minimum}, {validation_rule.maximum}]',
            None
        )
 def test_submit_job_calls_spark_ui_service_correctly_to_expose_ui(self):
     # Arrange
     body = {'name': 'test-spark-job', 'language': 'example-language'}
     self.mock_validation_service.validate_request_keys.return_value = ValidationResult(
         True, "", None)
     self.mock_validation_service.validate_request_values.return_value = ValidationResult(
         True, "", body)
     self.mock_spark_job_customiser.rename_job.return_value = 'test-spark-job-abcd1234'
     self.mock_spark_ui_service.expose_spark_ui.return_value = "some.url"
     # Act
     response = self.test_service.submit_job(body)
     # Assert
     self.mock_spark_ui_service.expose_spark_ui.assert_called_once_with(
         'test-spark-job-abcd1234')
     assert response['spark_ui'] == 'some.url'
 def test_submit_job_returns_invalid_body_values(self):
     # Arrange
     body = {'name': 'test-spark-job', 'language': 'example-language'}
     self.mock_validation_service.validate_request_keys.return_value = ValidationResult(
         True, "", None)
     self.mock_validation_service.validate_request_values.return_value = ValidationResult(
         False, "Msg", None)
     # Act
     result = self.test_service.submit_job(body)
     # Assert
     self.mock_kubernetes_adapter.create_namespaced_custom_object.assert_not_called(
     )
     self.assertDictEqual(result, {
         'status': StatusCodes.Bad_request.value,
         'message': 'Msg'
     })
예제 #8
0
    def validate_request_keys(self, request_body):
        is_valid = True
        error_msg = "The following errors were found:\n"
        # Get keys required/supported
        required_keys = self._validation_ruleset.get_keys_of_required_inputs()
        if 'language' in request_body:
            language = request_body['language']
            rule = self._validation_ruleset.get_validation_rule_for_key('language')
            validation_result = validate('language', language, rule)
            if validation_result.is_valid:
                required_keys += self._validation_ruleset.get_keys_for_language(request_body['language'])
            else:
                is_valid = False
                error_msg += f'Unsupported language "{language}" provided\n'
        supported_keys = required_keys + self._validation_ruleset.get_keys_of_optional_inputs()

        # Find any discrepancies
        missing_keys = get_set_difference(required_keys, request_body)
        unsupported_keys = get_set_difference(request_body, supported_keys)

        # Group the results together
        for key in missing_keys:
            is_valid = False
            error_msg += f'Missing required input "{key}"\n'
        for key in unsupported_keys:
            is_valid = False
            error_msg += f'Unsupported input "{key}" provided\n'
        result = ValidationResult(
            is_valid,
            "All input keys provided are valid\n" if is_valid else error_msg,
            None
        )

        return result
예제 #9
0
 def test_set_output_dir_as_first_argument_prepends_dir_to_other_arguments(
         self):
     # Arrange
     mock_storage_service = mock.create_autospec(IStorageService)
     mock_storage_service.protocol_route_to_bucket.return_value = 's3a://test-bucket'
     validated_body_values = ValidationResult(True, None, {
         'other': 'True',
         'another': 12,
         'arguments': ['1st', '2nd']
     })
     # Act
     result = self.test_customiser.set_output_dir_as_first_argument(
         'example-job', mock_storage_service, validated_body_values)
     # Assert
     self.mock_logger.debug.assert_called_once_with(
         'Setting first argument for job "example-job" to be "s3a://test-bucket/outputs/example-job/"'
     )
     self.assertDictEqual(
         result.validated_value, {
             'other':
             'True',
             'another':
             12,
             'arguments':
             ['s3a://test-bucket/outputs/example-job/', '1st', '2nd']
         })
예제 #10
0
def _validate_multiple_of_a_tenth(key, value, validation_rule):
    not_a_tenth_result = ValidationResult(False, f'"{key}" input must be a multiple of 0.1', None)
    try:
        numerical_value = float(value)
    except ValueError:
        return not_a_tenth_result
    except TypeError:
        return not_a_tenth_result

    multiples_of_a_tenth = 10 * numerical_value
    if multiples_of_a_tenth % 1 != 0:
        return not_a_tenth_result

    return ValidationResult(True, None, numerical_value) \
        if validation_rule.minimum <= numerical_value <= validation_rule.maximum \
        else ValidationResult(
            False,
            f'"{key}" input must be in range [{validation_rule.minimum}, {validation_rule.maximum}]',
            None
        )
 def test_submit_job_logs_and_returns_api_exception_reason(self):
     # Arrange
     body = {'name': 'test-spark-job', 'language': 'example-language'}
     self.mock_validation_service.validate_request_keys.return_value = ValidationResult(
         True, "", None)
     self.mock_validation_service.validate_request_values.return_value = ValidationResult(
         True, "", body)
     self.mock_spark_job_customiser.rename_job.return_value = 'test-spark-job-abcd1234'
     manifest = {
         'metadata': {
             'namespace': NAMESPACE,
             'name': 'test-spark-job-abcd1234',
             'language': 'example-language'
         }
     }
     self.mock_manifest_populator.build_manifest.return_value = manifest
     self.mock_kubernetes_adapter.create_namespaced_custom_object.side_effect = \
         ApiException(reason="Reason", status=999)
     # Act
     result = self.test_service.submit_job(body)
     # Assert
     self.mock_kubernetes_adapter.create_namespaced_custom_object.assert_called_once_with(
         CRD_GROUP, CRD_VERSION, NAMESPACE, CRD_PLURAL, manifest)
     expected_message = 'Kubernetes error when trying to submit job: Reason'
     self.mock_logger.error.assert_has_calls([
         mock.call(expected_message),
         mock.call({
             'metadata': {
                 'namespace': NAMESPACE,
                 'name': 'test-spark-job-abcd1234',
                 'language': 'example-language'
             }
         })
     ])
     self.assertDictEqual(result, {
         'status': 999,
         'message': expected_message
     })
 def test_submit_job_sends_expected_arguments(self):
     # Arrange
     body = {'name': 'test-spark-job', 'language': 'example-language'}
     self.mock_validation_service.validate_request_keys.return_value = ValidationResult(
         True, "", None)
     self.mock_validation_service.validate_request_values.return_value = ValidationResult(
         True, "", body)
     self.mock_spark_job_customiser.rename_job.return_value = 'test-spark-job-abcd1234'
     manifest = {
         'metadata': {
             'namespace': NAMESPACE,
             'name': 'test-spark-job-abcd1234',
             'language': 'example-language'
         }
     }
     self.mock_manifest_populator.build_manifest.return_value = manifest
     self.mock_kubernetes_adapter.create_namespaced_custom_object.return_value = {
         'metadata': {
             'namespace': NAMESPACE,
             'name': 'test-spark-job-abcd1234',
             'language': 'example-language'
         }
     }
     self.mock_spark_ui_service.expose_spark_ui.return_value = 'some_url'
     # Act
     result = self.test_service.submit_job(body)
     # Assert
     self.mock_kubernetes_adapter.create_namespaced_custom_object.assert_called_once_with(
         CRD_GROUP, CRD_VERSION, NAMESPACE, CRD_PLURAL, manifest)
     self.assertDictEqual(
         result, {
             'status': StatusCodes.Okay.value,
             'message': 'Job driver created successfully',
             'job_name': 'test-spark-job-abcd1234',
             'spark_ui': 'some_url'
         })
예제 #13
0
def validate(key, value, validation_rule):
    if key == "name":
        return _validate_name(key, value)
    if key in ["path_to_main_app_file", "main_class"]:
        return _validate_non_empty_string(key, value)
    if key in ["label"]:
        return _validate_label(value)
    if key in ["language", "python_version"]:
        return _validate_string_from_list(key, value, validation_rule)
    if key in ["executors", "executor_cores"]:
        return _validate_integer(key, value, validation_rule)
    if key in ["driver_cores", "driver_core_limit"]:
        return _validate_multiple_of_a_tenth(key, value, validation_rule)
    if key in ["driver_memory", "executor_memory"]:
        return _validate_byte_quantity(key, value, validation_rule)
    if key in ["arguments"]:
        return ValidationResult(True, None, value)
    raise ValueError(f"Unexpected argument {key}")
예제 #14
0
    def validate_request_values(self, request_body):
        validated_dict = {}
        error_msg = "The following errors were found:\n"
        is_valid = True

        for key, input_value in request_body.items():
            rule_for_key = self._validation_ruleset.get_validation_rule_for_key(key)
            validation_result = validate(key, input_value, rule_for_key)
            if validation_result.is_valid is True:
                validated_dict[key] = validation_result.validated_value
            else:
                error_msg += validation_result.message + '\n'
                is_valid = False

        result = ValidationResult(
            is_valid,
            "All inputs provided are valid\n" if is_valid else error_msg,
            validated_dict if is_valid else None
        )
        return result
예제 #15
0
def _validate_name(key, value):
    validation_result = _validate_non_empty_string("name", value)
    if not validation_result.is_valid:
        return validation_result

    is_name_valid = True
    if len(value) == 1:
        is_name_valid = value in string.ascii_lowercase
    elif 1 < len(value) <= 29:
        match = re.match("^([a-z])([\\.\\-0-9a-z]*)?([0-9a-z])$", value)
        if match is None:
            is_name_valid = False
        for pattern in ["--", "-.", ".-", ".."]:
            if pattern in value:
                is_name_valid = False
    else:
        is_name_valid = False

    msg = None if is_name_valid else f'"{key}" input must obey naming convention: ' \
        f'see https://github.com/ukaea/piezo/wiki/WebAppUserGuide#submit-a-job'
    return ValidationResult(is_name_valid, msg, value)
예제 #16
0
def _validate_non_empty_string(key, value):
    if not isinstance(value, str):
        return ValidationResult(False, f'"{key}" input must be a string', None)
    if is_str_empty(value):
        return ValidationResult(False, f'"{key}" input cannot be empty', None)
    return ValidationResult(True, None, value)