Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Server side validation #726

Draft
wants to merge 15 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions reana_client/api/client.py
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -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"])
6 changes: 4 additions & 2 deletions reana_client/cli/workflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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(
Expand Down
2 changes: 2 additions & 0 deletions reana_client/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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":
Expand Down
232 changes: 229 additions & 3 deletions reana_client/validation/utils.py
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand All @@ -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"]
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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


Expand Down