Skip to content

Commit

Permalink
feat(CLI): Create the basics of the cli to up
Browse files Browse the repository at this point in the history
created some basics commands, options and
help option for up and the its commands
  • Loading branch information
fabiobarkoski committed Jan 24, 2024
1 parent 0a5367d commit 81ea042
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 81ea042

Please sign in to comment.