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):
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

View file

@ -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,9 +20,10 @@ 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"""
if args is None:
args = Parser().get_args()
if command is None:
@ -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()

View file

@ -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
)
if num_projects == 1:
projects = "a project"
if not num_projects == 1:
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)
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)
def run(self, runner, args):
if 'services' in args and args.services:
project = Projects.from_args(args)[0]
# 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)
logging.info(f"{self._action} project '{project.get_name()}', services {args.services}")
return self._run_services(runner, args, project, args.services)
else:
return super().run(runner, args)

View file

@ -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

View file

@ -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 <cmd>`"
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

View file

@ -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()
]

View file

@ -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
])

View file

@ -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

View file

@ -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
])

View file

@ -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()

View file

@ -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:
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

View file

@ -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

View file

@ -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,
[
DockerCommand('docker').run(None, [
'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")
'--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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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')

View file

@ -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):
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()
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
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)
)

View file

@ -3,9 +3,13 @@ import logging
import os
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
if 'env' not in kwargs:
kwargs['env'] = {}
@ -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
)

View file

@ -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

View file

@ -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, [
def __exists(self):
ps = DockerCommand('docker').run(None, [
'images',
'--filter', f"reference={_image_name(self.__image_tag)}",
'--format', '{{.Repository}}:{{.Tag}}'
],
stdout=subprocess.PIPE
)
], 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,
[
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
)
], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
def run(self, config, args, process_args, **kwargs):
self.__build_image(config, args)
DockerCommand('docker').run(
config, args,
[
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
)
], **kwargs)
__image_tag = None
__instances = {}