diff --git a/src/kiwi/parser.py b/src/kiwi/parser.py index 01c869f..a54bb05 100644 --- a/src/kiwi/parser.py +++ b/src/kiwi/parser.py @@ -46,7 +46,8 @@ class Parser: def get_args(self): if self.__args is None: # parse args if needed - self.__args = self.__parser.parse_args() + self.__args, unknowns = self.__parser.parse_known_args() + self.__args.unknowns = unknowns return self.__args diff --git a/src/kiwi/runner.py b/src/kiwi/runner.py index d84cca1..026f25b 100644 --- a/src/kiwi/runner.py +++ b/src/kiwi/runner.py @@ -3,7 +3,6 @@ import logging # local from . import subcommands -from .config import LoadedConfig from .parser import Parser @@ -13,7 +12,6 @@ class Runner: class __Runner: """Singleton type""" - __parser = None __commands = [] def __init__(self): @@ -22,10 +20,11 @@ class Runner: cmd = getattr(subcommands, className) self.__commands.append(cmd()) - def run(self, command=None): + def run(self, command=None, args=None): """run the desired subcommand""" - args = Parser().get_args() + if args is None: + args = Parser().get_args() if command is None: command = args.command @@ -36,7 +35,7 @@ class Runner: logging.debug(f"Running '{cmd}' with args: {args}") try: - result = cmd.run(self, LoadedConfig.get(), args) + result = cmd.run(self, args) except KeyboardInterrupt: print() diff --git a/src/kiwi/subcommands/_subcommand.py b/src/kiwi/subcommands/_subcommand.py index 34c6aae..9566443 100644 --- a/src/kiwi/subcommands/_subcommand.py +++ b/src/kiwi/subcommands/_subcommand.py @@ -1,5 +1,6 @@ # system import logging +import os # local from .utils.project import Projects @@ -16,7 +17,9 @@ class SubCommand: # command parser _sub_parser = None - def __init__(self, name, add_parser=True, **kwargs): + _action = None + + def __init__(self, name, action='', add_parser=True, **kwargs): self.__name = name if add_parser: self._sub_parser = Parser().get_subparsers().add_parser( @@ -24,26 +27,38 @@ class SubCommand: **kwargs ) + if not action: + # default action string + self._action = f"Running '{str(self)}' for" + else: + self._action = action + def __str__(self): return self.__name - def run(self, runner, config, args): - """actually run command with this dir's config and parsed CLI args""" + def _run_instance(self, runner, args): pass + def run(self, runner, args): + """actually run command with parsed CLI args""" + + # run for entire instance + logging.info(f"{self._action} kiwi-config instance at '{os.getcwd()}'") + return self._run_instance(runner, args) + class ProjectCommand(SubCommand): """this command concerns a project in current instance""" - def __init__(self, name, num_projects, add_parser=True, **kwargs): + def __init__(self, name, num_projects, action='', add_parser=True, **kwargs): super().__init__( - name, add_parser=add_parser, + name, action=action, add_parser=add_parser, **kwargs ) - projects = "a project" - - if not num_projects == 1: + if num_projects == 1: + projects = "a project" + else: projects = "project(s)" self._sub_parser.add_argument( @@ -51,19 +66,41 @@ class ProjectCommand(SubCommand): help=f"select {projects} in this instance" ) + def _run_instance(self, runner, args): + # default: run for all enabled projects + return self._run_projects(runner, args, Projects.from_dir().filter_enabled()) + + def _run_projects(self, runner, args, projects): + pass + + def run(self, runner, args): + projects = Projects.from_args(args) + + if not projects.empty(): + # project(s) given + logging.info(f"{self._action} projects {projects}") + return self._run_projects(runner, args, projects) + + else: + return super().run(runner, args) + class ServiceCommand(ProjectCommand): """this command concerns service(s) in a project""" - def __init__(self, name, num_projects, num_services, add_parser=True, **kwargs): + def __init__(self, name, num_projects, num_services, action='', add_parser=True, **kwargs): super().__init__( - name, num_projects=num_projects, add_parser=add_parser, + name, num_projects=num_projects, action=action, add_parser=add_parser, **kwargs ) - services = "a service" + if (isinstance(num_projects, str) and num_projects == '*') \ + or (isinstance(num_projects, int) and num_projects > 1): + logging.warning(f"Invalid choice for project count: {num_projects}") - if not num_services == 1: + if num_services == 1: + services = "a service" + else: services = "service(s)" self._sub_parser.add_argument( @@ -71,52 +108,25 @@ class ServiceCommand(ProjectCommand): help=f"select {services} in a project" ) - -class FlexCommand(ServiceCommand): - """this command concerns the entire instance, a whole project or just service(s) in a project""" - - __action = None - - def __init__(self, name, action='', add_parser=True, **kwargs): - super().__init__( - name, num_projects='?', num_services='*', add_parser=add_parser, - **kwargs - ) - - if not action: - # default action string - self.__action = f"Running '{str(self)}' for" - else: - self.__action = action - - def _run_instance(self, runner, config, args): + def _run_projects(self, runner, args, projects): result = True - for project in Projects.from_args(args): - args.projects = project.get_name() - result &= runner.run(str(self)) + # default: run without services for all given + for project in projects: + result &= self._run_services(runner, args, project, []) return result - def _run_project(self, runner, config, args): - return self._run_services(runner, config, args, []) - - def _run_services(self, runner, config, args, services): + def _run_services(self, runner, args, project, services): pass - def run(self, runner, config, args): - projects = Projects.from_args(args) - if not projects: - # no project given, run for entire instance - logging.info(f"{self.__action} this instance") - return self._run_instance(runner, config, args) + def run(self, runner, args): + if 'services' in args and args.services: + project = Projects.from_args(args)[0] - project = projects[0] - if args is None or 'services' not in args or not args.services: - # no services given, run for whole project - logging.info(f"{self.__action} project '{project.get_name()}'") - return self._run_project(runner, config, args) + # run for service(s) inside project + logging.info(f"{self._action} project '{project.get_name()}', services {args.services}") + return self._run_services(runner, args, project, args.services) - # run for service(s) inside project - logging.info(f"{self.__action} services {args.services} in project '{project.get_name()}'") - return self._run_services(runner, config, args, args.services) + else: + return super().run(runner, args) diff --git a/src/kiwi/subcommands/build.py b/src/kiwi/subcommands/build.py index d53c50d..5676c17 100644 --- a/src/kiwi/subcommands/build.py +++ b/src/kiwi/subcommands/build.py @@ -1,19 +1,20 @@ # local -from ._subcommand import FlexCommand +from ._subcommand import ServiceCommand from .utils.dockercommand import DockerCommand -class BuildCommand(FlexCommand): +class BuildCommand(ServiceCommand): """kiwi build""" def __init__(self): super().__init__( - 'build', "Building images for", + 'build', num_projects='?', num_services='*', + action="Building images for", description="Build images for the whole instance, a project or service(s) inside a project" ) - def _run_services(self, runner, config, args, services): - DockerCommand('docker-compose').run( - config, args, ['build', '--pull', *services] - ) + def _run_services(self, runner, args, project, services): + DockerCommand('docker-compose').run(project, [ + 'build', '--pull', *services + ]) return True diff --git a/src/kiwi/subcommands/cmd.py b/src/kiwi/subcommands/cmd.py index 3182f11..f08c6dc 100644 --- a/src/kiwi/subcommands/cmd.py +++ b/src/kiwi/subcommands/cmd.py @@ -1,3 +1,6 @@ +# system +import logging + # local from ._subcommand import ProjectCommand from .utils.dockercommand import DockerCommand @@ -9,19 +12,32 @@ class CmdCommand(ProjectCommand): def __init__(self): super().__init__( 'cmd', num_projects=1, + action="Running docker-compose in", description="Run raw docker-compose command in a project" ) - # command string after docker-compose + # command for docker-compose self._sub_parser.add_argument( 'compose_cmd', metavar='cmd', type=str, - help="runs `docker-compose `" + help="command for 'docker-compose'" ) - def run(self, runner, config, args): - import shlex + # arguments for docker-compose command + self._sub_parser.add_argument( + 'compose_args', metavar='arg', nargs='*', type=str, + help="arguments for 'docker-compose' commands" + ) + + def _run_projects(self, runner, args, projects): + if args.unknowns: + args.compose_args = [*args.compose_args, *args.unknowns] + args.unknowns = [] + + logging.debug(f"Updated args: {args}") # run with split compose_cmd argument - DockerCommand('docker-compose').run(config, args, shlex.split(args.compose_cmd)) + DockerCommand('docker-compose').run(projects[0], [ + args.compose_cmd, *args.compose_args + ]) return True diff --git a/src/kiwi/subcommands/conf.py b/src/kiwi/subcommands/conf.py index 7656f07..b3d511f 100644 --- a/src/kiwi/subcommands/conf.py +++ b/src/kiwi/subcommands/conf.py @@ -10,6 +10,7 @@ from .utils.rootkit import Rootkit, prefix_path_mnt # parent from .._constants import CONF_DIRECTORY_NAME +from ..config import LoadedConfig class ConfCopyCommand(SubCommand): @@ -21,22 +22,20 @@ class ConfCopyCommand(SubCommand): description="Synchronize all config files to target directory" ) - def run(self, runner, config, args): + def _run_instance(self, runner, args): conf_dirs = [ project.conf_dir_name() - for project in Projects.all() - if project.is_enabled() + for project in Projects.from_dir().filter_enabled() ] if conf_dirs: # add target directory - conf_dirs.append(config['runtime:storage']) + conf_dirs.append(LoadedConfig.get()['runtime:storage']) logging.info(f"Sync directories: {conf_dirs}") - Rootkit('rsync').run( - config, args, ['rsync', '-r', *prefix_path_mnt(conf_dirs)], - stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL - ) + Rootkit('rsync').run([ + 'rsync', '-r', *prefix_path_mnt(conf_dirs) + ], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) return True @@ -50,14 +49,13 @@ class ConfPurgeCommand(SubCommand): description="Remove all config files in target directory" ) - def run(self, runner, config, args): - conf_target = f"{config['runtime:storage']}/{CONF_DIRECTORY_NAME}" + def _run_instance(self, runner, args): + conf_target = f"{LoadedConfig.get()['runtime:storage']}/{CONF_DIRECTORY_NAME}" logging.info(f"Purging directories: {conf_target}") - Rootkit().run( - config, args, ['rm', '-rf', prefix_path_mnt(conf_target)], - stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL - ) + Rootkit().run([ + 'rm', '-rf', prefix_path_mnt(conf_target) + ], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) return True @@ -71,12 +69,12 @@ class ConfCleanCommand(SubCommand): description="Cleanly sync all configs to target folder, relaunch affected projects" ) - def run(self, runner, config, args): + def _run_instance(self, runner, args): result = True affected_projects = [ project.conf_dir_name() - for project in Projects.all() + for project in Projects.from_dir() if project.has_configs() ] @@ -93,4 +91,4 @@ class ConfCleanCommand(SubCommand): args.projects = project_name result &= runner.run('up') - return result \ No newline at end of file + return result diff --git a/src/kiwi/subcommands/disable.py b/src/kiwi/subcommands/disable.py index 29b4f88..f2e9362 100644 --- a/src/kiwi/subcommands/disable.py +++ b/src/kiwi/subcommands/disable.py @@ -9,11 +9,12 @@ class DisableCommand(ProjectCommand): def __init__(self): super().__init__( 'disable', num_projects='+', + action="Disabling", description="Disable whole project(s) in this instance" ) - def run(self, runner, config, args): + def _run_projects(self, runner, args, projects): return all([ project.disable() - for project in Projects.from_args(args) + for project in projects ]) diff --git a/src/kiwi/subcommands/down.py b/src/kiwi/subcommands/down.py index ebb4a4c..286f65f 100644 --- a/src/kiwi/subcommands/down.py +++ b/src/kiwi/subcommands/down.py @@ -1,40 +1,42 @@ # local -from ._subcommand import FlexCommand +from ._subcommand import ServiceCommand from .utils.dockercommand import DockerCommand from .utils.misc import are_you_sure -class DownCommand(FlexCommand): +class DownCommand(ServiceCommand): """kiwi down""" def __init__(self): super().__init__( - 'down', "Bringing down", + 'down', num_projects='?', num_services='*', + action="Bringing down", description="Bring down the whole instance, a project or service(s) inside a project" ) - def _run_instance(self, runner, config, args): + def _run_instance(self, runner, args): if are_you_sure([ "This will bring down the entire instance.", "", "This may not be what you intended, because:", " - Bringing down the instance stops ALL services in here", ]): - return super()._run_instance(runner, config, args) + return super()._run_instance(runner, args) return False - def _run_project(self, runner, config, args): - DockerCommand('docker-compose').run( - config, args, ['down'] - ) + def _run_projects(self, runner, args, projects): + for project in projects: + DockerCommand('docker-compose').run(project, [ + 'down' + ]) return True - def _run_services(self, runner, config, args, services): - DockerCommand('docker-compose').run( - config, args, ['stop', *services] - ) - DockerCommand('docker-compose').run( - config, args, ['rm', '-f', *services] - ) + def _run_services(self, runner, args, project, services): + DockerCommand('docker-compose').run(project, [ + 'stop', *services + ]) + DockerCommand('docker-compose').run(project, [ + 'rm', '-f', *services + ]) return True diff --git a/src/kiwi/subcommands/enable.py b/src/kiwi/subcommands/enable.py index 9392b95..9432bdf 100644 --- a/src/kiwi/subcommands/enable.py +++ b/src/kiwi/subcommands/enable.py @@ -9,11 +9,12 @@ class EnableCommand(ProjectCommand): def __init__(self): super().__init__( 'enable', num_projects='+', + action="Enabling", description="Enable whole project(s) in this instance" ) - def run(self, runner, config, args): + def _run_projects(self, runner, args, projects): return all([ project.enable() - for project in Projects.from_args(args) + for project in projects ]) diff --git a/src/kiwi/subcommands/init.py b/src/kiwi/subcommands/init.py index a637c79..53ed9e3 100644 --- a/src/kiwi/subcommands/init.py +++ b/src/kiwi/subcommands/init.py @@ -7,6 +7,7 @@ from ._subcommand import SubCommand # parent (display purposes only) from .._constants import KIWI_CONF_NAME +from ..config import DefaultConfig, LoadedConfig def user_input(config, key, prompt): @@ -40,12 +41,12 @@ class InitCommand(SubCommand): help=f"use default values even if {KIWI_CONF_NAME} is present" ) - def run(self, runner, config, args): + def _run_instance(self, runner, args): logging.info(f"Initializing '{KIWI_CONF_NAME}' in '{os.getcwd()}'") + config = LoadedConfig.get() # check force switch if args.force and os.path.isfile(KIWI_CONF_NAME): - from ..config import DefaultConfig logging.warning(f"Overwriting existing '{KIWI_CONF_NAME}'!") config = DefaultConfig.get() diff --git a/src/kiwi/subcommands/list.py b/src/kiwi/subcommands/list.py index 6e4cd9f..df8068c 100644 --- a/src/kiwi/subcommands/list.py +++ b/src/kiwi/subcommands/list.py @@ -1,84 +1,75 @@ # system import logging import os -import subprocess import yaml # local -from ._subcommand import FlexCommand -from .utils.dockercommand import DockerCommand -from .utils.project import Projects +from ._subcommand import ServiceCommand +from .utils.project import Project, Projects def _print_list(strings): - if isinstance(strings, list): + if isinstance(strings, str): + print(f" - {strings}") + + elif isinstance(strings, Project): + _print_list(strings.get_name()) + + elif isinstance(strings, list): for string in strings: - print(f" - {string}") + _print_list(string) - elif isinstance(strings, str): - _print_list(strings.strip().split('\n')) - - elif isinstance(strings, bytes): - _print_list(str(strings, 'utf-8')) + else: + _print_list(list(strings)) -class ListCommand(FlexCommand): +class ListCommand(ServiceCommand): """kiwi list""" def __init__(self): super().__init__( - 'list', "Listing", + 'list', num_projects='?', num_services='*', + action="Listing", description="List projects in this instance, services inside a project or service(s) inside a project" ) - def _run_instance(self, runner, config, args): + def _run_instance(self, runner, args): print(f"kiwi-config instance at '{os.getcwd()}'") print("#########") - projects = Projects.all() + projects = Projects.from_dir() - enableds = [ - project.get_name() - for project in projects - if project.is_enabled() - ] - - if enableds: + enabled_projects = projects.filter_enabled() + if not enabled_projects.empty(): print(f"Enabled projects:") - _print_list(enableds) + _print_list(enabled_projects) - disableds = [ - project.get_name() - for project in projects - if project.is_disabled() - ] - - if disableds: + disabled_projects = projects.filter_disabled() + if not disabled_projects.empty(): print(f"Disabled projects:") - _print_list(disableds) + _print_list(disabled_projects) return True - def _run_project(self, runner, config, args): - project = Projects.from_args(args)[0] - + def _run_projects(self, runner, args, projects): + project = projects[0] if not project.exists(): - logging.error(f"Project '{project.get_name()}' not found") + logging.warning(f"Project '{project.get_name()}' not found") return False print(f"Services in project '{project.get_name()}':") print("#########") - ps = DockerCommand('docker-compose').run( - config, args, ['config', '--services'], - stdout=subprocess.PIPE - ) + with open(project.compose_file_name(), 'r') as stream: + try: + docker_compose_yml = yaml.safe_load(stream) + _print_list(docker_compose_yml['services'].keys()) + + except yaml.YAMLError as exc: + logging.error(exc) - _print_list(ps.stdout) return True - def _run_services(self, runner, config, args, services): - project = Projects.from_args(args)[0] - + def _run_services(self, runner, args, project, services): if not project.exists(): logging.error(f"Project '{project.get_name()}' not found") return False @@ -91,10 +82,13 @@ class ListCommand(FlexCommand): docker_compose_yml = yaml.safe_load(stream) for service_name in services: - print(yaml.dump( - {service_name: docker_compose_yml['services'][service_name]}, - default_flow_style=False, sort_keys=False - ).strip()) + try: + print(yaml.dump( + {service_name: docker_compose_yml['services'][service_name]}, + default_flow_style=False, sort_keys=False + ).strip()) + except KeyError: + logging.error(f"Service '{service_name}' not found") return True diff --git a/src/kiwi/subcommands/logs.py b/src/kiwi/subcommands/logs.py index a60122f..280a966 100644 --- a/src/kiwi/subcommands/logs.py +++ b/src/kiwi/subcommands/logs.py @@ -9,6 +9,7 @@ class LogsCommand(ServiceCommand): def __init__(self): super().__init__( 'logs', num_projects=1, num_services='*', + action="Showing logs of", description="Show logs of a project or service(s) of a project" ) @@ -18,7 +19,7 @@ class LogsCommand(ServiceCommand): help="output appended data as log grows" ) - def run(self, runner, config, args): + def _run_services(self, runner, args, project, services): # include timestamps compose_cmd = ['logs', '-t'] @@ -27,13 +28,13 @@ class LogsCommand(ServiceCommand): compose_cmd = [*compose_cmd, '-f', '--tail=10'] # append if one or more services are given - if args.services: + if services: compose_cmd = [*compose_cmd, *args.services] # use 'less' viewer if output will be static if args.follow: - DockerCommand('docker-compose').run(config, args, compose_cmd) + DockerCommand('docker-compose').run(project, compose_cmd) else: - DockerCommand('docker-compose').run_less(config, args, compose_cmd) + DockerCommand('docker-compose').run_less(project, compose_cmd) return True diff --git a/src/kiwi/subcommands/net.py b/src/kiwi/subcommands/net.py index 6c007f8..e83f513 100644 --- a/src/kiwi/subcommands/net.py +++ b/src/kiwi/subcommands/net.py @@ -7,16 +7,18 @@ from ._subcommand import SubCommand from .utils.dockercommand import DockerCommand from .utils.misc import are_you_sure +# parent +from ..config import LoadedConfig -def _find_net(config, args): - ps = DockerCommand('docker').run( - config, args, ['network', 'ls', '--filter', f"name={config['network:name']}", '--format', '{{.Name}}'], - stdout=subprocess.PIPE - ) + +def _find_net(net_name): + ps = DockerCommand('docker').run(None, [ + 'network', 'ls', '--filter', f"name={net_name}", '--format', '{{.Name}}' + ], stdout=subprocess.PIPE) net_found = str(ps.stdout, 'utf-8').strip() - return net_found == config['network:name'] + return net_found == net_name class NetUpCommand(SubCommand): @@ -25,30 +27,31 @@ class NetUpCommand(SubCommand): def __init__(self): super().__init__( 'net-up', + action="Creating the local network hub", description="Create the local network hub for this instance" ) - def run(self, runner, config, args): - if _find_net(config, args): - logging.info(f"Network '{config['network:name']}' already exists") + def _run_instance(self, runner, args): + config = LoadedConfig.get() + net_name = config['network:name'] + net_cidr = config['network:cidr'] + + if _find_net(net_name): + logging.info(f"Network '{net_name}' already exists") return True try: - DockerCommand('docker').run( - config, args, - [ - 'network', 'create', - '--driver', 'bridge', - '--internal', - '--subnet', config['network:cidr'], - config['network:name'] - ], - check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL - ) - logging.info(f"Network '{config['network:name']}' created") + DockerCommand('docker').run(None, [ + 'network', 'create', + '--driver', 'bridge', + '--internal', + '--subnet', net_cidr, + net_name + ], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + logging.info(f"Network '{net_name}' created") except subprocess.CalledProcessError: - logging.error(f"Error creating network '{config['network:name']}'") + logging.error(f"Error creating network '{net_name}'") return False return True @@ -63,25 +66,26 @@ class NetDownCommand(SubCommand): description="Remove the local network hub for this instance" ) - def run(self, runner, config, args): - if not _find_net(config, args): - logging.info(f"Network '{config['network:name']}' does not exist") + def _run_instance(self, runner, args): + net_name = LoadedConfig.get()['network:name'] + + if not _find_net(net_name): + logging.info(f"Network '{net_name}' does not exist") return True try: if are_you_sure("This will bring down this instance's hub network!"): if runner.run('down'): - DockerCommand('docker').run( - config, args, - ['network', 'rm', config['network:name']], - check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL - ) - logging.info(f"Network '{config['network:name']}' removed") + DockerCommand('docker').run(None, [ + 'network', 'rm', net_name + ], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + + logging.info(f"Network '{net_name}' removed") else: return False except subprocess.CalledProcessError: - logging.error(f"Error removing network '{config['network:name']}'") + logging.error(f"Error removing network '{net_name}'") return False return True diff --git a/src/kiwi/subcommands/new.py b/src/kiwi/subcommands/new.py index 913f134..e92142c 100644 --- a/src/kiwi/subcommands/new.py +++ b/src/kiwi/subcommands/new.py @@ -17,12 +17,12 @@ class NewCommand(ProjectCommand): def __init__(self): super().__init__( 'new', num_projects='+', + action="Creating", description="Create new empty project(s) in this instance" ) - def run(self, runner, config, args): + def _run_projects(self, runner, args, projects): result = True - projects = Projects.from_args(args) for project in projects: if project.exists(): @@ -31,7 +31,7 @@ class NewCommand(ProjectCommand): else: logging.info(f"Creating project '{project.get_name()}'") - os.mkdir(project.enabled_dir_name()) + os.mkdir(project.disabled_dir_name()) shutil.copy(DEFAULT_DOCKER_COMPOSE_NAME, project.compose_file_name()) return result diff --git a/src/kiwi/subcommands/pull.py b/src/kiwi/subcommands/pull.py index 019c007..debe467 100644 --- a/src/kiwi/subcommands/pull.py +++ b/src/kiwi/subcommands/pull.py @@ -1,19 +1,20 @@ # local -from ._subcommand import FlexCommand +from ._subcommand import ServiceCommand from .utils.dockercommand import DockerCommand -class PullCommand(FlexCommand): +class PullCommand(ServiceCommand): """kiwi pull""" def __init__(self): super().__init__( - 'pull', "Pulling images for", + 'pull', num_projects='?', num_services='*', + action="Pulling images for", description="Pull images for the whole instance, a project or service(s) inside a project" ) - def _run_services(self, runner, config, args, services): - DockerCommand('docker-compose').run( - config, args, ['pull', '--ignore-pull-failures', *services] - ) + def _run_services(self, runner, args, project, services): + DockerCommand('docker-compose').run(project, [ + 'pull', '--ignore-pull-failures', *services + ]) return True diff --git a/src/kiwi/subcommands/push.py b/src/kiwi/subcommands/push.py index f0b00ad..b026b1b 100644 --- a/src/kiwi/subcommands/push.py +++ b/src/kiwi/subcommands/push.py @@ -1,19 +1,20 @@ # local -from ._subcommand import FlexCommand +from ._subcommand import ServiceCommand from .utils.dockercommand import DockerCommand -class PushCommand(FlexCommand): +class PushCommand(ServiceCommand): """kiwi push""" def __init__(self): super().__init__( - 'push', "Pushing images for", + 'push', num_projects='?', num_services='*', + action="Pushing images for", description="Push images for the whole instance, a project or service(s) inside a project" ) - def _run_services(self, runner, config, args, services): - DockerCommand('docker-compose').run( - config, args, ['push', *services] - ) + def _run_services(self, runner, args, project, services): + DockerCommand('docker-compose').run(project, [ + 'push', *services + ]) return True diff --git a/src/kiwi/subcommands/sh.py b/src/kiwi/subcommands/sh.py index 7e3d31e..c945d42 100644 --- a/src/kiwi/subcommands/sh.py +++ b/src/kiwi/subcommands/sh.py @@ -6,19 +6,21 @@ import subprocess from ._subcommand import ServiceCommand from .utils.dockercommand import DockerCommand +# parent +from ..config import LoadedConfig -def _service_has_executable(config, args, compose_cmd, exe_name): + +def _service_has_executable(project, service, exe_name): """ - Test if container (as of compose_cmd array) has an executable exe_name in its PATH. + Test if service in project has an executable exe_name in its PATH. Requires /bin/sh and which. """ try: # test if desired shell exists - DockerCommand('docker-compose').run( - config, args, [*compose_cmd, '/bin/sh', '-c', f"which {exe_name}"], - check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL - ) + DockerCommand('docker-compose').run(project, [ + 'exec', service, '/bin/sh', '-c', f"which {exe_name}" + ], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) return True except subprocess.CalledProcessError as e: @@ -26,13 +28,14 @@ def _service_has_executable(config, args, compose_cmd, exe_name): return False -def _find_shell(config, args, compose_cmd): - """find first working shell (provided by config and args) in container (as of compose_cmd array)""" +def _find_shell(args, project, service): + """find first working shell (provided by config and args) in service in project""" # builtin shells: as a last resort, fallback to '/bin/sh' and 'sh' shells = ['/bin/sh', 'sh'] # load favorite shells from config + config = LoadedConfig.get() if config['runtime:shells']: shells = [*config['runtime:shells'], *shells] @@ -44,7 +47,7 @@ def _find_shell(config, args, compose_cmd): # actually try shells for i, shell in enumerate(shells): - if _service_has_executable(config, args, compose_cmd, shell): + if _service_has_executable(project, service, shell): # found working shell logging.debug(f"Using shell '{shell}'") return shell @@ -82,15 +85,15 @@ class ShCommand(ServiceCommand): help="shell to spawn" ) - def run(self, runner, config, args): - compose_cmd = ['exec', args.services[0]] - shell = _find_shell(config, args, compose_cmd) + def _run_services(self, runner, args, project, services): + service = services[0] + shell = _find_shell(args, project, service) if shell is not None: # spawn shell - DockerCommand('docker-compose').run( - config, args, [*compose_cmd, shell] - ) + DockerCommand('docker-compose').run(project, [ + 'exec', service, shell + ]) return True return False diff --git a/src/kiwi/subcommands/show.py b/src/kiwi/subcommands/show.py index 4448734..99834df 100644 --- a/src/kiwi/subcommands/show.py +++ b/src/kiwi/subcommands/show.py @@ -1,6 +1,9 @@ # local from ._subcommand import SubCommand +# parent +from ..config import LoadedConfig + class ShowCommand(SubCommand): """kiwi show""" @@ -11,6 +14,6 @@ class ShowCommand(SubCommand): description="Show effective kiwi.yml" ) - def run(self, runner, config, args): - print(config) + def _run_instance(self, runner, args): + print(LoadedConfig.get()) return True diff --git a/src/kiwi/subcommands/up.py b/src/kiwi/subcommands/up.py index 0f958cd..ae4261d 100644 --- a/src/kiwi/subcommands/up.py +++ b/src/kiwi/subcommands/up.py @@ -1,28 +1,29 @@ # local -from ._subcommand import FlexCommand +from ._subcommand import ServiceCommand from .utils.dockercommand import DockerCommand -class UpCommand(FlexCommand): +class UpCommand(ServiceCommand): """kiwi up""" def __init__(self): super().__init__( - 'up', "Bringing up", + 'up', num_projects='?', num_services='*', + action="Bringing up", description="Bring up the whole instance, a project or service(s) inside a project" ) - def _run_instance(self, runner, config, args): + def _run_instance(self, runner, args): if runner.run('conf-copy'): - return super()._run_instance(runner, config, args) + return super()._run_instance(runner, args) return False - def _run_services(self, runner, config, args, services): + def _run_services(self, runner, args, project, services): if runner.run('net-up'): - DockerCommand('docker-compose').run( - config, args, ['up', '-d', *services] - ) + DockerCommand('docker-compose').run(project, [ + 'up', '-d', *services + ]) return True return False diff --git a/src/kiwi/subcommands/update.py b/src/kiwi/subcommands/update.py index cfac6fe..ccb58a3 100644 --- a/src/kiwi/subcommands/update.py +++ b/src/kiwi/subcommands/update.py @@ -1,18 +1,19 @@ # local -from ._subcommand import FlexCommand +from ._subcommand import ServiceCommand from .utils.misc import are_you_sure -class UpdateCommand(FlexCommand): +class UpdateCommand(ServiceCommand): """kiwi update""" def __init__(self): super().__init__( - 'update', "Updating", + 'update', num_projects='?', num_services='*', + action="Updating", description="Update the whole instance, a project or service(s) inside a project" ) - def _run_instance(self, runner, config, args): + def _run_instance(self, runner, args): if are_you_sure([ "This will update the entire instance at once.", "", @@ -20,12 +21,14 @@ class UpdateCommand(FlexCommand): " - Updates may take a long time", " - Updates may break beloved functionality", ]): - return super()._run_instance(runner, config, args) + return super()._run_instance(runner, args) return False - def _run_services(self, runner, config, args, services): - result = runner.run('build') + def _run_services(self, runner, args, project, services): + result = True + + result &= runner.run('build') result &= runner.run('pull') result &= runner.run('conf-copy') result &= runner.run('down') diff --git a/src/kiwi/subcommands/utils/dockercommand.py b/src/kiwi/subcommands/utils/dockercommand.py index 5863f1d..ea2cc2f 100644 --- a/src/kiwi/subcommands/utils/dockercommand.py +++ b/src/kiwi/subcommands/utils/dockercommand.py @@ -5,21 +5,16 @@ import subprocess # local from .executable import Executable -from .project import Projects # parent from ..._constants import CONF_DIRECTORY_NAME -from ...parser import Parser from ...config import LoadedConfig -def _update_kwargs(**kwargs): - config = LoadedConfig.get() - projects = Projects.from_args(Parser().get_args()) - - # project given in args: command affects a project in this instance - if projects: - project = projects[0] +def _update_kwargs(project, **kwargs): + # enabled project given: command affects a project in this instance + if project is not None and project.is_enabled(): + config = LoadedConfig.get() # execute command in project directory kwargs['cwd'] = project.dir_name() @@ -58,19 +53,15 @@ class DockerCommand(Executable): except subprocess.CalledProcessError: raise PermissionError("Cannot access docker, please get into the docker group or run as root!") - def run(self, config, args, process_args, **kwargs): - kwargs = _update_kwargs(**kwargs) - + def run(self, project, process_args, **kwargs): # equivalent to 'super().run' but agnostic of nested class construct return super().__getattr__("run")( - process_args, config, - **kwargs + process_args, + **_update_kwargs(project, **kwargs) ) - def run_less(self, config, args, process_args, **kwargs): - kwargs = _update_kwargs(**kwargs) - + def run_less(self, project, process_args, **kwargs): return super().__getattr__("run_less")( - process_args, config, - **kwargs + process_args, + **_update_kwargs(project, **kwargs) ) diff --git a/src/kiwi/subcommands/utils/executable.py b/src/kiwi/subcommands/utils/executable.py index 4b7fecb..7f2a537 100644 --- a/src/kiwi/subcommands/utils/executable.py +++ b/src/kiwi/subcommands/utils/executable.py @@ -3,18 +3,22 @@ import logging import os import subprocess +# parent +from ...config import LoadedConfig -def _update_kwargs(config, **kwargs): - if config is not None: - # ensure there is an environment - if 'env' not in kwargs: - kwargs['env'] = {} - # add common environment from config - if config['runtime:env'] is not None: - kwargs['env'].update(config['runtime:env']) +def _update_kwargs(**kwargs): + config = LoadedConfig.get() - logging.debug(f"kwargs updated: {kwargs}") + # ensure there is an environment + if 'env' not in kwargs: + kwargs['env'] = {} + + # add common environment from config + if config['runtime:env'] is not None: + kwargs['env'].update(config['runtime:env']) + + logging.debug(f"kwargs updated: {kwargs}") return kwargs @@ -48,28 +52,24 @@ class Executable: logging.debug(f"Executable cmd{cmd}, kwargs{kwargs}") return cmd - def run(self, process_args, config=None, **kwargs): - kwargs = _update_kwargs(config, **kwargs) - + def run(self, process_args, **kwargs): return subprocess.run( - self.__build_cmd(process_args, **kwargs), + self.__build_cmd(process_args, **_update_kwargs(**kwargs)), **kwargs ) - def Popen(self, process_args, config=None, **kwargs): - kwargs = _update_kwargs(config, **kwargs) - + def Popen(self, process_args, **kwargs): return subprocess.Popen( - self.__build_cmd(process_args, **kwargs), + self.__build_cmd(process_args, **_update_kwargs(**kwargs)), **kwargs ) - def run_less(self, process_args, config=None, **kwargs): + def run_less(self, process_args, **kwargs): kwargs['stdout'] = subprocess.PIPE kwargs['stderr'] = subprocess.DEVNULL process = self.Popen( - process_args, config, + process_args, **kwargs ) diff --git a/src/kiwi/subcommands/utils/project.py b/src/kiwi/subcommands/utils/project.py index b248abe..d371d83 100644 --- a/src/kiwi/subcommands/utils/project.py +++ b/src/kiwi/subcommands/utils/project.py @@ -11,6 +11,20 @@ class Project: def __init__(self, name): self.__name = name + @classmethod + def from_file_name(cls, file_name): + if os.path.isdir(file_name): + config = LoadedConfig.get() + + if file_name.endswith(config['markers:disabled']): + file_name = file_name[:-len(config['markers:disabled'])] + + if file_name.endswith(config['markers:project']): + file_name = file_name[:-len(config['markers:project'])] + return cls(file_name) + + return None + def get_name(self): return self.__name @@ -78,40 +92,41 @@ class Project: return True -def _extract_project_name(file_name): - config = LoadedConfig.get() - enabled_suffix = config['markers:project'] - disabled_suffix = f"{enabled_suffix}{config['markers:disabled']}" - - if os.path.isdir(file_name): - # all subdirectories - if file_name.endswith(enabled_suffix): - # enabled projects - return file_name[:-len(enabled_suffix)] - - elif file_name.endswith(disabled_suffix): - # disabled projects - return file_name[:-len(disabled_suffix)] - - return None - - class Projects: __projects = None - def __init__(self, names): - self.__projects = [ - Project(name) - for name in names if isinstance(name, str) - ] - def __getitem__(self, item): return self.__projects[item] + def __str__(self): + return str([ + project.get_name() + for project + in self.__projects + ]) + @classmethod - def all(cls): - return cls([ - _extract_project_name(file_name) + def from_names(cls, project_names): + result = cls() + result.__projects = [ + Project(name) + for name in project_names if isinstance(name, str) + ] + return result + + @classmethod + def from_projects(cls, projects): + result = cls() + result.__projects = [ + project + for project in projects if isinstance(project, Project) + ] + return result + + @classmethod + def from_dir(cls): + return cls.from_projects([ + Project.from_file_name(file_name) for file_name in os.listdir() ]) @@ -119,9 +134,39 @@ class Projects: def from_args(cls, args): if args is not None and 'projects' in args: if isinstance(args.projects, list) and args.projects: - return cls(args.projects) + return cls.from_names(args.projects) elif isinstance(args.projects, str): - return cls([args.projects]) + return cls.from_names([args.projects]) - return [] + return cls() + + def empty(self): + return not self.__projects + + def filter_exists(self): + result = Projects() + result.__projects = [ + project + for project in self.__projects + if project.exists() + ] + return result + + def filter_enabled(self): + result = Projects() + result.__projects = [ + project + for project in self.__projects + if project.is_enabled() + ] + return result + + def filter_disabled(self): + result = Projects() + result.__projects = [ + project + for project in self.__projects + if project.is_disabled() + ] + return result diff --git a/src/kiwi/subcommands/utils/rootkit.py b/src/kiwi/subcommands/utils/rootkit.py index b3686c7..7f890eb 100644 --- a/src/kiwi/subcommands/utils/rootkit.py +++ b/src/kiwi/subcommands/utils/rootkit.py @@ -14,6 +14,7 @@ def _prefix_path(prefix, path): if isinstance(path, str): abs_path = os.path.abspath(path) return os.path.realpath(f"{prefix}/{abs_path}") + elif isinstance(path, list): return [_prefix_path(prefix, p) for p in path] @@ -36,55 +37,43 @@ class Rootkit: def __init__(self, image_tag=None): self.__image_tag = image_tag - def __exists(self, config, args): - ps = DockerCommand('docker').run( - config, args, [ - 'images', - '--filter', f"reference={_image_name(self.__image_tag)}", - '--format', '{{.Repository}}:{{.Tag}}' - ], - stdout=subprocess.PIPE - ) + def __exists(self): + ps = DockerCommand('docker').run(None, [ + 'images', + '--filter', f"reference={_image_name(self.__image_tag)}", + '--format', '{{.Repository}}:{{.Tag}}' + ], stdout=subprocess.PIPE) return str(ps.stdout, 'utf-8').strip() == _image_name(self.__image_tag) - def __build_image(self, config, args): - if self.__exists(config, args): + def __build_image(self): + if self.__exists(): logging.info(f"Using image {_image_name(self.__image_tag)}") else: if self.__image_tag is None: logging.info(f"Pulling image {_image_name(self.__image_tag)}") - DockerCommand('docker').run( - config, args, ['pull', _image_name(self.__image_tag)], - stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL - ) + DockerCommand('docker').run(None, [ + 'pull', _image_name(self.__image_tag) + ], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) else: logging.info(f"Building image {_image_name(self.__image_tag)}") - DockerCommand('docker').run( - config, args, - [ - 'build', - '-t', _image_name(self.__image_tag), - '-f', f"{IMAGES_DIRECTORY_NAME}/{self.__image_tag}.Dockerfile", - f"{IMAGES_DIRECTORY_NAME}" - ], - stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL - ) + DockerCommand('docker').run(None, [ + 'build', + '-t', _image_name(self.__image_tag), + '-f', f"{IMAGES_DIRECTORY_NAME}/{self.__image_tag}.Dockerfile", + f"{IMAGES_DIRECTORY_NAME}" + ], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) - def run(self, config, args, process_args, **kwargs): - self.__build_image(config, args) - DockerCommand('docker').run( - config, args, - [ - 'run', '--rm', - '-v', '/:/mnt', - '-u', 'root', - _image_name(self.__image_tag), - *process_args - ], - **kwargs - ) + def run(self, process_args, **kwargs): + self.__build_image() + DockerCommand('docker').run(None, [ + 'run', '--rm', + '-v', '/:/mnt', + '-u', 'root', + _image_name(self.__image_tag), + *process_args + ], **kwargs) __image_tag = None __instances = {}