async def execute_commands() -> None: # execute and collect all task commands results: Dict[TaskCommand, Any] = {} for command in commands: if isinstance(command, SendMessage): await self.message_bus.emit(command.message) results[command] = None elif isinstance(command, ExecuteOnCLI): # TODO: instead of executing it in process, we should do an http call here to a worker core. ctx = CLIContext({ **command.env, **wi.descriptor.environment }) result = await self.cli.execute_cli_command( command.command, stream.list, ctx) results[command] = result else: raise AttributeError( f"Does not understand this command: {wi.descriptor.name}: {command}" ) # The descriptor might be removed in the mean time. If this is the case stop execution. if wi.descriptor_alive: active_before_result = wi.is_active # before we move on, we need to store the current state of the task (or delete if it is done) await self.store_running_task_state(wi, origin_message) # inform the task about the result, which might trigger new tasks to execute new_commands = wi.handle_command_results(results) if new_commands: # note: recursion depth is defined by the number of steps in a job description and should be safe. await self.execute_task_commands(wi, new_commands) elif active_before_result and not wi.is_active: # if this was the last result the task was waiting for, delete the task await self.store_running_task_state(wi, origin_message)
async def test_configs_command(cli: CLI, tmp_directory: str) -> None: config_file = os.path.join(tmp_directory, "config.yml") async def check_file_is_yaml(res: Stream) -> None: async with res.stream() as streamer: async for s in streamer: with open(s, "r") as file: yaml.safe_load(file.read()) # create a new config entry create_result = await cli.execute_cli_command( "configs set test_config t1=1, t2=2, t3=3 ", stream.list) assert create_result[0][0] == "t1: 1\nt2: 2\nt3: 3\n" # show the entry - should be the same as the created one show_result = await cli.execute_cli_command("configs show test_config", stream.list) assert show_result[0][0] == "t1: 1\nt2: 2\nt3: 3\n" # list all configs: only one is defined list_result = await cli.execute_cli_command("configs list", stream.list) assert list_result[0] == ["test_config"] # edit the config: will make the config available as file await cli.execute_cli_command("configs edit test_config", check_file_is_yaml) # update the config update_doc = "a: '1'\nb: 2\nc: true\nd: null\n" with open(config_file, "w") as file: file.write(update_doc) ctx = CLIContext(uploaded_files={"config.yaml": config_file}) update_result = await cli.execute_cli_command( f"configs update test_config {config_file}", stream.list, ctx) assert update_result == [[]] # show the entry - should be the same as the created one show_updated_result = await cli.execute_cli_command( "configs show test_config", stream.list) assert show_updated_result[0][0] == update_doc
async def test_jq_command(cli: CLI) -> None: ctx = CLIContext(env={"section": "reported"}, query=Query.by("test")) # .test -> .reported.test assert JqCommand.rewrite_props(".a,.b", ctx) == ".reported.a,.reported.b" # absolute paths are rewritten correctly assert JqCommand.rewrite_props("./reported", ctx) == ".reported" # object construction is supported assert JqCommand.rewrite_props("{a:.a, b:.b}", ctx) == "{a:.reported.a, b:.reported.b}" # no replacement after pipe assert JqCommand.rewrite_props( "map(.color) | {a:.a, b:.b}", ctx) == "map(.reported.color) | {a:.a, b:.b}" result = await cli.execute_cli_command('json {"a":{"b":1}} | jq ".a.b"', stream.list) assert len(result[0]) == 1 assert result[0][0] == 1 # allow absolute paths as json path result = await cli.execute_cli_command( 'json {"id":"123", "reported":{"b":1}} | jq "./reported"', stream.list) assert result == [[{"b": 1}]] # jq .kind is rewritten as .reported.kind result = await cli.execute_cli_command("search is(foo) limit 2 | jq .kind", stream.list) assert result[0] == ["foo", "foo"]
def __init__( self, running_task_db: RunningTaskDb, job_db: JobDb, message_bus: MessageBus, event_sender: AnalyticsEventSender, subscription_handler: SubscriptionHandler, scheduler: Scheduler, cli: CLI, config: CoreConfig, ): self.running_task_db = running_task_db self.job_db = job_db self.message_bus = message_bus self.event_sender = event_sender self.subscription_handler = subscription_handler self.scheduler = scheduler self.cli = cli self.cli_context = CLIContext(source="task_handler") self.config = config # note: the waiting queue is kept in memory and lost when the service is restarted. self.start_when_done: Dict[str, TaskDescription] = {} # Step1: define all workflows and jobs in code: later it will be persisted and read from database self.task_descriptions: Sequence[TaskDescription] = [*self.known_workflows(config), *self.known_jobs()] self.tasks: Dict[str, RunningTask] = {} self.message_bus_watcher: Optional[Task[None]] = None self.initial_start_workflow_task: Optional[Task[None]] = None self.timeout_watcher = Periodic("task_timeout_watcher", self.check_overdue_tasks, timedelta(seconds=10)) self.registered_event_trigger: List[Tuple[EventTrigger, TaskDescription]] = [] self.registered_event_trigger_by_message_type: Dict[str, List[Tuple[EventTrigger, TaskDescription]]] = {}
def test_alias_template() -> None: params = [ AliasTemplateParameter("a", "some a"), AliasTemplateParameter("b", "some b", "bv") ] tpl = AliasTemplate("foo", "does foes", "{{a}} | {{b}}", params) assert tpl.render({"a": "test", "b": "bla"}) == "test | bla" assert tpl.rendered_help(CLIContext()) == dedent(""" foo: does foes ```shell foo a=<value>, b=<value> ``` ## Parameters - `a`: some a - `b` [default: bv]: some b ## Template ```shell > {{a}} | {{b}} ``` ## Example ```shell # Executing this alias template > foo a="test_a" # Will expand to this command > test_a | bv ``` """)
async def test_write_command(cli: CLI) -> None: async def check_file(res: Stream, check_content: Optional[str] = None) -> None: async with res.stream() as streamer: only_one = True async for s in streamer: assert isinstance(s, str) p = Path(s) assert p.exists() and p.is_file() assert 1 < p.stat().st_size < 100000 assert p.name.startswith("write_test") assert only_one only_one = False if check_content: with open(s, "r") as file: data = file.read() assert data == check_content # result can be read as json await cli.execute_cli_command( "search all limit 3 | format --json | write write_test.json ", check_file) # result can be read as yaml await cli.execute_cli_command( "search all limit 3 | format --yaml | write write_test.yaml ", check_file) # write enforces unescaped output. env = { "now": utc_str() } # fix the time, so that replacements will stay equal truecolor = CLIContext(console_renderer=ConsoleRenderer( 80, 25, ConsoleColorSystem.truecolor, True), env=env) monochrome = CLIContext( console_renderer=ConsoleRenderer.default_renderer(), env=env) # Make sure, that the truecolor output is different from monochrome output mono_out = await cli.execute_cli_command("help", stream.list, monochrome) assert await cli.execute_cli_command("help", stream.list, truecolor) != mono_out # We expect the content of the written file to contain monochrome output. assert await cli.execute_cli_command( "help | write write_test.txt", partial(check_file, check_content="".join(mono_out[0]) + "\n"), truecolor)
def cli_context_from_request(request: Request) -> CLIContext: try: columns = int(request.headers.get("Resoto-Shell-Columns", "120")) rows = int(request.headers.get("Resoto-Shell-Rows", "50")) terminal = request.headers.get("Resoto-Shell-Terminal", "false") == "true" colors = ConsoleColorSystem.from_name( request.headers.get("Resoto-Shell-Color-System", "monochrome")) renderer = ConsoleRenderer(width=columns, height=rows, color_system=colors, terminal=terminal) return CLIContext(env=dict(request.query), console_renderer=renderer) except Exception as ex: log.debug("Could not create CLI context.", exc_info=ex) return CLIContext( env=dict(request.query), console_renderer=ConsoleRenderer.default_renderer())
def test_supports_color() -> None: assert not CLIContext().supports_color() assert not CLIContext(console_renderer=ConsoleRenderer()).supports_color() assert not CLIContext(console_renderer=ConsoleRenderer( color_system=ConsoleColorSystem.monochrome)).supports_color() assert CLIContext(console_renderer=ConsoleRenderer( color_system=ConsoleColorSystem.standard)).supports_color() assert CLIContext(console_renderer=ConsoleRenderer( color_system=ConsoleColorSystem.eight_bit)).supports_color() assert CLIContext(console_renderer=ConsoleRenderer( color_system=ConsoleColorSystem.truecolor)).supports_color()
async def test_system_restore_command(cli: CLI, tmp_directory: str) -> None: backup = os.path.join(tmp_directory, "backup") async def move_backup(res: Stream) -> None: async with res.stream() as streamer: async for s in streamer: os.rename(s, backup) await cli.execute_cli_command("system backup create", move_backup) ctx = CLIContext(uploaded_files={"backup": backup}) restore = await cli.execute_cli_command( f"BACKUP_NO_SYS_EXIT=true system backup restore {backup}", stream.list, ctx) assert restore == [[ "Database has been restored successfully!", "Since all data has changed in the database eventually, this service needs to be restarted!", ]]
def test_format() -> None: context = CLIContext() fn = context.formatter("foo={foo} and bla={bla}: {bar}") assert fn({}) == "foo=null and bla=null: null" assert fn({"foo": 1, "bla": 2, "bar": 3}) == "foo=1 and bla=2: 3"
def test_context_format() -> None: context = CLIContext() fn, vs = context.formatter_with_variables("foo={foo} and bla={bla}: {bar}") assert vs == {"foo", "bla", "bar"} assert fn({}) == "foo=null and bla=null: null" assert fn({"foo": 1, "bla": 2, "bar": 3}) == "foo=1 and bla=2: 3"
async def execute(cmd: str) -> List[List[JsonElement]]: ctx = CLIContext(cli.cli_env) return await cli.execute_cli_command(cmd, stream.list, ctx)
async def test_welcome(cli: CLI) -> None: ctx = CLIContext(console_renderer=ConsoleRenderer.default_renderer()) result = await cli.execute_cli_command(f"welcome", stream.list, ctx) assert "Resoto" in result[0][0]