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

cli: implement share-add command #683

Closed
wants to merge 1 commit into from
Closed
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
1 change: 1 addition & 0 deletions AUTHORS.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ The list of contributors in alphabetical order:
- `Audrius Mecionis <https://orcid.org/0000-0002-3759-1663>`_
- `Anton Khodak <https://orcid.org/0000-0003-3263-4553>`_
- `Camila Diaz <https://orcid.org/0000-0001-5543-797X>`_
- `Daan Rosendal <https://orcid.org/0000-0002-3447-9000>`_
- `Daniel Prelipcean <https://orcid.org/0000-0002-4855-194X>`_
- `Diego Rodriguez <https://orcid.org/0000-0003-0649-2002>`_
- `Dinos Kousidis <https://orcid.org/0000-0002-4914-4289>`_
Expand Down
3 changes: 3 additions & 0 deletions docs/cmd_list.txt
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ Workflow execution commands:
stop Stop a running workflow.
validate Validate workflow specification file.

Workflow sharing commands:
share-add Share a workflow with other users (read-only).

Workspace interactive commands:
close Close an interactive session.
open Open an interactive session inside the workspace.
Expand Down
67 changes: 57 additions & 10 deletions reana_client/api/client.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
#
# This file is part of REANA.
# Copyright (C) 2017, 2018, 2019, 2020, 2021, 2022 CERN.
# Copyright (C) 2017, 2018, 2019, 2020, 2021, 2022, 2023 CERN.
#
# REANA is free software; you can redistribute it and/or modify it
# under the terms of the MIT License; see LICENSE file for more details.
Expand All @@ -17,20 +17,19 @@

import requests
from bravado.exception import HTTPError
from reana_commons.validation.utils import validate_reana_yaml, validate_workflow_name
from reana_commons.specification import (
load_workflow_spec_from_reana_yaml,
load_input_parameters,
)
from reana_client.config import ERROR_MESSAGES
from reana_client.errors import FileDeletionError, FileUploadError
from reana_client.utils import is_regular_path, is_uuid_v4
from reana_commons.api_client import get_current_api_client
from reana_commons.config import REANA_WORKFLOW_ENGINES
from reana_commons.errors import REANASecretAlreadyExists, REANASecretDoesNotExist
from reana_commons.specification import (
load_input_parameters,
load_workflow_spec_from_reana_yaml,
)
from reana_commons.validation.utils import validate_reana_yaml, validate_workflow_name
from werkzeug.local import LocalProxy

from reana_client.config import ERROR_MESSAGES
from reana_client.errors import FileDeletionError, FileUploadError
from reana_client.utils import is_uuid_v4, is_regular_path

current_rs_api_client = LocalProxy(
partial(get_current_api_client, component="reana-server")
)
Expand Down Expand Up @@ -1280,3 +1279,51 @@ def prune_workspace(workflow, include_inputs, include_outputs, access_token):
)
)
raise Exception(e.response.json()["message"])


def share_workflow(
workflow, user_email_to_share_with, access_token, message=None, valid_until=None
):
"""Share a workflow with a user.

:param workflow: name or id of the workflow.
:param user_email_to_share_with: user to share the workflow with.
:param access_token: access token of the current user.
:param message: Optional message to include when sharing the workflow.
:param valid_until: Specify the date when access to the workflow will expire (format: YYYY-MM-DD).

:return: a dictionary containing the ``workflow_id``, ``workflow_name``, and
a ``message`` key with the result of the operation.
"""
try:
share_params = {
"workflow_id_or_name": workflow,
"user_email_to_share_with": user_email_to_share_with,
"access_token": access_token,
}

if message:
share_params["message"] = message

if valid_until:
share_params["valid_until"] = valid_until

(response, http_response) = current_rs_api_client.api.share_workflow(
**share_params
).result()

if http_response.status_code == 200:
return response
else:
raise Exception(
"Expected status code 200 but replied with "
f"{http_response.status_code}"
)

except HTTPError as e:
logging.debug(
"Workflow could not be shared: "
f"\nStatus: {e.response.status_code}\nReason: {e.response.reason}\n"
f"Message: {e.response.json()['message']}"
)
raise Exception(e.response.json()["message"])
8 changes: 4 additions & 4 deletions reana_client/cli/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
#
# This file is part of REANA.
# Copyright (C) 2017, 2018, 2019, 2020, 2021, 2022 CERN.
# Copyright (C) 2017, 2018, 2019, 2020, 2021, 2022, 2023 CERN.
#
# REANA is free software; you can redistribute it and/or modify it
# under the terms of the MIT License; see LICENSE file for more details.
Expand All @@ -11,10 +11,9 @@
import sys

import click
from urllib3 import disable_warnings

from reana_client.cli import workflow, files, ping, secrets, quotas, retention_rules
from reana_client.cli import files, ping, quotas, retention_rules, secrets, workflow
from reana_client.utils import get_api_url
from urllib3 import disable_warnings

DEBUG_LOG_FORMAT = (
"[%(asctime)s] p%(process)s "
Expand All @@ -41,6 +40,7 @@ class ReanaCLI(click.Group):
ping.configuration_group,
workflow.workflow_management_group,
workflow.workflow_execution_group,
workflow.workflow_sharing_group,
workflow.interactive_group,
files.files_group,
retention_rules.retention_rules_group,
Expand Down
103 changes: 95 additions & 8 deletions reana_client/cli/workflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,8 @@
import traceback

import click
from jsonschema.exceptions import ValidationError
from reana_commons.config import INTERACTIVE_SESSION_TYPES, REANA_COMPUTE_BACKENDS
from reana_commons.errors import REANAValidationError
from reana_commons.validation.operational_options import validate_operational_options
import yaml

from jsonschema.exceptions import ValidationError
from reana_client.cli.files import get_files, upload_files
from reana_client.cli.utils import (
add_access_token_options,
Expand Down Expand Up @@ -51,6 +47,9 @@
validate_input_parameters,
validate_workflow_name_parameter,
)
from reana_commons.config import INTERACTIVE_SESSION_TYPES, REANA_COMPUTE_BACKENDS
from reana_commons.errors import REANAValidationError
from reana_commons.validation.operational_options import validate_operational_options


@click.group(help="Workflow management commands")
Expand All @@ -67,6 +66,13 @@ def workflow_execution_group(ctx):
logging.debug(ctx.info_name)


@click.group(help="Workflow sharing commands")
@click.pass_context
def workflow_sharing_group(ctx):
"""Top level wrapper for workflow sharing."""
logging.debug(ctx.info_name)


@workflow_management_group.command("list")
@click.option(
"-w",
Expand Down Expand Up @@ -451,12 +457,12 @@ def workflow_start(
\t $ reana-client start -w myanalysis.42 -p sleeptime=10 -p myparam=4\n
\t $ reana-client start -w myanalysis.42 -p myparam1=myvalue1 -o CACHE=off
"""
from reana_client.utils import get_api_url
from reana_client.api.client import (
get_workflow_parameters,
get_workflow_status,
start_workflow,
)
from reana_client.utils import get_api_url

def display_status(workflow: str, current_status: str):
"""Display the current status of the workflow."""
Expand Down Expand Up @@ -586,12 +592,12 @@ def workflow_restart(
\t $ reana-client restart -w myanalysis.42 -o TARGET=gendata\n
\t $ reana-client restart -w myanalysis.42 -o FROM=fitdata
"""
from reana_client.utils import get_api_url
from reana_client.api.client import (
get_workflow_parameters,
get_workflow_status,
start_workflow,
)
from reana_client.utils import get_api_url

logging.debug("command: {}".format(ctx.command_path.replace(" ", ".")))
for p in ctx.params:
Expand Down Expand Up @@ -1373,7 +1379,7 @@ def workflow_open_interactive_session(
Examples:\n
\t $ reana-client open -w myanalysis.42 jupyter
"""
from reana_client.api.client import open_interactive_session, info
from reana_client.api.client import info, open_interactive_session

if workflow:
try:
Expand Down Expand Up @@ -1458,3 +1464,84 @@ def workflow_close_interactive_session(workflow, access_token): # noqa: D301
sys.exit(1)
else:
display_message("Cannot find workflow {} ".format(workflow), msg_type="error")


@workflow_sharing_group.command("share-add")
@check_connection
@add_workflow_option
@add_access_token_options
@click.option(
"-u",
"--user",
"users",
multiple=True,
help="Users to share the workflow with.",
required=True,
)
@click.option(
"-m",
"--message",
help="Optional message that is sent to the user(s) with the sharing invitation.",
)
@click.option(
"-v",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The shorthand option -v is normally used for --verbose in other scripts, so I think we should either change it to another letter or even drop the shorthand version. What do you think?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed it to allow for --valid-until only, removed the -v shorthand.

The changes were applied to the following superseding PR: #692

"--valid-until",
help="Optional date when access to the workflow will expire for the given user(s) (format: YYYY-MM-DD).",
)
Comment on lines +1486 to +1490
Copy link
Member

@mdonadoni mdonadoni Jan 8, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the suggestion. Added.

The changes were applied to the following superseding PR: #692

@click.pass_context
def share_workflow_add(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick:

Suggested change
def share_workflow_add(
def workflow_share_add(

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

The changes were applied to the following superseding PR: #692

ctx, workflow, access_token, users, message, valid_until
): # noqa D412
"""Share a workflow with other users (read-only).

The `share-add` command allows sharing a workflow with other users. The
users will be able to view the workflow but not modify it.

Examples:

$ reana-client share-add -w myanalysis.42 --user [email protected]

$ reana-client share-add -w myanalysis.42 --user [email protected] --user [email protected] --message "Please review my analysis" --valid-until 2025-12-31
Comment on lines +1502 to +1504
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
$ reana-client share-add -w myanalysis.42 --user bob@example.org
$ reana-client share-add -w myanalysis.42 --user bob@example.org --user cecile@example.org --message "Please review my analysis" --valid-until 2025-12-31
\t $ reana-client share-add -w myanalysis.42 --user bob@example.org
\t $ reana-client share-add -w myanalysis.42 --user bob@example.org --user cecile@example.org --message "Please review my analysis" --valid-until 2025-12-31

To show the example commands indented when calling reana-client share-add --help

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

The changes were applied to the following superseding PR: #692

"""
from reana_client.api.client import share_workflow

share_errors = []
shared_users = []

if workflow:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to do if workflow: ... else: display_message(...), as this is already handled by @add_workflow_option. I think there are still some commands that do this, but it's a leftover from the past (that we should probably clean up at some point 😅 )

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nicely spotted. Changed

The changes were applied to the following superseding PR: #692

try:
for user in users:
try:
logging.info(f"Sharing workflow {workflow} with user {user}")
share_workflow(
workflow,
user,
access_token,
message=message,
valid_until=valid_until,
)
shared_users.append(user)
except Exception as e:
share_errors.append(
f"Failed to share {workflow} with {user}: {str(e)}"
)
logging.debug(traceback.format_exc())
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: You can use logging.exception("... message ...") instead of calling logging.debug manually

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

The changes were applied to the following superseding PR: #692

except Exception as e:
logging.debug(traceback.format_exc())
logging.debug(str(e))
display_message(
"An error occurred while sharing workflow:\n{}".format(str(e)),
msg_type="error",
)
Comment on lines +1529 to +1535
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In which cases can we end up here? All the exceptions are already caught by the other try ... except block, aren't they?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right! Removed it

The changes were applied to the following superseding PR: #692


if shared_users:
display_message(
f"{workflow} is now read-only shared with {', '.join(shared_users)}",
msg_type="success",
)
if share_errors:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to check if share_errors: ..., as if the array is empty then the for loop will not do anything

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nicely spotted. Changed

The changes were applied to the following superseding PR: #692

for error in share_errors:
display_message(error, msg_type="error")
Copy link
Member

@mdonadoni mdonadoni Jan 9, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If there are errors (here or before), then reana-client should exit with non-zero exit code

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would cancel out any subsequent errors. E.g. imagine someone shares with 20 people at a time where some emails have typos; the current implementation would then show all errors after 1 command execution, while the implementation you suggest requires the user to run the command again after each error to find the next error.

We can still change this if you prefer the latter scenario for a reason that I'm not seeing.

Copy link
Member

@mdonadoni mdonadoni Mar 15, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, it's good that the errors are all shown at the end! What I meant was to add something like

if share_errors:
    sys.exit(1)

at the end of the command, so that in case of errors reana-client returns an appropriate status code. No need to call sys.exit just after the error happens.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed 👍


else:
display_message(f"Cannot find workflow {workflow}", msg_type="error")
3 changes: 0 additions & 3 deletions reana_client/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,6 @@
reana_yaml_valid_file_names = ["reana.yaml", "reana.yml"]
"""REANA specification valid file names."""

default_user = "00000000-0000-0000-0000-000000000000"
"""Default user to use when submitting workflows to REANA Server."""

ERROR_MESSAGES = {
"missing_access_token": "Please provide your access token by using"
" the -t/--access-token flag, or by setting the"
Expand Down
46 changes: 42 additions & 4 deletions tests/test_cli_workflows.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
#
# This file is part of REANA.
# Copyright (C) 2018, 2019, 2020, 2021, 2022 CERN.
# Copyright (C) 2018, 2019, 2020, 2021, 2022, 2023 CERN.
#
# REANA is free software; you can redistribute it and/or modify it
# under the terms of the MIT License; see LICENSE file for more details.
Expand All @@ -17,12 +17,11 @@
from click.testing import CliRunner
from mock import Mock, patch
from pytest_reana.test_utils import make_mock_api_client
from reana_commons.config import INTERACTIVE_SESSION_TYPES

from reana_client.api.client import create_workflow_from_json
from reana_client.config import RUN_STATUSES
from reana_client.cli import cli
from reana_client.config import RUN_STATUSES
from reana_client.utils import get_workflow_status_change_msg
from reana_commons.config import INTERACTIVE_SESSION_TYPES


def test_workflows_server_not_connected():
Expand Down Expand Up @@ -988,3 +987,42 @@ def test_yml_ext_specification(create_yaml_workflow_schema):
result = runner.invoke(cli, ["validate", "-t", reana_token])
assert result.exit_code != 0
assert message in result.output


def test_share_add_workflow():
"""Test share-add workflows."""
status_code = 200
response = {
"message": "is now read-only shared with",
"workflow_id": "string",
"workflow_name": "string",
}
env = {"REANA_SERVER_URL": "localhost"}
mock_http_response, mock_response = Mock(), Mock()
mock_http_response.status_code = status_code
mock_response = response
reana_token = "000000"
runner = CliRunner(env=env)
with runner.isolation():
with patch(
"reana_client.api.client.current_rs_api_client",
make_mock_api_client("reana-server")(mock_response, mock_http_response),
):
result = runner.invoke(
cli,
[
"share-add",
"-t",
reana_token,
"--workflow",
"test-workflow.1",
"--user",
"[email protected]",
"--message",
"Test message",
"--valid-until",
"2024-01-01",
],
)
assert result.exit_code == 0
assert response["message"] in result.output
Loading