Skip to content

Commit

Permalink
Merge pull request #17 from fabiobarkoski/cli
Browse files Browse the repository at this point in the history
CLI Improvements
  • Loading branch information
faermanj authored Feb 12, 2024
2 parents 0a5367d + 81ea042 commit 7c393cb
Show file tree
Hide file tree
Showing 13 changed files with 314 additions and 29 deletions.
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,24 @@ Provide a framework so that solutions can be automated with Ansible, and leverag

For a better integration with the Ansible ecosystem, this project is mostly written in python, using pluggy as a plugin system.

# Documentation

## The CLI

### How do I use up?
Below, an example usage of up:
```bash
$ up ansible --version # will output the Ansible version
```
"But what if I want to pass a port or volume?" So you have 2 options: first: set the volume/port inside the `up.yaml` of your plugin, or in the example case the Ansible plugin; or second: pass the volume/port on the command, e.g:
```bash
$ up -Xp=8080/tcp:5000 ansible --version # will set the port in the container
$ up -Xv=/home:/mnt/vol3,rw ansible --version # will set the volume in the container
```
### Does up have some internal command?
Yes, the current available commands are in `plugin` reserved keyword:
```bash
$ up plugin list # List all installed plugins
$ up plugin prompts ansible # Shows the prompts from up.yaml of a plugin
$ up plugin details ansible --version # Shows the prompt configuration from up.yaml of a plugin
```
79 changes: 78 additions & 1 deletion upcli/poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions upcli/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ up-splat = "^0.2.19"
up-ansible = "^0.2.19"
up-aws = "^0.2.19"
up-demo = "^0.2.19"
ruamel-yaml = "^0.18.5"

[build-system]
requires = ["poetry-core"]
Expand Down
2 changes: 2 additions & 0 deletions upcli/up/__version__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
version = "0.2.19"

82 changes: 82 additions & 0 deletions upcli/up/commands.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import sys
import pkgutil
from pathlib import Path
from ruamel.yaml import YAML
from typing import TypeAlias
from collections.abc import Callable, Iterator
from importlib.util import find_spec

from uplib import Prompt
from up.help_texts import list_help_text, details_help_text, prompts_help_text

yaml=YAML()

Arguments: TypeAlias = list[str]
Commands: TypeAlias = dict[str, tuple[Callable[[Arguments], None], str]]

def _get_prompts(plugin_name: str) -> Iterator[dict]:
plugin_path = Path(find_spec(f"up_{plugin_name}").origin).parent
prompts_path = Path(plugin_path, 'up.yaml')
try:
with open(prompts_path, 'r') as file:
prompts = (yaml.load(file))['prompts']
for prompt in prompts:
yield prompt
except FileNotFoundError:
raise FileNotFoundError("Plugin doens't have yaml file!")

def _list_plugins(args: Arguments):
"""
Shows all installed plugins
"""
plugins = []
for pkg in pkgutil.iter_modules():
if pkg.name.startswith("up_"):
plugins.append(pkg.name)
print("Installed plugins:")
print("\n".join(plugins))

def _list_prompts(args: Arguments):
"""
Shows the prompts from up.yaml of a plugin
"""
for prompt in _get_prompts(args[0]):
yaml.dump(prompt, sys.stdout)

def _prompt_details(args: Arguments):
"""
Shows the prompt configuration from up.yaml of a plugin
"""
for prompt in _get_prompts(args[0]):
if prompt['prompt'] == ' '.join(args):
yaml.dump(prompt, sys.stdout)
break

_commands: Commands = {
"list": (_list_plugins, list_help_text),
"prompts": (_list_prompts, prompts_help_text),
"details": (_prompt_details, details_help_text),
}

def _get_command_description(command:str) -> str:
description = _commands[command][0].__doc__.replace('\n','')
return description.lstrip()

def cmd_in_prompt(prompt: Prompt) -> bool:
reserved_keyword = prompt[0]
if reserved_keyword != 'plugin' or len(prompt) <= 1:
return False
command = prompt[1]
match command:
case cmd if cmd in _commands.keys():
return True
case _:
raise Exception('Command not found!')

def run_cmd(command: str, arguments: Arguments):
if len(arguments) == 1 and '--help' in arguments or '-h' in arguments:
cmd_description = _get_command_description(command)
print(_commands[command][1] % cmd_description)
exit(0)
_commands[command][0](arguments)
exit(0)
35 changes: 35 additions & 0 deletions upcli/up/help_texts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
up_help_text = """\rUnified and Pluggable CLI - Version %s
\n\rUsage:
up <prompt>
or
up plugin <command>
\n\rPrompt options:
-Xv\t Set a volume to be used
-Xp\t Set a port to be used
\n\rAvailable commands:
\r%s
\rExamples:
up ansible --version\t Shows ansible version
up -Xp={8080/tcp: 5000} ansible -h\t Will set the port to ansible
up plugin prompts ansible\t List all ansible prompts from up.yaml
"""
list_help_text = """\rDescription: %s
\n\rUsage:
up plugin list
"""
details_help_text = """\rDescription: %s
\n\rUsage:
up plugin details <prompt>
\rExample:
up plugin details ansible --version\t Shows prompt configuration from up.yaml
"""
prompts_help_text = """Description: %s
\n\rUsage:
up plugin prompts <plugin>
\rExample:
up plugin details ansible\t List all ansible prompts from up.yaml
"""

36 changes: 18 additions & 18 deletions upcli/up/main.py
Original file line number Diff line number Diff line change
@@ -1,42 +1,42 @@
import sys
from datetime import datetime
import shlex
import pkgutil

import uplib
from up.__version__ import version
from up.options import setup_options
from up.help_texts import up_help_text
from up.utils import get_commands_information
from up.commands import cmd_in_prompt, run_cmd

log = uplib.log

def print_help():
log.debug("up [prompt]")

def get_modules():
up_plugins = []
for pkg in pkgutil.iter_modules():
if pkg.name.startswith("up_"):
up_plugins.append(pkg.name)
return up_plugins
def _print_up_help(args) -> bool:
if len(args) == 2:
if args[1] == "--help" or args[1] == "-h":
return True
return False

def exit_cli(code=-1):
if code:
log.error("Exiting up cli with code %s", code)
if code == "NO_COMMAND_SPECIFIED":
log.error(f"installed plugins:\n{'\n'.join(get_modules())}")
sys.exit(code)

def cli_main():
now = datetime.now()
log.info(f"Starting UP cli at {now.isoformat()}")
args = sys.argv
len_args = len(args)
if len_args <= 1:
print_help()
exit_cli("NO_COMMAND_SPECIFIED")
if len(args) <= 1 or _print_up_help(args):
print(up_help_text % (version, get_commands_information()))
exit(0)
executable = args[0]
prompt = args[1:]
if cmd_in_prompt(prompt):
run_cmd(prompt[1], prompt[2:])
context = {"executable": executable}
try:
uplib.up_main(context, prompt)
prompt_without_options = setup_options(prompt)
print(prompt_without_options)
uplib.up_main(context, prompt_without_options)
except Exception as e:
log.error(e)
# print stack trace
Expand Down
51 changes: 51 additions & 0 deletions upcli/up/options.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import re
from shlex import join

from uplib import options_map, Prompt

_options_pattern = re.compile(r'\-X\w+\=\S+')

def _have_options(prompt: Prompt) -> bool:
if _options_pattern.match(join(prompt)):
return True
return False

def _match_value(pattern: str, string: str) -> str:
value = re.match(pattern, string)
return value.groups()[0]

def _set_ports(value: str):
formated_value = value.split(':')
options_map['ports'] = {formated_value[0]: formated_value[1]}

def _set_volumes(value: str):
formated_value = value.split(':')
volume_key = formated_value[0]
volume_bind = formated_value[1].split(',')[0]
volume_mode = formated_value[1].split(',')[1]
options_map['volumes'] = {
volume_key: {
'bind': volume_bind,
'mode': volume_mode,
},
}

_options = {
'p': _set_ports,
'v': _set_volumes,
}

def _set_option(option: str):
key = _match_value(r'\-X(\w)', option)
value = _match_value(r'\-X\w.(.+)', option)
_options[key](value)

def setup_options(prompt: Prompt) -> Prompt:
prompt_without_options = join(prompt)
if _have_options(prompt):
for option in _options_pattern.findall(join(prompt)):
_set_option(option)
prompt_without_options = _options_pattern.sub(
'', prompt_without_options)
return prompt_without_options.lstrip().split()
return prompt
10 changes: 10 additions & 0 deletions upcli/up/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from up.commands import _commands

def get_commands_information() -> str:
commands = []
for key, value in _commands.items():
command = key
command_description = value[0].__doc__.replace('\n','')
commands.append(f" {command}\t {command_description}")
return "\n".join(commands)

4 changes: 3 additions & 1 deletion uplib/uplib/__init__.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
import pluggy
from typing import TypeAlias
from typing import TypeAlias, Union


Context: TypeAlias = dict[str, str]
Prompt: TypeAlias = list[str]
Options: TypeAlias = dict[str, dict[str, dict[str, str] | str]]


# TODO: Consider moving all globals to a single object
hookspec = pluggy.HookspecMarker("up")
hookimpl = pluggy.HookimplMarker("up")
pm = pluggy.PluginManager("up")
settings_maps = {}
options_map: Options = {}

from .match import does_match, if_prompt_matches
from .containers import ContainerRun, ContainerRuns
Expand Down
Loading

0 comments on commit 7c393cb

Please sign in to comment.