diff --git a/reana_client/api/client.py b/reana_client/api/client.py old mode 100644 new mode 100755 index ecdfd5e4..e683a27f --- a/reana_client/api/client.py +++ b/reana_client/api/client.py @@ -1280,3 +1280,28 @@ def prune_workspace(workflow, include_inputs, include_outputs, access_token): ) ) raise Exception(e.response.json()["message"]) + +def validate_workflow(reana_yaml): + """Validate a reana yaml file in the server. + + :param reana_yaml: the reana yaml file that will be submitted for validation to the server. + + :return: a response from the server which is in a JSON format TODO: add more + """ + try: + #TODO: remove + #print("\nSending:") + #print(json.loads(json.dumps(reana_yaml, sort_keys=True))) + (response, http_response) = current_rs_api_client.api.validate_workflow(reana_yaml=json.loads(json.dumps(reana_yaml, sort_keys=True))).result() + + return response, http_response + + except HTTPError as e: + logging.debug( + "Workflow could not be validated: " + "\nStatus: {}\nReason: {}\n" + "Message: {}".format( + e.response.status_code, e.response.reason, e.response.json()["message"] + ) + ) + raise Exception(e.response.json()["message"]) diff --git a/reana_client/cli/workflow.py b/reana_client/cli/workflow.py index 12b4eb68..a7fe2b58 100644 --- a/reana_client/cli/workflow.py +++ b/reana_client/cli/workflow.py @@ -342,7 +342,7 @@ def get_sort_key(x, column_id): @add_access_token_options @check_connection @click.pass_context -def workflow_create(ctx, file, name, skip_validation, access_token): # noqa: D301 +def workflow_create(ctx, file, name, skip_validation, access_token, parameters): # noqa: D301 """Create a new workflow. The ``create`` command allows to create a new workflow from reana.yaml @@ -377,6 +377,7 @@ def workflow_create(ctx, file, name, skip_validation, access_token): # noqa: D3 access_token=access_token, skip_validation=skip_validation, server_capabilities=True, + parameters=parameters ) logging.info("Connecting to {0}".format(get_api_url())) response = create_workflow(reana_specification, name, access_token) @@ -1022,7 +1023,7 @@ def workflow_validate( logging.debug(traceback.format_exc()) logging.debug(str(e)) display_message( - "Something went wrong when trying to validate {}".format(file), + "An error occured when trying to validate {}".format(file) + "\n" + str(e), msg_type="error", ) sys.exit(1) @@ -1155,6 +1156,7 @@ def workflow_run( name=name, skip_validation=skip_validation, access_token=access_token, + parameters=parameters, ) display_message("Uploading files...", msg_type="info") ctx.invoke( diff --git a/reana_client/utils.py b/reana_client/utils.py index 16e2ac14..e9bf3fc1 100644 --- a/reana_client/utils.py +++ b/reana_client/utils.py @@ -46,6 +46,7 @@ def load_validate_reana_spec( skip_validate_environments=True, pull_environment_image=False, server_capabilities=False, + parameters=False, ): """Load and validate reana specification file. @@ -64,6 +65,7 @@ def load_validate_reana_spec( skip_validate_environments=skip_validate_environments, pull_environment_image=pull_environment_image, server_capabilities=server_capabilities, + parameters=parameters, ) if reana_yaml["workflow"]["type"] == "yadage": diff --git a/reana_client/validation/utils.py b/reana_client/validation/utils.py old mode 100644 new mode 100755 index 5f02323c..befcdf45 --- a/reana_client/validation/utils.py +++ b/reana_client/validation/utils.py @@ -12,6 +12,7 @@ from typing import Dict, NoReturn, Union import click +import json from reana_commons.errors import REANAValidationError from reana_commons.validation.operational_options import validate_operational_options @@ -24,6 +25,39 @@ from reana_client.validation.parameters import validate_parameters from reana_client.validation.workspace import _validate_workspace +def display_reana_params_warnings(validator) -> None: + """Display REANA specification parameter validation warnings.""" + _display_messages_type( + info_msg="Verifying REANA specification parameters... ", + success_msg="REANA specification parameters appear valid.", + messages=validator["reana_params_warnings"], + ) + + +def display_workflow_params_warnings(validator) -> None: + """Display REANA workflow parameter and command validation warnings.""" + _display_messages_type( + info_msg="Verifying workflow parameters and commands... ", + success_msg="Workflow parameters and commands appear valid.", + messages=validator["workflow_params_warnings"], + ) + + +def display_operations_warnings(validator) -> None: + """Display dangerous workflow operation warnings.""" + _display_messages_type( + info_msg="Verifying dangerous workflow operations... ", + success_msg="Workflow operations appear valid.", + messages=validator["operations_warnings"], + ) + + +def _display_messages_type(info_msg, success_msg, messages) -> None: + display_message(info_msg, msg_type="info") + for msg in messages: + display_message(msg["message"], msg_type=msg["type"], indented=True) + if not messages: + display_message(success_msg, msg_type="success", indented=True) def validate_reana_spec( reana_yaml, @@ -33,7 +67,48 @@ def validate_reana_spec( skip_validate_environments=True, pull_environment_image=False, server_capabilities=False, + parameters=False, +): + + local_validation( + reana_yaml, + filepath, + access_token=access_token, + skip_validation=skip_validation, + skip_validate_environments=skip_validate_environments, + pull_environment_image=pull_environment_image, + server_capabilities=server_capabilities, + parameters=parameters + ) + + server_validation( + reana_yaml, + filepath, + access_token=access_token, + skip_validation=skip_validation, + skip_validate_environments=skip_validate_environments, + pull_environment_image=pull_environment_image, + server_capabilities=server_capabilities, + parameters=parameters + ) + +def local_validation( + reana_yaml, + filepath, + access_token=None, + skip_validation=False, + skip_validate_environments=True, + pull_environment_image=False, + server_capabilities=False, + parameters=False, ): + + print("") + display_message( + f"Results from local validation", + msg_type="info", + ) + """Validate REANA specification file.""" if "options" in reana_yaml.get("inputs", {}): workflow_type = reana_yaml["workflow"]["type"] @@ -108,6 +183,157 @@ def validate_reana_spec( ) validate_environment(reana_yaml, pull=pull_environment_image) +def server_validation( + reana_yaml, + filepath, + access_token=None, + skip_validation=False, + skip_validate_environments=True, + pull_environment_image=False, + server_capabilities=False, + parameters=False, +): + + print("") + display_message( + f"Results from server side validation", + msg_type="info", + ) + + reana_yaml = json.loads(json.dumps(reana_yaml)) + + # Instruct server to also check server capanilities if needed + reana_yaml['server_capabilities'] = server_capabilities + + # Add runtime_parameters if they exist + reana_yaml['runtime_parameters'] = parameters + + # Also check environments if needed + reana_yaml['skip_validate_environments'] = skip_validate_environments + + # Send to server's api for validation + from reana_client.api.client import validate_workflow + response, http_response = validate_workflow(reana_yaml) + #TODO: remove + #print("\nResponse from server:") + #print(response, http_response) + #print("") + + display_message( + f"Verifying REANA specification file... {filepath}", + msg_type="info", + ) + + validation_warnings = response["message"]["reana_spec_file_warnings"] + if validation_warnings: + display_message( + "The REANA specification appears valid, but some warnings were found.", + msg_type="warning", + indented=True, + ) + for warning_key, warning_values in validation_warnings.items(): + if warning_key == "additional_properties": + # warning_values is a list of unexpected properties + messages = [ + f"'{value['property']}'" + + (f" (at {value['path']})" if value["path"] else "") + for value in warning_values + ] + message = ( + f"Unexpected properties found in REANA specification file: " + f"{', '.join(messages)}." + ) + else: + # warning_values is a list of dictionaries with 'message' and 'path' + messages = [ + f"{value['message']}" + + (f" (at {value['path']})" if value["path"] else "") + for value in warning_values + ] + message = f"{'; '.join(messages)}." + display_message( + message, + msg_type="warning", + indented=True, + ) + if validation_warnings: + display_message( + "Please make sure that the REANA specification file is correct.", + msg_type="warning", + indented=True, + ) + else: + display_message( + "Valid REANA specification file.", + msg_type="success", + indented=True, + ) + + validation_parameter_warnings = json.loads(response["message"]["reana_spec_params_warnings"]) + display_reana_params_warnings(validation_parameter_warnings) + display_workflow_params_warnings(validation_parameter_warnings) + display_operations_warnings(validation_parameter_warnings) + + if parameters: + display_message( + f"Validating runtime parameters...", + msg_type="info", + ) + runtime_params_warnings = response["message"]["runtime_params_warnings"] + if runtime_params_warnings: + for warning_message in runtime_params_warnings: + display_message( + warning_message, + msg_type="warning", + indented=True, + ) + + runtime_params_errors = response["message"]["runtime_params_errors"] + if runtime_params_errors: + for error_message in runtime_params_errors: + display_message( + error_message, + msg_type="error", + indented=True, + ) + sys.exit(1) + + if not runtime_params_warnings: + display_message( + "Runtime paramters appear valid.", + msg_type="success", + indented=True, + ) + + server_capabilities = response["message"]["server_capabilities"] + if server_capabilities: + display_message( + f"Verifying compute backends in REANA specification file...", + msg_type="info", + ) + for message in server_capabilities: + if message: + display_message( + message["message"], + msg_type=message["msg_type"], + indented=True, + ) + + if not skip_validate_environments: + display_message( + "Verifying environments in REANA specification file...", + msg_type="info", + ) + environments_warnings = response["message"]["environments_warnings"] + for message in environments_warnings: + if message: + display_message( + message["message"], + msg_type=message["type"], + indented=True, + ) + + print("") def _validate_server_capabilities(reana_yaml: Dict, access_token: str) -> None: """Validate server capabilities in REANA specification file. @@ -137,10 +363,10 @@ def validate_input_parameters(live_parameters, original_parameters): for parameter in parsed_input_parameters.keys(): if parameter not in original_parameters: display_message( - "Given parameter - {0}, is not in reana.yaml".format(parameter), - msg_type="error", + 'Command-line parameter "{0}" is not defined in reana.yaml'.format(parameter), + msg_type='error', ) - del live_parameters[parameter] + sys.exit(1) return live_parameters