diff --git a/app/connectors_service/connectors/connectors_cli.py b/app/connectors_service/connectors/connectors_cli.py index ffccc9aba..58cb2c2ac 100644 --- a/app/connectors_service/connectors/connectors_cli.py +++ b/app/connectors_service/connectors/connectors_cli.py @@ -14,6 +14,7 @@ import asyncio import json import os +from functools import wraps import click import yaml @@ -32,19 +33,47 @@ __all__ = ["main"] -def load_config(ctx, config): +def load_config(config): if config: return yaml.safe_load(config) elif os.path.isfile(CONFIG_FILE_PATH): with open(CONFIG_FILE_PATH, "r") as f: return yaml.safe_load(f.read()) - elif ctx.invoked_subcommand == "login": - pass else: msg = f"{CONFIG_FILE_PATH} was not found." raise FileNotFoundError(msg) +def ensure_config(ctx): + ctx.ensure_object(dict) + if "config" in ctx.obj: + return + + try: + ctx.obj["config"] = load_config(ctx.obj.get("config_file")) + except FileNotFoundError as e: + click.echo( + f"{e} Make sure that the config is either present at the default location ({CONFIG_FILE_PATH}) or it's passed via the '-c' or '--config' option." + ) + ctx.exit(1) + + +def eager_config_check(ctx, _param, value): + if not ctx.resilient_parsing: + ensure_config(ctx) + return value + + +def requires_config(func): + @wraps(func) + def wrapper(*args, **kwargs): + ctx = click.get_current_context() + ensure_config(ctx) + return func(ctx.obj, *args, **kwargs) + + return wrapper + + # Main group @click.group( invoke_without_command=True, @@ -55,19 +84,13 @@ def load_config(ctx, config): @click.pass_context def cli(ctx, config): # print help page if no subcommands provided + ctx.ensure_object(dict) + ctx.obj["config_file"] = config + if ctx.invoked_subcommand is None: click.echo(ctx.get_help()) return - ctx.ensure_object(dict) - try: - ctx.obj["config"] = load_config(ctx, config) - except FileNotFoundError as e: - click.echo( - f"{e} Make sure that the config is either present at the default location ({CONFIG_FILE_PATH}) or it's passed via the '-c' or '--config' option." - ) - ctx.exit(1) - @click.command(help="Authenticate Connectors CLI with an Elasticsearch instance") @click.option("--host", prompt="Elastic host") @@ -112,13 +135,12 @@ def login(host, method): # Connector group @click.group(invoke_without_command=False, help="Connectors management") -@click.pass_context -def connector(ctx): +def connector(): pass @click.command(name="list", help="List all existing connectors") -@click.pass_obj +@requires_config def list_connectors(obj): connector = Connector(config=obj["config"]["elasticsearch"]) coro = connector.list_connectors() @@ -184,6 +206,14 @@ def interactive_service_type_prompt(): @click.command(help="Creates a new connector and a search index") +@click.option( + "--config-precheck", + is_flag=True, + is_eager=True, + expose_value=False, + hidden=True, + callback=eager_config_check, +) @click.option( "--index-name", prompt=f"{click.style('?', fg='green')} Index name", @@ -235,7 +265,7 @@ def interactive_service_type_prompt(): prompt=f"{click.style('?', fg='green')} Connector name", help="Connector name", ) -@click.pass_obj +@requires_config def create( obj, index_name, @@ -385,13 +415,12 @@ def prompt(): # Index group @click.group(invoke_without_command=False, help="Search indices management") -@click.pass_obj -def index(obj): +def index(): pass @click.command(name="list", help="Show all indices") -@click.pass_obj +@requires_config def list_indices(obj): index = Index(config=obj["config"]["elasticsearch"]) indices = index.list_indices() @@ -418,7 +447,7 @@ def list_indices(obj): @click.command(help="Remove all documents from the index") -@click.pass_obj +@requires_config @click.argument("index", nargs=1) def clean(obj, index): index_cli = Index(config=obj["config"]["elasticsearch"]) @@ -443,7 +472,7 @@ def clean(obj, index): @click.command(help="Delete an index") -@click.pass_obj +@requires_config @click.argument("index", nargs=1) def delete(obj, index): index_cli = Index(config=obj["config"]["elasticsearch"]) @@ -471,13 +500,12 @@ def delete(obj, index): # Job group @click.group(invoke_without_command=False, help="Sync jobs management") -@click.pass_obj -def job(obj): +def job(): pass @click.command(help="Start a sync job.") -@click.pass_obj +@requires_config @click.option("-i", help="Connector ID", required=True) @click.option( "-t", @@ -519,7 +547,7 @@ def start(obj, i, t, output_format): @click.command(name="list", help="List of jobs sorted by date.") -@click.pass_obj +@requires_config @click.argument("connector_id", nargs=1) def list_jobs(obj, connector_id): job_cli = Job(config=obj["config"]["elasticsearch"]) @@ -564,7 +592,7 @@ def list_jobs(obj, connector_id): @click.command(help="Cancel a job") -@click.pass_obj +@requires_config @click.argument("job_id") def cancel(obj, job_id): job_cli = Job(config=obj["config"]["elasticsearch"]) @@ -589,7 +617,7 @@ def cancel(obj, job_id): @click.command(help="Show information about a job", name="view") -@click.pass_obj +@requires_config @click.argument("job_id") @click.option( "-o", diff --git a/app/connectors_service/tests/test_connectors_cli.py b/app/connectors_service/tests/test_connectors_cli.py index 7f7de65bc..0181c7304 100644 --- a/app/connectors_service/tests/test_connectors_cli.py +++ b/app/connectors_service/tests/test_connectors_cli.py @@ -128,6 +128,68 @@ def test_connector_help_page(): assert "Commands:" in result.output +@pytest.mark.parametrize( + "commands", [["connector", "--help"], ["index", "--help"], ["job", "--help"]] +) +def test_group_help_page_does_not_require_config(commands, mock_cli_config): + mock_cli_config.side_effect = FileNotFoundError(CONFIG_FILE_PATH) + + runner = CliRunner() + result = runner.invoke(cli, commands) + + assert result.exit_code == 0 + assert "Usage:" in result.output + mock_cli_config.assert_not_called() + + +@pytest.mark.parametrize( + "commands", + [ + ["connector", "list", "--help"], + ["connector", "create", "--help"], + ["index", "list", "--help"], + ["index", "clean", "--help"], + ["index", "delete", "--help"], + ["job", "start", "--help"], + ["job", "list", "--help"], + ["job", "cancel", "--help"], + ["job", "view", "--help"], + ], +) +def test_command_help_page_does_not_require_config(commands, mock_cli_config): + mock_cli_config.side_effect = FileNotFoundError(CONFIG_FILE_PATH) + + runner = CliRunner() + result = runner.invoke(cli, commands) + + assert result.exit_code == 0 + assert "Usage:" in result.output + mock_cli_config.assert_not_called() + + +def test_command_without_config_fails(mock_cli_config): + mock_cli_config.side_effect = FileNotFoundError(CONFIG_FILE_PATH) + + runner = CliRunner() + result = runner.invoke(cli, ["connector", "list"]) + + assert result.exit_code == 1 + assert f"{CONFIG_FILE_PATH} was not found." in result.output + assert "Make sure that the config is either present" in result.output + mock_cli_config.assert_called_once() + + +def test_connector_create_without_config_fails_before_prompt(mock_cli_config): + mock_cli_config.side_effect = FileNotFoundError(CONFIG_FILE_PATH) + + runner = CliRunner() + result = runner.invoke(cli, ["connector", "create"]) + + assert result.exit_code == 1 + assert "Index name" not in result.output + mock_cli_config.assert_called_once() + + @patch("connectors.cli.connector.Connector.list_connectors", AsyncMock(return_value=[])) def test_connector_list_no_connectors(): runner = CliRunner()