Skip to content
Open
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
82 changes: 55 additions & 27 deletions app/connectors_service/connectors/connectors_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import asyncio
import json
import os
from functools import wraps

import click
import yaml
Expand All @@ -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,
Expand All @@ -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")
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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(
Comment thread
Saithej2k marked this conversation as resolved.
obj,
index_name,
Expand Down Expand Up @@ -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()
Expand All @@ -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"])
Expand All @@ -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"])
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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"])
Expand Down Expand Up @@ -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"])
Expand All @@ -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",
Expand Down
62 changes: 62 additions & 0 deletions app/connectors_service/tests/test_connectors_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,68 @@ def test_connector_help_page():
assert "Commands:" in result.output


@pytest.mark.parametrize(
Comment thread
Saithej2k marked this conversation as resolved.
"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()
Expand Down