Rework: FlexCommand defaults into lower hierarchy (override protected _run variants by default)

This commit is contained in:
Jörn-Michael Miehe 2020-08-19 16:10:56 +02:00
parent 0060bf8878
commit 93d0b56eb7
24 changed files with 414 additions and 348 deletions

View file

@ -46,7 +46,8 @@ class Parser:
def get_args(self): def get_args(self):
if self.__args is None: if self.__args is None:
# parse args if needed # 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 return self.__args

View file

@ -3,7 +3,6 @@ import logging
# local # local
from . import subcommands from . import subcommands
from .config import LoadedConfig
from .parser import Parser from .parser import Parser
@ -13,7 +12,6 @@ class Runner:
class __Runner: class __Runner:
"""Singleton type""" """Singleton type"""
__parser = None
__commands = [] __commands = []
def __init__(self): def __init__(self):
@ -22,9 +20,10 @@ class Runner:
cmd = getattr(subcommands, className) cmd = getattr(subcommands, className)
self.__commands.append(cmd()) self.__commands.append(cmd())
def run(self, command=None): def run(self, command=None, args=None):
"""run the desired subcommand""" """run the desired subcommand"""
if args is None:
args = Parser().get_args() args = Parser().get_args()
if command is None: if command is None:
@ -36,7 +35,7 @@ class Runner:
logging.debug(f"Running '{cmd}' with args: {args}") logging.debug(f"Running '{cmd}' with args: {args}")
try: try:
result = cmd.run(self, LoadedConfig.get(), args) result = cmd.run(self, args)
except KeyboardInterrupt: except KeyboardInterrupt:
print() print()

View file

@ -1,5 +1,6 @@
# system # system
import logging import logging
import os
# local # local
from .utils.project import Projects from .utils.project import Projects
@ -16,7 +17,9 @@ class SubCommand:
# command parser # command parser
_sub_parser = None _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 self.__name = name
if add_parser: if add_parser:
self._sub_parser = Parser().get_subparsers().add_parser( self._sub_parser = Parser().get_subparsers().add_parser(
@ -24,26 +27,38 @@ class SubCommand:
**kwargs **kwargs
) )
if not action:
# default action string
self._action = f"Running '{str(self)}' for"
else:
self._action = action
def __str__(self): def __str__(self):
return self.__name return self.__name
def run(self, runner, config, args): def _run_instance(self, runner, args):
"""actually run command with this dir's config and parsed CLI args"""
pass 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): class ProjectCommand(SubCommand):
"""this command concerns a project in current instance""" """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__( super().__init__(
name, add_parser=add_parser, name, action=action, add_parser=add_parser,
**kwargs **kwargs
) )
if num_projects == 1:
projects = "a project" projects = "a project"
else:
if not num_projects == 1:
projects = "project(s)" projects = "project(s)"
self._sub_parser.add_argument( self._sub_parser.add_argument(
@ -51,19 +66,41 @@ class ProjectCommand(SubCommand):
help=f"select {projects} in this instance" 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): class ServiceCommand(ProjectCommand):
"""this command concerns service(s) in a project""" """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__( super().__init__(
name, num_projects=num_projects, add_parser=add_parser, name, num_projects=num_projects, action=action, add_parser=add_parser,
**kwargs **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)" services = "service(s)"
self._sub_parser.add_argument( self._sub_parser.add_argument(
@ -71,52 +108,25 @@ class ServiceCommand(ProjectCommand):
help=f"select {services} in a project" help=f"select {services} in a project"
) )
def _run_projects(self, runner, args, projects):
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):
result = True result = True
for project in Projects.from_args(args): # default: run without services for all given
args.projects = project.get_name() for project in projects:
result &= runner.run(str(self)) result &= self._run_services(runner, args, project, [])
return result return result
def _run_project(self, runner, config, args): def _run_services(self, runner, args, project, services):
return self._run_services(runner, config, args, [])
def _run_services(self, runner, config, args, services):
pass pass
def run(self, runner, config, args): def run(self, runner, args):
projects = Projects.from_args(args) if 'services' in args and args.services:
if not projects: project = Projects.from_args(args)[0]
# no project given, run for entire instance
logging.info(f"{self.__action} this instance")
return self._run_instance(runner, config, args)
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 # run for service(s) inside project
logging.info(f"{self.__action} services {args.services} in project '{project.get_name()}'") logging.info(f"{self._action} project '{project.get_name()}', services {args.services}")
return self._run_services(runner, config, args, args.services) return self._run_services(runner, args, project, args.services)
else:
return super().run(runner, args)

View file

@ -1,19 +1,20 @@
# local # local
from ._subcommand import FlexCommand from ._subcommand import ServiceCommand
from .utils.dockercommand import DockerCommand from .utils.dockercommand import DockerCommand
class BuildCommand(FlexCommand): class BuildCommand(ServiceCommand):
"""kiwi build""" """kiwi build"""
def __init__(self): def __init__(self):
super().__init__( 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" description="Build images for the whole instance, a project or service(s) inside a project"
) )
def _run_services(self, runner, config, args, services): def _run_services(self, runner, args, project, services):
DockerCommand('docker-compose').run( DockerCommand('docker-compose').run(project, [
config, args, ['build', '--pull', *services] 'build', '--pull', *services
) ])
return True return True

View file

@ -1,3 +1,6 @@
# system
import logging
# local # local
from ._subcommand import ProjectCommand from ._subcommand import ProjectCommand
from .utils.dockercommand import DockerCommand from .utils.dockercommand import DockerCommand
@ -9,19 +12,32 @@ class CmdCommand(ProjectCommand):
def __init__(self): def __init__(self):
super().__init__( super().__init__(
'cmd', num_projects=1, 'cmd', num_projects=1,
action="Running docker-compose in",
description="Run raw docker-compose command in a project" description="Run raw docker-compose command in a project"
) )
# command string after docker-compose # command for docker-compose
self._sub_parser.add_argument( self._sub_parser.add_argument(
'compose_cmd', metavar='cmd', type=str, 'compose_cmd', metavar='cmd', type=str,
help="runs `docker-compose <cmd>`" help="command for 'docker-compose'"
) )
def run(self, runner, config, args): # arguments for docker-compose command
import shlex 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 # 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 return True

View file

@ -10,6 +10,7 @@ from .utils.rootkit import Rootkit, prefix_path_mnt
# parent # parent
from .._constants import CONF_DIRECTORY_NAME from .._constants import CONF_DIRECTORY_NAME
from ..config import LoadedConfig
class ConfCopyCommand(SubCommand): class ConfCopyCommand(SubCommand):
@ -21,22 +22,20 @@ class ConfCopyCommand(SubCommand):
description="Synchronize all config files to target directory" description="Synchronize all config files to target directory"
) )
def run(self, runner, config, args): def _run_instance(self, runner, args):
conf_dirs = [ conf_dirs = [
project.conf_dir_name() project.conf_dir_name()
for project in Projects.all() for project in Projects.from_dir().filter_enabled()
if project.is_enabled()
] ]
if conf_dirs: if conf_dirs:
# add target directory # add target directory
conf_dirs.append(config['runtime:storage']) conf_dirs.append(LoadedConfig.get()['runtime:storage'])
logging.info(f"Sync directories: {conf_dirs}") logging.info(f"Sync directories: {conf_dirs}")
Rootkit('rsync').run( Rootkit('rsync').run([
config, args, ['rsync', '-r', *prefix_path_mnt(conf_dirs)], 'rsync', '-r', *prefix_path_mnt(conf_dirs)
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL ], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
)
return True return True
@ -50,14 +49,13 @@ class ConfPurgeCommand(SubCommand):
description="Remove all config files in target directory" description="Remove all config files in target directory"
) )
def run(self, runner, config, args): def _run_instance(self, runner, args):
conf_target = f"{config['runtime:storage']}/{CONF_DIRECTORY_NAME}" conf_target = f"{LoadedConfig.get()['runtime:storage']}/{CONF_DIRECTORY_NAME}"
logging.info(f"Purging directories: {conf_target}") logging.info(f"Purging directories: {conf_target}")
Rootkit().run( Rootkit().run([
config, args, ['rm', '-rf', prefix_path_mnt(conf_target)], 'rm', '-rf', prefix_path_mnt(conf_target)
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL ], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
)
return True return True
@ -71,12 +69,12 @@ class ConfCleanCommand(SubCommand):
description="Cleanly sync all configs to target folder, relaunch affected projects" 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 result = True
affected_projects = [ affected_projects = [
project.conf_dir_name() project.conf_dir_name()
for project in Projects.all() for project in Projects.from_dir()
if project.has_configs() if project.has_configs()
] ]

View file

@ -9,11 +9,12 @@ class DisableCommand(ProjectCommand):
def __init__(self): def __init__(self):
super().__init__( super().__init__(
'disable', num_projects='+', 'disable', num_projects='+',
action="Disabling",
description="Disable whole project(s) in this instance" description="Disable whole project(s) in this instance"
) )
def run(self, runner, config, args): def _run_projects(self, runner, args, projects):
return all([ return all([
project.disable() project.disable()
for project in Projects.from_args(args) for project in projects
]) ])

View file

@ -1,40 +1,42 @@
# local # local
from ._subcommand import FlexCommand from ._subcommand import ServiceCommand
from .utils.dockercommand import DockerCommand from .utils.dockercommand import DockerCommand
from .utils.misc import are_you_sure from .utils.misc import are_you_sure
class DownCommand(FlexCommand): class DownCommand(ServiceCommand):
"""kiwi down""" """kiwi down"""
def __init__(self): def __init__(self):
super().__init__( 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" 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([ if are_you_sure([
"This will bring down the entire instance.", "This will bring down the entire instance.",
"", "",
"This may not be what you intended, because:", "This may not be what you intended, because:",
" - Bringing down the instance stops ALL services in here", " - Bringing down the instance stops ALL services in here",
]): ]):
return super()._run_instance(runner, config, args) return super()._run_instance(runner, args)
return False return False
def _run_project(self, runner, config, args): def _run_projects(self, runner, args, projects):
DockerCommand('docker-compose').run( for project in projects:
config, args, ['down'] DockerCommand('docker-compose').run(project, [
) 'down'
])
return True return True
def _run_services(self, runner, config, args, services): def _run_services(self, runner, args, project, services):
DockerCommand('docker-compose').run( DockerCommand('docker-compose').run(project, [
config, args, ['stop', *services] 'stop', *services
) ])
DockerCommand('docker-compose').run( DockerCommand('docker-compose').run(project, [
config, args, ['rm', '-f', *services] 'rm', '-f', *services
) ])
return True return True

View file

@ -9,11 +9,12 @@ class EnableCommand(ProjectCommand):
def __init__(self): def __init__(self):
super().__init__( super().__init__(
'enable', num_projects='+', 'enable', num_projects='+',
action="Enabling",
description="Enable whole project(s) in this instance" description="Enable whole project(s) in this instance"
) )
def run(self, runner, config, args): def _run_projects(self, runner, args, projects):
return all([ return all([
project.enable() project.enable()
for project in Projects.from_args(args) for project in projects
]) ])

View file

@ -7,6 +7,7 @@ from ._subcommand import SubCommand
# parent (display purposes only) # parent (display purposes only)
from .._constants import KIWI_CONF_NAME from .._constants import KIWI_CONF_NAME
from ..config import DefaultConfig, LoadedConfig
def user_input(config, key, prompt): 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" 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()}'") logging.info(f"Initializing '{KIWI_CONF_NAME}' in '{os.getcwd()}'")
config = LoadedConfig.get()
# check force switch # check force switch
if args.force and os.path.isfile(KIWI_CONF_NAME): if args.force and os.path.isfile(KIWI_CONF_NAME):
from ..config import DefaultConfig
logging.warning(f"Overwriting existing '{KIWI_CONF_NAME}'!") logging.warning(f"Overwriting existing '{KIWI_CONF_NAME}'!")
config = DefaultConfig.get() config = DefaultConfig.get()

View file

@ -1,84 +1,75 @@
# system # system
import logging import logging
import os import os
import subprocess
import yaml import yaml
# local # local
from ._subcommand import FlexCommand from ._subcommand import ServiceCommand
from .utils.dockercommand import DockerCommand from .utils.project import Project, Projects
from .utils.project import Projects
def _print_list(strings): 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: for string in strings:
print(f" - {string}") _print_list(string)
elif isinstance(strings, str): else:
_print_list(strings.strip().split('\n')) _print_list(list(strings))
elif isinstance(strings, bytes):
_print_list(str(strings, 'utf-8'))
class ListCommand(FlexCommand): class ListCommand(ServiceCommand):
"""kiwi list""" """kiwi list"""
def __init__(self): def __init__(self):
super().__init__( 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" 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(f"kiwi-config instance at '{os.getcwd()}'")
print("#########") print("#########")
projects = Projects.all() projects = Projects.from_dir()
enableds = [ enabled_projects = projects.filter_enabled()
project.get_name() if not enabled_projects.empty():
for project in projects
if project.is_enabled()
]
if enableds:
print(f"Enabled projects:") print(f"Enabled projects:")
_print_list(enableds) _print_list(enabled_projects)
disableds = [ disabled_projects = projects.filter_disabled()
project.get_name() if not disabled_projects.empty():
for project in projects
if project.is_disabled()
]
if disableds:
print(f"Disabled projects:") print(f"Disabled projects:")
_print_list(disableds) _print_list(disabled_projects)
return True return True
def _run_project(self, runner, config, args): def _run_projects(self, runner, args, projects):
project = Projects.from_args(args)[0] project = projects[0]
if not project.exists(): if not project.exists():
logging.error(f"Project '{project.get_name()}' not found") logging.warning(f"Project '{project.get_name()}' not found")
return False return False
print(f"Services in project '{project.get_name()}':") print(f"Services in project '{project.get_name()}':")
print("#########") print("#########")
ps = DockerCommand('docker-compose').run( with open(project.compose_file_name(), 'r') as stream:
config, args, ['config', '--services'], try:
stdout=subprocess.PIPE 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 return True
def _run_services(self, runner, config, args, services): def _run_services(self, runner, args, project, services):
project = Projects.from_args(args)[0]
if not project.exists(): if not project.exists():
logging.error(f"Project '{project.get_name()}' not found") logging.error(f"Project '{project.get_name()}' not found")
return False return False
@ -91,10 +82,13 @@ class ListCommand(FlexCommand):
docker_compose_yml = yaml.safe_load(stream) docker_compose_yml = yaml.safe_load(stream)
for service_name in services: for service_name in services:
try:
print(yaml.dump( print(yaml.dump(
{service_name: docker_compose_yml['services'][service_name]}, {service_name: docker_compose_yml['services'][service_name]},
default_flow_style=False, sort_keys=False default_flow_style=False, sort_keys=False
).strip()) ).strip())
except KeyError:
logging.error(f"Service '{service_name}' not found")
return True return True

View file

@ -9,6 +9,7 @@ class LogsCommand(ServiceCommand):
def __init__(self): def __init__(self):
super().__init__( super().__init__(
'logs', num_projects=1, num_services='*', 'logs', num_projects=1, num_services='*',
action="Showing logs of",
description="Show logs of a project or service(s) of a project" 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" help="output appended data as log grows"
) )
def run(self, runner, config, args): def _run_services(self, runner, args, project, services):
# include timestamps # include timestamps
compose_cmd = ['logs', '-t'] compose_cmd = ['logs', '-t']
@ -27,13 +28,13 @@ class LogsCommand(ServiceCommand):
compose_cmd = [*compose_cmd, '-f', '--tail=10'] compose_cmd = [*compose_cmd, '-f', '--tail=10']
# append if one or more services are given # append if one or more services are given
if args.services: if services:
compose_cmd = [*compose_cmd, *args.services] compose_cmd = [*compose_cmd, *args.services]
# use 'less' viewer if output will be static # use 'less' viewer if output will be static
if args.follow: if args.follow:
DockerCommand('docker-compose').run(config, args, compose_cmd) DockerCommand('docker-compose').run(project, compose_cmd)
else: else:
DockerCommand('docker-compose').run_less(config, args, compose_cmd) DockerCommand('docker-compose').run_less(project, compose_cmd)
return True return True

View file

@ -7,16 +7,18 @@ from ._subcommand import SubCommand
from .utils.dockercommand import DockerCommand from .utils.dockercommand import DockerCommand
from .utils.misc import are_you_sure from .utils.misc import are_you_sure
# parent
from ..config import LoadedConfig
def _find_net(config, args):
ps = DockerCommand('docker').run( def _find_net(net_name):
config, args, ['network', 'ls', '--filter', f"name={config['network:name']}", '--format', '{{.Name}}'], ps = DockerCommand('docker').run(None, [
stdout=subprocess.PIPE 'network', 'ls', '--filter', f"name={net_name}", '--format', '{{.Name}}'
) ], stdout=subprocess.PIPE)
net_found = str(ps.stdout, 'utf-8').strip() net_found = str(ps.stdout, 'utf-8').strip()
return net_found == config['network:name'] return net_found == net_name
class NetUpCommand(SubCommand): class NetUpCommand(SubCommand):
@ -25,30 +27,31 @@ class NetUpCommand(SubCommand):
def __init__(self): def __init__(self):
super().__init__( super().__init__(
'net-up', 'net-up',
action="Creating the local network hub",
description="Create the local network hub for this instance" description="Create the local network hub for this instance"
) )
def run(self, runner, config, args): def _run_instance(self, runner, args):
if _find_net(config, args): config = LoadedConfig.get()
logging.info(f"Network '{config['network:name']}' already exists") 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 return True
try: try:
DockerCommand('docker').run( DockerCommand('docker').run(None, [
config, args,
[
'network', 'create', 'network', 'create',
'--driver', 'bridge', '--driver', 'bridge',
'--internal', '--internal',
'--subnet', config['network:cidr'], '--subnet', net_cidr,
config['network:name'] net_name
], ], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL logging.info(f"Network '{net_name}' created")
)
logging.info(f"Network '{config['network:name']}' created")
except subprocess.CalledProcessError: except subprocess.CalledProcessError:
logging.error(f"Error creating network '{config['network:name']}'") logging.error(f"Error creating network '{net_name}'")
return False return False
return True return True
@ -63,25 +66,26 @@ class NetDownCommand(SubCommand):
description="Remove the local network hub for this instance" description="Remove the local network hub for this instance"
) )
def run(self, runner, config, args): def _run_instance(self, runner, args):
if not _find_net(config, args): net_name = LoadedConfig.get()['network:name']
logging.info(f"Network '{config['network:name']}' does not exist")
if not _find_net(net_name):
logging.info(f"Network '{net_name}' does not exist")
return True return True
try: try:
if are_you_sure("This will bring down this instance's hub network!"): if are_you_sure("This will bring down this instance's hub network!"):
if runner.run('down'): if runner.run('down'):
DockerCommand('docker').run( DockerCommand('docker').run(None, [
config, args, 'network', 'rm', net_name
['network', 'rm', config['network:name']], ], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
) logging.info(f"Network '{net_name}' removed")
logging.info(f"Network '{config['network:name']}' removed")
else: else:
return False return False
except subprocess.CalledProcessError: except subprocess.CalledProcessError:
logging.error(f"Error removing network '{config['network:name']}'") logging.error(f"Error removing network '{net_name}'")
return False return False
return True return True

View file

@ -17,12 +17,12 @@ class NewCommand(ProjectCommand):
def __init__(self): def __init__(self):
super().__init__( super().__init__(
'new', num_projects='+', 'new', num_projects='+',
action="Creating",
description="Create new empty project(s) in this instance" description="Create new empty project(s) in this instance"
) )
def run(self, runner, config, args): def _run_projects(self, runner, args, projects):
result = True result = True
projects = Projects.from_args(args)
for project in projects: for project in projects:
if project.exists(): if project.exists():
@ -31,7 +31,7 @@ class NewCommand(ProjectCommand):
else: else:
logging.info(f"Creating project '{project.get_name()}'") 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()) shutil.copy(DEFAULT_DOCKER_COMPOSE_NAME, project.compose_file_name())
return result return result

View file

@ -1,19 +1,20 @@
# local # local
from ._subcommand import FlexCommand from ._subcommand import ServiceCommand
from .utils.dockercommand import DockerCommand from .utils.dockercommand import DockerCommand
class PullCommand(FlexCommand): class PullCommand(ServiceCommand):
"""kiwi pull""" """kiwi pull"""
def __init__(self): def __init__(self):
super().__init__( 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" description="Pull images for the whole instance, a project or service(s) inside a project"
) )
def _run_services(self, runner, config, args, services): def _run_services(self, runner, args, project, services):
DockerCommand('docker-compose').run( DockerCommand('docker-compose').run(project, [
config, args, ['pull', '--ignore-pull-failures', *services] 'pull', '--ignore-pull-failures', *services
) ])
return True return True

View file

@ -1,19 +1,20 @@
# local # local
from ._subcommand import FlexCommand from ._subcommand import ServiceCommand
from .utils.dockercommand import DockerCommand from .utils.dockercommand import DockerCommand
class PushCommand(FlexCommand): class PushCommand(ServiceCommand):
"""kiwi push""" """kiwi push"""
def __init__(self): def __init__(self):
super().__init__( 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" description="Push images for the whole instance, a project or service(s) inside a project"
) )
def _run_services(self, runner, config, args, services): def _run_services(self, runner, args, project, services):
DockerCommand('docker-compose').run( DockerCommand('docker-compose').run(project, [
config, args, ['push', *services] 'push', *services
) ])
return True return True

View file

@ -6,19 +6,21 @@ import subprocess
from ._subcommand import ServiceCommand from ._subcommand import ServiceCommand
from .utils.dockercommand import DockerCommand 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. Requires /bin/sh and which.
""" """
try: try:
# test if desired shell exists # test if desired shell exists
DockerCommand('docker-compose').run( DockerCommand('docker-compose').run(project, [
config, args, [*compose_cmd, '/bin/sh', '-c', f"which {exe_name}"], 'exec', service, '/bin/sh', '-c', f"which {exe_name}"
check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL ], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
)
return True return True
except subprocess.CalledProcessError as e: except subprocess.CalledProcessError as e:
@ -26,13 +28,14 @@ def _service_has_executable(config, args, compose_cmd, exe_name):
return False return False
def _find_shell(config, args, compose_cmd): def _find_shell(args, project, service):
"""find first working shell (provided by config and args) in container (as of compose_cmd array)""" """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' # builtin shells: as a last resort, fallback to '/bin/sh' and 'sh'
shells = ['/bin/sh', 'sh'] shells = ['/bin/sh', 'sh']
# load favorite shells from config # load favorite shells from config
config = LoadedConfig.get()
if config['runtime:shells']: if config['runtime:shells']:
shells = [*config['runtime:shells'], *shells] shells = [*config['runtime:shells'], *shells]
@ -44,7 +47,7 @@ def _find_shell(config, args, compose_cmd):
# actually try shells # actually try shells
for i, shell in enumerate(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 # found working shell
logging.debug(f"Using shell '{shell}'") logging.debug(f"Using shell '{shell}'")
return shell return shell
@ -82,15 +85,15 @@ class ShCommand(ServiceCommand):
help="shell to spawn" help="shell to spawn"
) )
def run(self, runner, config, args): def _run_services(self, runner, args, project, services):
compose_cmd = ['exec', args.services[0]] service = services[0]
shell = _find_shell(config, args, compose_cmd) shell = _find_shell(args, project, service)
if shell is not None: if shell is not None:
# spawn shell # spawn shell
DockerCommand('docker-compose').run( DockerCommand('docker-compose').run(project, [
config, args, [*compose_cmd, shell] 'exec', service, shell
) ])
return True return True
return False return False

View file

@ -1,6 +1,9 @@
# local # local
from ._subcommand import SubCommand from ._subcommand import SubCommand
# parent
from ..config import LoadedConfig
class ShowCommand(SubCommand): class ShowCommand(SubCommand):
"""kiwi show""" """kiwi show"""
@ -11,6 +14,6 @@ class ShowCommand(SubCommand):
description="Show effective kiwi.yml" description="Show effective kiwi.yml"
) )
def run(self, runner, config, args): def _run_instance(self, runner, args):
print(config) print(LoadedConfig.get())
return True return True

View file

@ -1,28 +1,29 @@
# local # local
from ._subcommand import FlexCommand from ._subcommand import ServiceCommand
from .utils.dockercommand import DockerCommand from .utils.dockercommand import DockerCommand
class UpCommand(FlexCommand): class UpCommand(ServiceCommand):
"""kiwi up""" """kiwi up"""
def __init__(self): def __init__(self):
super().__init__( 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" 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'): if runner.run('conf-copy'):
return super()._run_instance(runner, config, args) return super()._run_instance(runner, args)
return False return False
def _run_services(self, runner, config, args, services): def _run_services(self, runner, args, project, services):
if runner.run('net-up'): if runner.run('net-up'):
DockerCommand('docker-compose').run( DockerCommand('docker-compose').run(project, [
config, args, ['up', '-d', *services] 'up', '-d', *services
) ])
return True return True
return False return False

View file

@ -1,18 +1,19 @@
# local # local
from ._subcommand import FlexCommand from ._subcommand import ServiceCommand
from .utils.misc import are_you_sure from .utils.misc import are_you_sure
class UpdateCommand(FlexCommand): class UpdateCommand(ServiceCommand):
"""kiwi update""" """kiwi update"""
def __init__(self): def __init__(self):
super().__init__( super().__init__(
'update', "Updating", 'update', num_projects='?', num_services='*',
action="Updating",
description="Update the whole instance, a project or service(s) inside a project" 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([ if are_you_sure([
"This will update the entire instance at once.", "This will update the entire instance at once.",
"", "",
@ -20,12 +21,14 @@ class UpdateCommand(FlexCommand):
" - Updates may take a long time", " - Updates may take a long time",
" - Updates may break beloved functionality", " - Updates may break beloved functionality",
]): ]):
return super()._run_instance(runner, config, args) return super()._run_instance(runner, args)
return False return False
def _run_services(self, runner, config, args, services): def _run_services(self, runner, args, project, services):
result = runner.run('build') result = True
result &= runner.run('build')
result &= runner.run('pull') result &= runner.run('pull')
result &= runner.run('conf-copy') result &= runner.run('conf-copy')
result &= runner.run('down') result &= runner.run('down')

View file

@ -5,21 +5,16 @@ import subprocess
# local # local
from .executable import Executable from .executable import Executable
from .project import Projects
# parent # parent
from ..._constants import CONF_DIRECTORY_NAME from ..._constants import CONF_DIRECTORY_NAME
from ...parser import Parser
from ...config import LoadedConfig from ...config import LoadedConfig
def _update_kwargs(**kwargs): 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() 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]
# execute command in project directory # execute command in project directory
kwargs['cwd'] = project.dir_name() kwargs['cwd'] = project.dir_name()
@ -58,19 +53,15 @@ class DockerCommand(Executable):
except subprocess.CalledProcessError: except subprocess.CalledProcessError:
raise PermissionError("Cannot access docker, please get into the docker group or run as root!") raise PermissionError("Cannot access docker, please get into the docker group or run as root!")
def run(self, config, args, process_args, **kwargs): def run(self, project, process_args, **kwargs):
kwargs = _update_kwargs(**kwargs)
# equivalent to 'super().run' but agnostic of nested class construct # equivalent to 'super().run' but agnostic of nested class construct
return super().__getattr__("run")( return super().__getattr__("run")(
process_args, config, process_args,
**kwargs **_update_kwargs(project, **kwargs)
) )
def run_less(self, config, args, process_args, **kwargs): def run_less(self, project, process_args, **kwargs):
kwargs = _update_kwargs(**kwargs)
return super().__getattr__("run_less")( return super().__getattr__("run_less")(
process_args, config, process_args,
**kwargs **_update_kwargs(project, **kwargs)
) )

View file

@ -3,9 +3,13 @@ import logging
import os import os
import subprocess import subprocess
# parent
from ...config import LoadedConfig
def _update_kwargs(**kwargs):
config = LoadedConfig.get()
def _update_kwargs(config, **kwargs):
if config is not None:
# ensure there is an environment # ensure there is an environment
if 'env' not in kwargs: if 'env' not in kwargs:
kwargs['env'] = {} kwargs['env'] = {}
@ -48,28 +52,24 @@ class Executable:
logging.debug(f"Executable cmd{cmd}, kwargs{kwargs}") logging.debug(f"Executable cmd{cmd}, kwargs{kwargs}")
return cmd return cmd
def run(self, process_args, config=None, **kwargs): def run(self, process_args, **kwargs):
kwargs = _update_kwargs(config, **kwargs)
return subprocess.run( return subprocess.run(
self.__build_cmd(process_args, **kwargs), self.__build_cmd(process_args, **_update_kwargs(**kwargs)),
**kwargs **kwargs
) )
def Popen(self, process_args, config=None, **kwargs): def Popen(self, process_args, **kwargs):
kwargs = _update_kwargs(config, **kwargs)
return subprocess.Popen( return subprocess.Popen(
self.__build_cmd(process_args, **kwargs), self.__build_cmd(process_args, **_update_kwargs(**kwargs)),
**kwargs **kwargs
) )
def run_less(self, process_args, config=None, **kwargs): def run_less(self, process_args, **kwargs):
kwargs['stdout'] = subprocess.PIPE kwargs['stdout'] = subprocess.PIPE
kwargs['stderr'] = subprocess.DEVNULL kwargs['stderr'] = subprocess.DEVNULL
process = self.Popen( process = self.Popen(
process_args, config, process_args,
**kwargs **kwargs
) )

View file

@ -11,6 +11,20 @@ class Project:
def __init__(self, name): def __init__(self, name):
self.__name = 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): def get_name(self):
return self.__name return self.__name
@ -78,40 +92,41 @@ class Project:
return True 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: class Projects:
__projects = None __projects = None
def __init__(self, names):
self.__projects = [
Project(name)
for name in names if isinstance(name, str)
]
def __getitem__(self, item): def __getitem__(self, item):
return self.__projects[item] return self.__projects[item]
def __str__(self):
return str([
project.get_name()
for project
in self.__projects
])
@classmethod @classmethod
def all(cls): def from_names(cls, project_names):
return cls([ result = cls()
_extract_project_name(file_name) 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() for file_name in os.listdir()
]) ])
@ -119,9 +134,39 @@ class Projects:
def from_args(cls, args): def from_args(cls, args):
if args is not None and 'projects' in args: if args is not None and 'projects' in args:
if isinstance(args.projects, list) and args.projects: if isinstance(args.projects, list) and args.projects:
return cls(args.projects) return cls.from_names(args.projects)
elif isinstance(args.projects, str): 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

View file

@ -14,6 +14,7 @@ def _prefix_path(prefix, path):
if isinstance(path, str): if isinstance(path, str):
abs_path = os.path.abspath(path) abs_path = os.path.abspath(path)
return os.path.realpath(f"{prefix}/{abs_path}") return os.path.realpath(f"{prefix}/{abs_path}")
elif isinstance(path, list): elif isinstance(path, list):
return [_prefix_path(prefix, p) for p in path] return [_prefix_path(prefix, p) for p in path]
@ -36,55 +37,43 @@ class Rootkit:
def __init__(self, image_tag=None): def __init__(self, image_tag=None):
self.__image_tag = image_tag self.__image_tag = image_tag
def __exists(self, config, args): def __exists(self):
ps = DockerCommand('docker').run( ps = DockerCommand('docker').run(None, [
config, args, [
'images', 'images',
'--filter', f"reference={_image_name(self.__image_tag)}", '--filter', f"reference={_image_name(self.__image_tag)}",
'--format', '{{.Repository}}:{{.Tag}}' '--format', '{{.Repository}}:{{.Tag}}'
], ], stdout=subprocess.PIPE)
stdout=subprocess.PIPE
)
return str(ps.stdout, 'utf-8').strip() == _image_name(self.__image_tag) return str(ps.stdout, 'utf-8').strip() == _image_name(self.__image_tag)
def __build_image(self, config, args): def __build_image(self):
if self.__exists(config, args): if self.__exists():
logging.info(f"Using image {_image_name(self.__image_tag)}") logging.info(f"Using image {_image_name(self.__image_tag)}")
else: else:
if self.__image_tag is None: if self.__image_tag is None:
logging.info(f"Pulling image {_image_name(self.__image_tag)}") logging.info(f"Pulling image {_image_name(self.__image_tag)}")
DockerCommand('docker').run( DockerCommand('docker').run(None, [
config, args, ['pull', _image_name(self.__image_tag)], 'pull', _image_name(self.__image_tag)
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL ], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
)
else: else:
logging.info(f"Building image {_image_name(self.__image_tag)}") logging.info(f"Building image {_image_name(self.__image_tag)}")
DockerCommand('docker').run( DockerCommand('docker').run(None, [
config, args,
[
'build', 'build',
'-t', _image_name(self.__image_tag), '-t', _image_name(self.__image_tag),
'-f', f"{IMAGES_DIRECTORY_NAME}/{self.__image_tag}.Dockerfile", '-f', f"{IMAGES_DIRECTORY_NAME}/{self.__image_tag}.Dockerfile",
f"{IMAGES_DIRECTORY_NAME}" f"{IMAGES_DIRECTORY_NAME}"
], ], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
)
def run(self, config, args, process_args, **kwargs): def run(self, process_args, **kwargs):
self.__build_image(config, args) self.__build_image()
DockerCommand('docker').run( DockerCommand('docker').run(None, [
config, args,
[
'run', '--rm', 'run', '--rm',
'-v', '/:/mnt', '-v', '/:/mnt',
'-u', 'root', '-u', 'root',
_image_name(self.__image_tag), _image_name(self.__image_tag),
*process_args *process_args
], ], **kwargs)
**kwargs
)
__image_tag = None __image_tag = None
__instances = {} __instances = {}