diff --git a/.gitignore b/.gitignore index 3b22f8e..85c217c 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ dist .pyenv __pycache__ + diff --git a/up_ansible/poetry.lock b/up_ansible/poetry.lock index 93c0ea6..a9d867f 100644 --- a/up_ansible/poetry.lock +++ b/up_ansible/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. [[package]] name = "uplib" diff --git a/up_aws/poetry.lock b/up_aws/poetry.lock index 1186fa7..3af4d0e 100644 --- a/up_aws/poetry.lock +++ b/up_aws/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. package = [] [metadata] diff --git a/up_demo/poetry.lock b/up_demo/poetry.lock index 93c0ea6..a9d867f 100644 --- a/up_demo/poetry.lock +++ b/up_demo/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. [[package]] name = "uplib" diff --git a/up_splat/poetry.lock b/up_splat/poetry.lock index 1ec554f..e07251d 100644 --- a/up_splat/poetry.lock +++ b/up_splat/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. [[package]] name = "pluggy" diff --git a/upcli/ansible_version.sh b/upcli/ansible_version.sh new file mode 100755 index 0000000..9973610 --- /dev/null +++ b/upcli/ansible_version.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +# FROM cytopia/ansible:latest +ansible --version diff --git a/upcli/fedora_version.sh b/upcli/fedora_version.sh new file mode 100755 index 0000000..6c606ce --- /dev/null +++ b/upcli/fedora_version.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +# FROM fedora:latest +cat /etc/os-release diff --git a/upcli/hello.sh b/upcli/hello.sh new file mode 100755 index 0000000..ca3b9f0 --- /dev/null +++ b/upcli/hello.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +echo "hello" diff --git a/upcli/poetry.lock b/upcli/poetry.lock index 7bd21bf..79ccb61 100644 --- a/upcli/poetry.lock +++ b/upcli/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. [[package]] name = "ruamel-yaml" diff --git a/upcli/up/main.py b/upcli/up/main.py index 3966cde..eeccbc3 100644 --- a/upcli/up/main.py +++ b/upcli/up/main.py @@ -35,7 +35,7 @@ def cli_main(): context = {"executable": executable} try: prompt_without_options = setup_options(prompt) - print(prompt_without_options) + # print(prompt_without_options) uplib.up_main(context, prompt_without_options) except Exception as e: log.error(e) diff --git a/uplib/poetry.lock b/uplib/poetry.lock index d6dffab..9be2eb9 100644 --- a/uplib/poetry.lock +++ b/uplib/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. [[package]] name = "atomicwrites" @@ -187,13 +187,13 @@ ssh = ["paramiko (>=2.4.3)"] [[package]] name = "dynaconf" -version = "3.2.4" +version = "3.2.5" description = "The dynamic configurator for your Python Project" optional = false python-versions = ">=3.8" files = [ - {file = "dynaconf-3.2.4-py2.py3-none-any.whl", hash = "sha256:858f9806fab2409c4f5442614c2605d4c4071d5e5153b0e7f24a225f27465aed"}, - {file = "dynaconf-3.2.4.tar.gz", hash = "sha256:2e6adebaa587f4df9241a16a4bec3fda521154d26b15f3258fde753a592831b6"}, + {file = "dynaconf-3.2.5-py2.py3-none-any.whl", hash = "sha256:12202fc26546851c05d4194c80bee00197e7c2febcb026e502b0863be9cbbdd8"}, + {file = "dynaconf-3.2.5.tar.gz", hash = "sha256:42c8d936b32332c4b84e4d4df6dd1626b6ef59c5a94eb60c10cd3c59d6b882f2"}, ] [package.dependencies] @@ -207,7 +207,7 @@ all = ["configobj", "hvac", "redis", "ruamel.yaml"] configobj = ["configobj"] ini = ["configobj"] redis = ["redis"] -test = ["configobj", "django", "flake8", "flake8-debugger", "flake8-print", "flake8-todo", "flask (>=0.12)", "hvac (>=1.1.0)", "pep8-naming", "pytest", "pytest-cov", "pytest-mock", "pytest-xdist", "python-dotenv", "radon", "redis", "toml"] +test = ["configobj", "django", "flask (>=0.12)", "hvac (>=1.1.0)", "pytest", "pytest-cov", "pytest-mock", "pytest-xdist", "python-dotenv", "radon", "redis", "toml"] toml = ["toml"] vault = ["hvac"] yaml = ["ruamel.yaml"] @@ -288,13 +288,13 @@ files = [ [[package]] name = "packaging" -version = "23.2" +version = "24.0" description = "Core utilities for Python packages" optional = false python-versions = ">=3.7" files = [ - {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"}, - {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, + {file = "packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5"}, + {file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"}, ] [[package]] @@ -387,13 +387,13 @@ files = [ [[package]] name = "redis" -version = "5.0.1" +version = "5.0.3" description = "Python client for Redis database and key-value store" optional = false python-versions = ">=3.7" files = [ - {file = "redis-5.0.1-py3-none-any.whl", hash = "sha256:ed4802971884ae19d640775ba3b03aa2e7bd5e8fb8dfaed2decce4d0fc48391f"}, - {file = "redis-5.0.1.tar.gz", hash = "sha256:0dab495cd5753069d3bc650a0dde8a8f9edde16fc5691b689a566eda58100d0f"}, + {file = "redis-5.0.3-py3-none-any.whl", hash = "sha256:5da9b8fe9e1254293756c16c008e8620b3d15fcc6dde6babde9541850e72a32d"}, + {file = "redis-5.0.3.tar.gz", hash = "sha256:4973bae7444c0fbed64a06b87446f79361cb7e4ec1538c022d696ed7a5015580"}, ] [package.extras] @@ -423,13 +423,13 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "rich" -version = "13.7.0" +version = "13.7.1" description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" optional = false python-versions = ">=3.7.0" files = [ - {file = "rich-13.7.0-py3-none-any.whl", hash = "sha256:6da14c108c4866ee9520bbffa71f6fe3962e193b7da68720583850cd4548e235"}, - {file = "rich-13.7.0.tar.gz", hash = "sha256:5cb5123b5cf9ee70584244246816e9114227e0b98ad9176eede6ad54bf5403fa"}, + {file = "rich-13.7.1-py3-none-any.whl", hash = "sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222"}, + {file = "rich-13.7.1.tar.gz", hash = "sha256:9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432"}, ] [package.dependencies] @@ -540,13 +540,13 @@ files = [ [[package]] name = "urllib3" -version = "2.2.0" +version = "2.2.1" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=3.8" files = [ - {file = "urllib3-2.2.0-py3-none-any.whl", hash = "sha256:ce3711610ddce217e6d113a2732fafad960a03fd0318c91faa79481e35c11224"}, - {file = "urllib3-2.2.0.tar.gz", hash = "sha256:051d961ad0c62a94e50ecf1af379c3aba230c66c710493493560c0c223c49f20"}, + {file = "urllib3-2.2.1-py3-none-any.whl", hash = "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d"}, + {file = "urllib3-2.2.1.tar.gz", hash = "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19"}, ] [package.extras] diff --git a/uplib/uplib/containers.py b/uplib/uplib/containers.py index f7d4d74..7deaca1 100644 --- a/uplib/uplib/containers.py +++ b/uplib/uplib/containers.py @@ -11,6 +11,8 @@ from datetime import datetime from . import settings_maps, options_map +import sys + # https://docker-py.readthedocs.io/en/stable/containers.html @dataclass class ContainerRun: @@ -35,8 +37,11 @@ class ContainerRun: class DockerContainers: @classmethod def volumes_of(cls, run: ContainerRun, prompt: str): - plugin_name = prompt.split()[0] - log.debug("loading volumes for plugin_name: %s", plugin_name) + plugin_name = "__NOT_FOUND__" + tokens = prompt.split() + if len(tokens): + plugin_name = tokens[0] + log.debug("loading volumes for prompt: %s", prompt) plugin_settings = settings_maps.get(plugin_name, {}) default_volumes = plugin_settings.get("volumes", {}) @@ -56,14 +61,15 @@ def volumes_of(cls, run: ContainerRun, prompt: str): } } options_volumes = options_map.get('volumes', {}) - print(type(run.volumes)) - print(run.volumes) result = run.volumes | settings_vols | default_vols | options_volumes return result @classmethod def ports_of(cls, prompt: str): - plugin_name = prompt.split()[0] + plugin_name = "__UNDEFINED__" + tokens = prompt.split() + if len(tokens): + plugin_name = tokens[0] options_ports = options_map.get('ports', {}) default_ports = settings_maps.get(plugin_name, {}).get("ports", {}) ports = default_ports.to_dict() if default_ports else {} @@ -89,9 +95,10 @@ def run(self, run: ContainerRun): # user = "up_user" user = run.user working_dir = run.working_dir - console = Console() - console.log(f"Running container: {name}") - console.log({ + up_console = Console(file=sys.stderr) + app_console = Console(file=sys.stdout) + up_console.log(f"Running container: {name}") + up_console.log({ "name": name, "image": run.image, "command": command, @@ -116,7 +123,7 @@ def run(self, run: ContainerRun): ) for line in container.logs(stream=True): line = line.decode("utf-8").strip() - console.print(escape(line)) + app_console.print(escape(line)) container.wait() log.debug("container ended.") @@ -124,7 +131,7 @@ def run(self, run: ContainerRun): log.error("Failed to run container") log.debug("%s", run) log.error("%s", e) - raise e + # raise e _container_name_pattern = '[^a-zA-Z0-9_.-]' def generate_container_name(run): diff --git a/uplib/uplib/logging.py b/uplib/uplib/logging.py index 94254cd..5a3844e 100644 --- a/uplib/uplib/logging.py +++ b/uplib/uplib/logging.py @@ -8,7 +8,7 @@ def init_logging(): sys.set_int_max_str_digits(999999) level = get_log_level() - print("Initializing logging with level "+str(level)) + # print("Initializing logging with level "+str(level)) logging.basicConfig( level=level ) diff --git a/uplib/uplib/main.py b/uplib/uplib/main.py index 669df9d..b4746be 100644 --- a/uplib/uplib/main.py +++ b/uplib/uplib/main.py @@ -1,11 +1,12 @@ import shlex +import os from . import pm, Context, Prompt from .containers import Containers, ContainerRun from .plugins import load_plugins from .logging import log from .config import Config - +from .parser import parse_doclets def up_main(context: Context, prompt: Prompt): log.info("*** %s", Config.welcome_message.get() + " ***") @@ -29,16 +30,92 @@ def up_main(context: Context, prompt: Prompt): def default_container(prompt): return ContainerRun( image=Config.default_image.get(), + working_dir="/tmp/up_cwd", command=prompt) +def is_exec(exec: str) -> bool: + file_exists = os.path.exists(exec) + is_exec = os.access(exec, os.X_OK) + result = file_exists and is_exec + return result + +def is_text_file(file_path, threshold=0.30): + """ + Checks if the given file is a text file. + + Args: + - file_path: Path to the file to be checked. + - threshold: The fraction of non-text characters at which a file is considered binary. + + Returns: + - True if the file is likely a text file, False otherwise. + """ + text_characters = bytearray({7, 8, 9, 10, 12, 13, 27} | set(range(0x20, 0x100)) - {0x7f}) + is_binary = lambda bytes: bool(bytes.translate(None, text_characters)) + + with open(file_path, 'rb') as file: + # Read up to 1024 bytes from the file + block = file.read(1024) + if not block: + # Empty file are considered text files + return True + if is_binary(block): + return False + + # Check the proportion of binary content in the block + num_binary = sum(is_binary(bytearray([b])) for b in block) + if num_binary / 1024 > threshold: + return False + + return True -def containers_for_prompt(prompt) -> list[ContainerRun]: - lists = containers_from_plugins(prompt) - flat = [item for sublist in lists for item in sublist] - return flat + +def containers_for_prompt(prompt: list[str]) -> list[ContainerRun]: + result = [] + if (len(prompt) == 0): + return result + head = prompt[0] + if (is_exec(head)): + result= containers_from_exec(prompt) + else: + lists = containers_from_plugins(prompt) + result = [item for sublist in lists for item in sublist] + return result def containers_from_plugins(prompt: list[str]) -> list[ContainerRun]: result = pm.hook.containers_for_prompt(prompt=prompt) if not result: return [] return result + +def containers_from_exec(prompt: list[str]) -> list[ContainerRun]: + result = [] + head = prompt[0] + if (is_text_file(head)): + # log.error("CONTAINERS FROM TEXT "+str(prompt)) + doclets = parse_doclets(head) + # log.error(str(doclets)) + run = ContainerRun() + run.command = prompt + run.working_dir = "/tmp/up_cwd/" + # iterate on doclets and set the run properties + for key in doclets: + if key == "FROM": + run.image = doclets[key] + if key == "CMD": + run.command = doclets[key] + if key == "WORKDIR": + run.working_dir = doclets[key] + if key == "ENV": + run.environment = doclets[key] + if key == "VOLUME": + run.volumes = doclets[key] + if key == "PORT": + run.ports = doclets[key] + if key == "USER": + run.user = doclets[key] + if key == "ENTRYPOINT": + run.command = doclets[key] + + result = [run] + return result \ No newline at end of file diff --git a/uplib/uplib/parser.py b/uplib/uplib/parser.py new file mode 100644 index 0000000..d082868 --- /dev/null +++ b/uplib/uplib/parser.py @@ -0,0 +1,42 @@ +def parse_doclets(file_path): + # Define the list of Docker keywords + docker_keywords = [ + "FROM", "LABEL", "RUN", "CMD", "EXPOSE", "ENV", "ADD", "COPY", "ENTRYPOINT", + "VOLUME", "USER", "WORKDIR", "ARG", "ONBUILD", "STOPSIGNAL", "HEALTHCHECK", "SHELL" + ] + + # Define the list of comment delimiters + comment_delimiters = ['#', '//', '*', '/*'] + + # Initialize an empty dictionary to hold the results + docker_declarations = {} + + # Open the file and read it line by line + with open(file_path, 'r') as file: + for line in file: + # Check if the line starts with any of the comment delimiters + if any(line.lstrip().startswith(delimiter) for delimiter in comment_delimiters): + # Remove the comment delimiter and leading/trailing whitespace + uncommented_line = line.lstrip().lstrip(''.join(comment_delimiters)).strip() + tokens = uncommented_line.split() + # Split the uncommented line into words and get the first word + head = tokens[0] if tokens else None + # tail is the rest of line without first workd + tail = ' '.join(tokens[1:]) + # Check if the first word is a Docker keyword + + if head in docker_keywords: + # Add the uncommented line to the dictionary, keyed by the Docker keyword + # This allows for multiple comments for the same keyword to be concatenated + + docker_declarations.setdefault(head, []).append(tail) + + # Convert lists of lines back to single string entries for each keyword + for key in docker_declarations: + docker_declarations[key] = '\n'.join(docker_declarations[key]) + + return docker_declarations + +# Example usage (you would replace 'example.txt' with your actual file path): +# docker_declarations = parse_docker_comments('example.txt') +# print(docker_declarations)