#!/usr/bin/env python
import argparse
import os
from dataclasses import dataclass
from pathlib import Path
from typing import Set, List
from ruamel.yaml import YAML
from pydeployhelp import __version__
from pydeployhelp.base import ABC, Configs
[docs]@dataclass
class QuickstartDefaults:
deploy_dir: str
deploy_tasks: Set[str]
dockerfile: str
[docs]class Quickstart(ABC):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.defaults = QuickstartDefaults(
deploy_dir='deploy',
deploy_tasks={'build', 'up', 'down'},
dockerfile="""# use some base image
FROM python:buster
# run console commands inside image
RUN python -m pip install --upgrade pip wheel setuptools
RUN mkdir {project_name}
# change current directory inside image
WORKDIR {project_name}
# copy files from host to image
COPY requirements.txt .
"""
)
[docs] def start(self):
""" Receive info from user input and create deploy directory scripts """
try:
project_name = self.enter_project_name()
deploy_dir = self.enter_deploy_dir()
deploy_tasks = self.enter_deploy_tasks()
self.ask_to_continue()
except (KeyboardInterrupt, InterruptedError):
self._print_service_message('Interrupted', color=self.colors.red)
else:
self._print_service_message(
f'Creating service files for project "{project_name}" at "{deploy_dir}":',
color=self.colors.green
)
self.create_config_file(deploy_dir, deploy_tasks)
self.create_dockerfile(deploy_dir, project_name)
self.create_compose(deploy_dir, project_name)
self._print_service_message('Done!', color=self.colors.green)
[docs] def enter_project_name(self) -> str:
""" Receive project name from user input """
project_name = Path(os.getcwd()).name
if not self.silent:
project_name = input(f'Enter project name [{project_name}]: ').strip() or project_name
return project_name
[docs] def enter_deploy_dir(self) -> Path:
""" Receive deploy directory path from user input """
deploy_dir = self.defaults.deploy_dir
if not self.silent:
deploy_dir = input(
f'Enter directory path where deploy scripts should be created [{deploy_dir}]: '
).strip() or deploy_dir
deploy_dir = Path(deploy_dir)
if deploy_dir.exists() and not deploy_dir.is_dir():
self._print_service_message(
f'"{deploy_dir}"" is not a valid directory path, please try again',
color=self.colors.red
)
if self.silent:
raise InterruptedError # prevent from RecursionError
return self.enter_deploy_dir()
deploy_dir.mkdir(exist_ok=True, parents=True)
self._add_permissions(deploy_dir)
return deploy_dir
[docs] def enter_deploy_tasks(self) -> List[str]:
""" Receive deploy tasks names from user input """
allowed_tasks = list(self.defaults.deploy_tasks)
return self.enter(allowed_items=allowed_tasks, default='all', items_name='deploy tasks')
[docs] def create_config_file(self, deploy_dir: Path, deploy_tasks: List[str]):
""" Create file with deploy configs and tasks pipeline """
configs = Configs(
context=dict(env_file='.env', compose=f'{deploy_dir}/docker-compose-template.j2'),
tasks={task: [dict(
title=f'{task} all',
pipeline=[f'docker-compose -f {deploy_dir}/docker-compose-{"{ENV}"}.yml {task}']
)] for task in deploy_tasks}
)
yaml = YAML()
yaml.indent(mapping=2, sequence=4, offset=2)
configs_path = Path(f'{deploy_dir}/config.yml')
with configs_path.open('w', encoding='utf-8') as fp:
yaml.dump(configs.dict(), fp)
self._add_permissions(configs_path)
self._print_service_message('\tconfigs\t\t\N{check mark}', color=self.colors.green)
[docs] def create_dockerfile(self, deploy_dir: Path, project_name: str):
""" Create file with instructions for Docker daemon to build an image """
dockerfile_path = Path(f'{deploy_dir}/Dockerfile')
with dockerfile_path.open('w', encoding='utf-8') as fp:
for line in self.defaults.dockerfile.format(project_name=project_name).splitlines():
fp.write(f'{line.strip()}\n')
self._add_permissions(dockerfile_path)
self._print_service_message('\tdockerfile\t\N{check mark}', color=self.colors.green)
[docs] def create_compose(self, deploy_dir: Path, project_name: str):
data = {
'version': '3',
'services': {
project_name + '-{{ ENV }}': {
'build': {
'context': '..',
'dockerfile': f'{deploy_dir}/Dockerfile'
},
'image': project_name + ':{{ ENV }}',
'container_name': project_name + '-{{ ENV }}'
}
}
}
yaml = YAML()
yaml.indent(mapping=2, sequence=4, offset=2)
compose_path = Path(f'{deploy_dir}/docker-compose-template.j2')
with compose_path.open('w', encoding='utf-8') as fp:
yaml.dump(data, fp)
self._add_permissions(compose_path)
self._print_service_message('\tdocker-compose\t\N{check mark}', color=self.colors.green)
[docs]def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser()
parser.add_argument(
'-s', '--silent',
action='store_true',
help='If specified, all communication with user will be ignored, default values will be used instead'
)
parser.add_argument(
'-v', '--version',
action='store_true',
help='Print version and exit'
)
return parser.parse_args()
[docs]def main():
args = parse_args()
if args.version:
print(f'pydeployhelp-quickstart version {__version__}')
else:
quickstart = Quickstart(silent=args.silent)
quickstart.start()
if __name__ == "__main__":
main()