From 44918a0fe9b569c47391bf32ddec97771f585f86 Mon Sep 17 00:00:00 2001 From: Nuttapong Rojanavanich Date: Mon, 7 Oct 2019 19:39:58 +0700 Subject: [PATCH] Add pre commit (#13) * add pre-commit config * add yapf hook * format using google style * use black instead of yapf, add prettier * fix missing .yaml * run updated pre-commit * set black line length to 120 * run pre-commit using line length=120 --- .dockerignore | 1 + .pre-commit-config.yaml | 21 ++++ CHANGELOG.md | 30 +++++- README.md | 37 ++++--- docs/conf.py | 48 +++++---- eastern/cli.py | 153 +++++++++++---------------- eastern/formatter.py | 7 +- eastern/job_manager.py | 15 ++- eastern/kubectl.py | 70 ++++++------ eastern/kubeyml_helper.py | 10 +- eastern/plugin.py | 11 +- eastern/timeout.py | 12 +-- eastern/yaml_formatter/formatter.py | 34 +++--- eastern/yaml_formatter/overrides.py | 8 +- eastern/yaml_formatter/utils.py | 6 +- example/overrides/ingress-prod.yaml | 14 +-- example/overrides/ingress.yaml | 14 +-- example/sentry.yaml | 16 +-- setup.py | 45 ++++---- test_data/kubeyml_helper/sentry.yaml | 49 ++++----- tests/test_job_manager.py | 4 + tests/test_kubectl.py | 9 +- tests/test_kubeyml_helper.py | 7 +- tests/test_overrides.py | 30 ++---- tests/test_timeout.py | 23 ++-- 25 files changed, 341 insertions(+), 333 deletions(-) create mode 100644 .pre-commit-config.yaml diff --git a/.dockerignore b/.dockerignore index 2ea6b37..24d1672 100644 --- a/.dockerignore +++ b/.dockerignore @@ -2,3 +2,4 @@ .git __pycache__ test_*.py +.pre-commit-config.yaml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..60cde80 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,21 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks.git + rev: v1.4.0 + hooks: + - id: check-added-large-files + - id: check-ast + - id: check-case-conflict + - id: check-merge-conflict + - id: check-docstring-first + - id: trailing-whitespace + - id: check-symlinks + - repo: https://github.com/ambv/black.git + rev: 19.3b0 + hooks: + - id: black + entry: black -l 120 --target-version py36 + - repo: https://github.com/prettier/prettier.git + rev: 1.18.2 + hooks: + - id: prettier + files: \.(css|less|scss|ts|tsx|graphql|gql|js|jsx|md|yaml|yml)$ diff --git a/CHANGELOG.md b/CHANGELOG.md index 118661b..0cb07c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,59 +8,83 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ## [Unreleased] ## [4.4.1] - 2019-09-18 + ### Changed + - Bump pyyaml (@seehait) ## [4.4.0] - 2019-09-18 + ### Changed + - Docker image now use Debian Buster - Docker image now comes with Kubectl 1.15.3 ### Fixed + - Compatibility with kubectl 1.15 (@seehait) ## [4.3.0] - 2018-11-03 + ### Changed + - Job now exit with code 3 when job failure status is greater than zero. ## [4.2.1] - 2018-10-01 + ### Fixed + - Republished due to unclean build directory. Now using CI to cleanly submit ## [4.2.0] - 2018-09-24 + ### Changed + - Deploy now exit with code 3 when `kubectl rollout wait` exit with status code ## [4.1.2] - 2018-08-17 + ### Fixed + - Republished due to unclean build directory ## [4.1.1] - 2018-08-06 + ### Changed + - Updated dependencies - Docker now use Python 3.7 - Updated Kubernetes client in Docker image to 1.11.1 ## [4.1.0] - 2018-07-06 + ### Changed + - Job deployment will always removed after job success or failed - Fix exception raised when deploy job but pod was not scheduled ## [4.0.0] - 2018-06-15 + ### Added + - Pluggable formatters ### Changed + - BREAKING: The `formatter` module is moved to `yaml_formatter`. `formatter` now offer an abstract base class and the previous `format` function - BREAKING: Unmatched variable now cause warning (#4, @blead) ## [3.0.1] - 2018-04-17 + ### Changed + - Set PYTHONUNBUFFERED in Docker - Default timeout is 300s ## [3.0.0] - 2018-04-10 + ### Changed + - BREAKING: Timed out jobs now exit with status code 2 - BREAKING: Job timeout now count only idle time. - The previous behavior resulting in failing deployments when rollout involve many pods. A workaround is to adjust timeout based on expected number of pod. This new behavior ensure that the default timeout should be relevant regardless of number of pods. It should still be adjusted if the pod takes longer to start. @@ -68,14 +92,18 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - Updated kubectl ## [2.1.0] - 2017-12-22 + ### Added + - deploy/job now have `--timeout` option, defaulting to 60s ## [2.0.0] - 2017-12-22 + ### Changed + - Initial open source release 🎉 -[Unreleased]: https://github.com/wongnai/eastern/compare/v4.4.0...HEAD +[unreleased]: https://github.com/wongnai/eastern/compare/v4.4.0...HEAD [4.4.0]: https://github.com/wongnai/eastern/compare/v4.3.0...v4.4.0 [4.3.0]: https://github.com/wongnai/eastern/compare/v4.2.1...v4.3.0 [4.2.1]: https://github.com/wongnai/eastern/compare/v4.2.0...v4.2.1 diff --git a/README.md b/README.md index d718617..0345365 100644 --- a/README.md +++ b/README.md @@ -10,24 +10,24 @@ A Kubernetes templating and deployment tool. ## Table of Contents -* [Features](#features) -* [Installation](#installation) - * [Installing from PyPI](#installing-from-pypi) - * [Running from Docker Image](#running-from-docker-image) - * [Installing from Git](#installing-from-git) -* [Usage](#usage) - * [Template language](#template-language) - * [Deploy](#deploy) - * [Deploy jobs](#deploy-jobs) -* [Plugin](#plugin) -* [License](#license) +- [Features](#features) +- [Installation](#installation) + - [Installing from PyPI](#installing-from-pypi) + - [Running from Docker Image](#running-from-docker-image) + - [Installing from Git](#installing-from-git) +- [Usage](#usage) + - [Template language](#template-language) + - [Deploy](#deploy) + - [Deploy jobs](#deploy-jobs) +- [Plugin](#plugin) +- [License](#license) ## Features -* Simple, logicless template engine designed for YAML -* Work with multiple environments -* In use in production at [Wongnai](https://www.wongnai.com) -* Extensible plugin architecture +- Simple, logicless template engine designed for YAML +- Work with multiple environments +- In use in production at [Wongnai](https://www.wongnai.com) +- Extensible plugin architecture ## Installation @@ -52,7 +52,9 @@ docker run -v `pwd`:/projects/ --rm wongnai/eastern eastern generate /projects/k 3. Run `eastern` to verify that it is installed. ## Usage -### Template language + +### Template language + At its core, Eastern is a YAML templating tool. Eastern provides the following commands as YAML comment. - `load? file_1.yaml, file_2.yaml ...`: Load the first file available @@ -87,6 +89,7 @@ Available options: - `--no-wait`: Exit after running `kubectl` without waiting for rolling deploy ### Deploy jobs + Eastern comes with [Job](https://kubernetes.io/docs/concepts/workloads/controllers/jobs-run-to-completion/) deployment tool. To start a job, run `eastern job path/to/file.yaml namespace image_tag`. The file must have the job as its only document. Eastern will add `image_tag` as job suffix, deploy, wait until job's completion and remove the job. @@ -94,9 +97,11 @@ To start a job, run `eastern job path/to/file.yaml namespace image_tag`. The fil Supplied `image_tag` is available in the template as `${IMAGE_TAG}`. ## Plugin + Eastern is extensible. We use Eastern plugins ourselves. The API docs is available on [Read the Docs](https://eastern.readthedocs.io/en/latest/). ## License + (C) 2017 Wongnai Media Co, Ltd. Eastern is licensed under [MIT License](LICENSE) diff --git a/docs/conf.py b/docs/conf.py index 6949ed5..ad8e94e 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -30,33 +30,33 @@ # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. -extensions = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode'] +extensions = ["sphinx.ext.autodoc", "sphinx.ext.viewcode"] # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # # source_suffix = ['.rst', '.md'] -source_suffix = '.rst' +source_suffix = ".rst" # The master toctree document. -master_doc = 'index' +master_doc = "index" # General information about the project. -project = 'Eastern' -copyright = '2017, Wongnai' -author = 'Wongnai' +project = "Eastern" +copyright = "2017, Wongnai" +author = "Wongnai" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. -version = '2.0.0' +version = "2.0.0" # The full version, including alpha/beta/rc tags. -release = '2.0.0' +release = "2.0.0" # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. @@ -68,10 +68,10 @@ # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This patterns also effect to html_static_path and html_extra_path -exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] +exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' +pygments_style = "sphinx" # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = False @@ -81,7 +81,7 @@ # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # -html_theme = 'sphinx_rtd_theme' +html_theme = "sphinx_rtd_theme" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the @@ -92,7 +92,7 @@ # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] +html_static_path = ["_static"] # Custom sidebar templates, must be a dictionary that maps document names # to template names. @@ -104,7 +104,7 @@ # -- Options for HTMLHelp output ------------------------------------------ # Output file base name for HTML help builder. -htmlhelp_basename = 'Easterndoc' +htmlhelp_basename = "Easterndoc" # -- Options for LaTeX output --------------------------------------------- @@ -112,15 +112,12 @@ # The paper size ('letterpaper' or 'a4paper'). # # 'papersize': 'letterpaper', - # The font size ('10pt', '11pt' or '12pt'). # # 'pointsize': '10pt', - # Additional stuff for the LaTeX preamble. # # 'preamble': '', - # Latex figure (float) alignment # # 'figure_align': 'htbp', @@ -129,15 +126,13 @@ # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). -latex_documents = [ - (master_doc, 'Eastern.tex', 'Eastern Documentation', 'Wongnai', 'manual'), -] +latex_documents = [(master_doc, "Eastern.tex", "Eastern Documentation", "Wongnai", "manual")] # -- Options for manual page output --------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). -man_pages = [(master_doc, 'eastern', 'Eastern Documentation', [author], 1)] +man_pages = [(master_doc, "eastern", "Eastern Documentation", [author], 1)] # -- Options for Texinfo output ------------------------------------------- @@ -145,6 +140,13 @@ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - (master_doc, 'Eastern', 'Eastern Documentation', author, 'Eastern', - 'One line description of project.', 'Miscellaneous'), + ( + master_doc, + "Eastern", + "Eastern Documentation", + author, + "Eastern", + "One line description of project.", + "Miscellaneous", + ) ] diff --git a/eastern/cli.py b/eastern/cli.py index a07791c..1d65fc0 100644 --- a/eastern/cli.py +++ b/eastern/cli.py @@ -13,17 +13,15 @@ def print_info(message): - click.echo(click.style(message, fg='white', bold=True), err=True) + click.echo(click.style(message, fg="white", bold=True), err=True) def print_error(message): - click.echo(click.style(message, fg='red', bold=True), err=True) + click.echo(click.style(message, fg="red", bold=True), err=True) def format_yaml(file, namespace, edit=False, print=True, extra=[]): - env = { - 'NAMESPACE': namespace, - } + env = {"NAMESPACE": namespace} env.update(**dict(extra)) manifest = formatter.format(file, env) @@ -32,7 +30,7 @@ def format_yaml(file, namespace, edit=False, print=True, extra=[]): manifest = click.edit(manifest) if manifest is None: - click.echo('File not saved, aborting') + click.echo("File not saved, aborting") return elif print: click.echo(manifest) @@ -41,14 +39,14 @@ def format_yaml(file, namespace, edit=False, print=True, extra=[]): def deploy_from_manifest(ctx, namespace, manifest): - print_info('Deploying to namespace {}...'.format(namespace)) + print_info("Deploying to namespace {}...".format(namespace)) - ctx.obj['kubectl'].namespace = namespace + ctx.obj["kubectl"].namespace = namespace plugin = get_plugin_manager() - manifest = plugin.chain('deploy_pre_hook', manifest, ctx=ctx) - out = ctx.obj['kubectl'].apply(data=manifest.encode('utf8')) - plugin.map_method('deploy_post_hook', manifest, ctx=ctx) + manifest = plugin.chain("deploy_pre_hook", manifest, ctx=ctx) + out = ctx.obj["kubectl"].apply(data=manifest.encode("utf8")) + plugin.map_method("deploy_post_hook", manifest, ctx=ctx) return out @@ -58,27 +56,27 @@ def wait_for_rolling_deploy(ctx, namespace, manifest, timeout=None): for resource in wait_resources: ns = resource.namespace or namespace name = resource.name - print_info('Waiting for {} in namespace {} to be completed'.format( - name, ns)) + print_info("Waiting for {} in namespace {} to be completed".format(name, ns)) - ctx.obj['kubectl'].namespace = namespace - exit_code = ctx.obj['kubectl'].rollout_wait(name, timeout=timeout) + ctx.obj["kubectl"].namespace = namespace + exit_code = ctx.obj["kubectl"].rollout_wait(name, timeout=timeout) if exit_code != 0: - raise subprocess.CalledProcessError(returncode=exit_code, cmd='') + raise subprocess.CalledProcessError(returncode=exit_code, cmd="") class Timeout(Exception): pass + def wait_for_pod_to_exit(pod_name, kctl, timeout=None): time_start = time.time() phase = kctl.get_pod_phase(pod_name) - last_phase = '' + last_phase = "" - while phase not in ('', 'Succeeded'): + while phase not in ("", "Succeeded"): if phase != last_phase: - print_info('Pod {} is in phase {}'.format(pod_name, phase)) + print_info("Pod {} is in phase {}".format(pod_name, phase)) last_phase = phase if timeout is not None and time_start + timeout < time.time(): @@ -88,67 +86,50 @@ def wait_for_pod_to_exit(pod_name, kctl, timeout=None): phase = kctl.get_pod_phase(pod_name) -@click.group(context_settings={'help_option_names': ['-h', '--help']}) -@click.version_option(prog_name='Project Eastern') -@click.option('--kubectl', default='kubectl', help='Path to kubectl') -@click.option('--context', '-c', help='Kubernetes context to use') +@click.group(context_settings={"help_option_names": ["-h", "--help"]}) +@click.version_option(prog_name="Project Eastern") +@click.option("--kubectl", default="kubectl", help="Path to kubectl") +@click.option("--context", "-c", help="Kubernetes context to use") @click.pass_context @click_log.simple_verbosity_option() def cli(ctx, context, **kwargs): click_log.basic_config() - kctl = kubectl.Kubectl(kwargs['kubectl']) + kctl = kubectl.Kubectl(kwargs["kubectl"]) kctl.context = context - ctx.obj = {'kubectl': kctl} + ctx.obj = {"kubectl": kctl} def parse_set(ctx, param, value): out = [] for item in value: - if '=' not in item: - raise click.BadParameter( - '{item} must be in format KEY=value'.format(item=item)) + if "=" not in item: + raise click.BadParameter("{item} must be in format KEY=value".format(item=item)) - item = item.split('=', maxsplit=1) + item = item.split("=", maxsplit=1) out.append(item) return out @cli.command() -@click.argument('file', type=click.Path(exists=True)) -@click.argument('namespace', default='default') -@click.option( - '--set', - '-s', - callback=parse_set, - multiple=True, - help='Additional variables to set') +@click.argument("file", type=click.Path(exists=True)) +@click.argument("namespace", default="default") +@click.option("--set", "-s", callback=parse_set, multiple=True, help="Additional variables to set") def generate(file, namespace, **kwargs): - format_yaml(file, namespace, extra=kwargs['set']) + format_yaml(file, namespace, extra=kwargs["set"]) @cli.command() -@click.argument('file', type=click.Path(exists=True)) -@click.argument('namespace', default='default') -@click.option( - '--set', - '-s', - callback=parse_set, - multiple=True, - help='Additional variables to set') -@click.option( - '--edit', - '-e', - is_flag=True, - help='Edit generated manifest before deploying') -@click.option( - '--wait/--no-wait', default=True, help='Wait for deployment to finish') -@click.option( - '--timeout', default=300, help='Wait timeout (default 300s, 0 to disable)') +@click.argument("file", type=click.Path(exists=True)) +@click.argument("namespace", default="default") +@click.option("--set", "-s", callback=parse_set, multiple=True, help="Additional variables to set") +@click.option("--edit", "-e", is_flag=True, help="Edit generated manifest before deploying") +@click.option("--wait/--no-wait", default=True, help="Wait for deployment to finish") +@click.option("--timeout", default=300, help="Wait timeout (default 300s, 0 to disable)") @click.pass_context def deploy(ctx, file, namespace, edit, wait, timeout, **kwargs): - manifest = format_yaml(file, namespace, edit=edit, extra=kwargs['set']) + manifest = format_yaml(file, namespace, edit=edit, extra=kwargs["set"]) result = deploy_from_manifest(ctx, namespace, manifest) if not wait or result != 0: @@ -160,47 +141,41 @@ def deploy(ctx, file, namespace, edit, wait, timeout, **kwargs): try: wait_for_rolling_deploy(ctx, namespace, manifest, timeout) except subprocess.CalledProcessError as e: - print_error('Improper exit with code ' + str(e.returncode) + ', exiting...') + print_error("Improper exit with code " + str(e.returncode) + ", exiting...") sys.exit(3) except subprocess.TimeoutExpired: - print_error('Rollout took too long, exiting...') + print_error("Rollout took too long, exiting...") sys.exit(2) @cli.command() -@click.argument('file', type=click.Path(exists=True)) -@click.argument('namespace', default='default') -@click.argument('tag') -@click.option('--set', '-s', callback=parse_set, multiple=True) -@click.option( - '--edit', - '-e', - is_flag=True, - help='Edit generated manifest before deploying') -@click.option( - '--timeout', default=300, help='Wait timeout (default 300s, 0 to disable)') +@click.argument("file", type=click.Path(exists=True)) +@click.argument("namespace", default="default") +@click.argument("tag") +@click.option("--set", "-s", callback=parse_set, multiple=True) +@click.option("--edit", "-e", is_flag=True, help="Edit generated manifest before deploying") +@click.option("--timeout", default=300, help="Wait timeout (default 300s, 0 to disable)") @click.pass_context def job(ctx, file, namespace, tag, edit, timeout, **kwargs): exit_status = 0 - kwargs['set'].append(('IMAGE_TAG', tag)) - manifest = format_yaml( - file, namespace, edit=edit, extra=kwargs['set'], print=False) + kwargs["set"].append(("IMAGE_TAG", tag)) + manifest = format_yaml(file, namespace, edit=edit, extra=kwargs["set"], print=False) # Modify the name to contain imageTag manifest = list(yaml.load_all(manifest)) found_job = False for item in manifest: - if item['kind'] != 'Job': + if item["kind"] != "Job": continue found_job = True - name = '{}-{}'.format(item['metadata']['name'], tag) + name = "{}-{}".format(item["metadata"]["name"], tag) name = name.lower()[:63] - item['metadata']['name'] = name + item["metadata"]["name"] = name if not found_job: - print_error('Manifest does not contains any job') + print_error("Manifest does not contains any job") sys.exit(1) manifest = yaml.dump_all(manifest) @@ -211,13 +186,13 @@ def job(ctx, file, namespace, tag, edit, timeout, **kwargs): if result != 0: sys.exit(result) - job = job_manager.JobManager(ctx.obj['kubectl'], name) + job = job_manager.JobManager(ctx.obj["kubectl"], name) try: # Wait pod to be started job.wait_pod_scheduled() - if timeout == 0: # timeout = 0 means disable + if timeout == 0: # timeout = 0 means disable timeout = None # Wait until job complete job.wait_completion(idle_timeout=timeout) @@ -225,37 +200,37 @@ def job(ctx, file, namespace, tag, edit, timeout, **kwargs): pod_name = job.get_pod_name() try: # Just print only one pod - click.echo(ctx.obj['kubectl'].get_pod_log(pod_name)) + click.echo(ctx.obj["kubectl"].get_pod_log(pod_name)) except subprocess.SubprocessError: - print_error( - 'Cannot read log of pod {}, dumping pod data'.format(pod_name)) - click.echo(yaml.dump(ctx.obj['kubectl'].get_pod(pod_name))) + print_error("Cannot read log of pod {}, dumping pod data".format(pod_name)) + click.echo(yaml.dump(ctx.obj["kubectl"].get_pod(pod_name))) if not job.is_succeeded(): - print_error('Job failed...') + print_error("Job failed...") exit_status = 3 except Timeout: - print_error('Timed out, exiting...') + print_error("Timed out, exiting...") exit_status = 2 except KeyboardInterrupt: pass finally: try: - print_info('Cleaning up job {}'.format(name)) + print_info("Cleaning up job {}".format(name)) job.remove() except job_manager.JobOperationException as e: print_error('Failed to cleanup job "{}"!'.format(name)) print_error(e) print_info( - 'To cleanup manually, run `{kubectl} delete job {name}`'.format( - kubectl=' '.join(ctx.obj['kubectl'].get_launch_args()), - name=name)) + "To cleanup manually, run `{kubectl} delete job {name}`".format( + kubectl=" ".join(ctx.obj["kubectl"].get_launch_args()), name=name + ) + ) exit_status = 1 sys.exit(exit_status) -if sys.platform == 'win32': +if sys.platform == "win32": loop = asyncio.ProactorEventLoop() asyncio.set_event_loop(loop) diff --git a/eastern/formatter.py b/eastern/formatter.py index c739b14..744ed93 100644 --- a/eastern/formatter.py +++ b/eastern/formatter.py @@ -5,8 +5,7 @@ class BaseFormatter(ABC): - - def __init__(self, raw, path='', env=None): + def __init__(self, raw, path="", env=None): self.raw = raw if not isinstance(path, PurePath): @@ -29,8 +28,8 @@ def format(filename, env=None): :type filename: str or :py:class:`pathlib.Path` :param dict[str,str] env: List of variables """ - ext = str(filename).split('.')[-1] - driver = DriverManager('eastern.formatter', ext) + ext = str(filename).split(".")[-1] + driver = DriverManager("eastern.formatter", ext) driver.propagate_map_exceptions = True env = env or {} diff --git a/eastern/job_manager.py b/eastern/job_manager.py index 5d8bca1..f6fb47a 100644 --- a/eastern/job_manager.py +++ b/eastern/job_manager.py @@ -5,7 +5,7 @@ logger = logging.getLogger(__name__) -WAIT_POD_READY_TIMEOUT = 30 * 60 # 30 mins +WAIT_POD_READY_TIMEOUT = 30 * 60 # 30 mins class JobManager(object): @@ -62,8 +62,7 @@ def wait_completion(self, idle_timeout=None): :param idle_timeout int: time to wait in seconds """ if not self.is_pod_scheduled(): - raise JobOperationException( - "Wait for completion must run after pod was scheduled") + raise JobOperationException("Wait for completion must run after pod was scheduled") pod_names = self.get_pod_names() for pod_name in pod_names: @@ -71,10 +70,9 @@ def wait_completion(self, idle_timeout=None): return # wait for pod phrase to be ready before tail log - retry(lambda: is_pod_phrase_can_get_log( - self.kubectl.get_pod_phase(pod_name)), count=WAIT_POD_READY_TIMEOUT) + retry(lambda: is_pod_phrase_can_get_log(self.kubectl.get_pod_phase(pod_name)), count=WAIT_POD_READY_TIMEOUT) - args = self.kubectl.get_launch_args() + ['logs', '-f', pod_name] + args = self.kubectl.get_launch_args() + ["logs", "-f", pod_name] ProcessTimeout(idle_timeout, *args).run_sync() def get_pod_name(self): @@ -107,7 +105,8 @@ def remove(self): exit_code = self.kubectl.delete_job(self.job_name) if exit_code != 0: raise JobOperationException( - "remove job {name} failed, exit code {exit_code}".format(name=self.job_name, exit_code=exit_code)) + "remove job {name} failed, exit code {exit_code}".format(name=self.job_name, exit_code=exit_code) + ) def retry(bool_fn, count=10, interval=1): @@ -121,7 +120,7 @@ def retry(bool_fn, count=10, interval=1): def is_pod_phrase_can_get_log(phrase): - logger.debug('Pod phrase: %s', phrase) + logger.debug("Pod phrase: %s", phrase) return phrase and phrase.lower() in ["running", "succeeded", "failed"] diff --git a/eastern/kubectl.py b/eastern/kubectl.py index b4ec982..dc251f1 100644 --- a/eastern/kubectl.py +++ b/eastern/kubectl.py @@ -10,12 +10,13 @@ class Kubectl: :param str path: Path to kubectl (or command name if in path) """ + #: Current namespace (default to omit parameter) namespace = None #: Current context (default to omit parameter) context = None - def __init__(self, path='kubectl'): + def __init__(self, path="kubectl"): self.path = path def get_launch_args(self): @@ -29,14 +30,14 @@ def get_launch_args(self): out = [self.path] if self.namespace: - out.extend(['--namespace', self.namespace]) + out.extend(["--namespace", self.namespace]) if self.context: - out.extend(['--context', self.context]) + out.extend(["--context", self.context]) return out - def apply(self, file='-', data=None): + def apply(self, file="-", data=None): """ Run :command:`kubectl apply` @@ -44,13 +45,7 @@ def apply(self, file='-', data=None): :param str data: Manifest body to apply, only when ``file`` is ``-`` :return: Return value of the command (0 for success) """ - process = subprocess.Popen( - self.get_launch_args() + [ - 'apply', - '-f', - file, - ], - stdin=subprocess.PIPE) + process = subprocess.Popen(self.get_launch_args() + ["apply", "-f", file], stdin=subprocess.PIPE) process.communicate(data) @@ -63,11 +58,7 @@ def rollout_wait(self, name, timeout=None): :param str name: Deployment name to wait :return: Return value of the command (0 for success) """ - args = self.get_launch_args() + [ - 'rollout', - 'status', - name, - ] + args = self.get_launch_args() + ["rollout", "status", name] if timeout: return ProcessTimeout(timeout, *args).run_sync() @@ -82,15 +73,22 @@ def get_job_pod_name(self, name): :return: Job name :raises JobNotFound: If no pod for that job is found """ - out = subprocess.check_output(self.get_launch_args() + [ - 'get', 'pod', '--all-namespaces', '--selector=job-name={}'.format(name), '-o', - 'jsonpath={.items..metadata.name}' - ]) + out = subprocess.check_output( + self.get_launch_args() + + [ + "get", + "pod", + "--all-namespaces", + "--selector=job-name={}".format(name), + "-o", + "jsonpath={.items..metadata.name}", + ] + ) if not out.strip(): raise JobNotFound - return out.decode('utf8') + return out.decode("utf8") def get_pod_phase(self, name): """ @@ -99,9 +97,9 @@ def get_pod_phase(self, name): :param str name: Pod name :return: `Pod phase `_ """ - return subprocess.check_output(self.get_launch_args( - ) + ['get', 'pod', name, '-o', 'jsonpath={.status.phase}']).decode( - 'utf8') + return subprocess.check_output( + self.get_launch_args() + ["get", "pod", name, "-o", "jsonpath={.status.phase}"] + ).decode("utf8") def get_pod_log(self, name): """ @@ -110,7 +108,7 @@ def get_pod_log(self, name): :param str name: Pod name :rtype: str """ - return subprocess.check_output(self.get_launch_args() + ['logs', name]) + return subprocess.check_output(self.get_launch_args() + ["logs", name]) def get_pod(self, name): """ @@ -119,9 +117,7 @@ def get_pod(self, name): :param str name: Pod name :rtype: dict """ - return json.loads( - subprocess.check_output( - self.get_launch_args() + ['get', 'pod', name, '-o', 'json'])) + return json.loads(subprocess.check_output(self.get_launch_args() + ["get", "pod", name, "-o", "json"])) def delete_job(self, name): """ @@ -130,11 +126,7 @@ def delete_job(self, name): :param str name: Job name :return: Return value (0 for success) """ - return subprocess.call(self.get_launch_args() + [ - 'delete', - 'job', - name, - ]) + return subprocess.call(self.get_launch_args() + ["delete", "job", name]) def get_job_status(self, name): """ @@ -144,9 +136,8 @@ def get_job_status(self, name): :rtype: JobStatus :raises: KeyError in case there is no status in response """ - job = subprocess.check_output( - self.get_launch_args() + ['get', 'job', name, '-o', 'json']) - return JobStatus(json.loads(job)['status']) + job = subprocess.check_output(self.get_launch_args() + ["get", "job", name, "-o", "json"]) + return JobStatus(json.loads(job)["status"]) class JobStatus(object): @@ -155,15 +146,15 @@ def __init__(self, json_dict): @property def succeeded(self): - return self.json_dict.get('succeeded', 0) + return self.json_dict.get("succeeded", 0) @property def active(self): - return self.json_dict.get('active', 0) + return self.json_dict.get("active", 0) @property def failed(self): - return self.json_dict.get('failed', 0) + return self.json_dict.get("failed", 0) class KubernetesException(Exception): @@ -176,4 +167,5 @@ class JobNotFound(KubernetesException): Raised by :py:func:`Kubectl.get_job_pod_name` """ + pass diff --git a/eastern/kubeyml_helper.py b/eastern/kubeyml_helper.py index 11e5f0f..94aa64c 100644 --- a/eastern/kubeyml_helper.py +++ b/eastern/kubeyml_helper.py @@ -7,20 +7,20 @@ def __init__(self, yml): @property def name(self): - return "{}/{}".format(self.yml['kind'], self.yml['metadata']['name']) + return "{}/{}".format(self.yml["kind"], self.yml["metadata"]["name"]) @property def namespace(self): - if 'namespace' in self.yml['metadata']: - return self.yml['metadata']['namespace'] + if "namespace" in self.yml["metadata"]: + return self.yml["metadata"]["namespace"] return None def is_rolling_resource(doc): - if not doc or 'kind' not in doc: + if not doc or "kind" not in doc: return False - return doc['kind'] in ['Deployment', 'ReplicaSet', 'DaemonSet'] + return doc["kind"] in ["Deployment", "ReplicaSet", "DaemonSet"] def get_supported_rolling_resources(text): diff --git a/eastern/plugin.py b/eastern/plugin.py index fdcee71..6730fe9 100644 --- a/eastern/plugin.py +++ b/eastern/plugin.py @@ -120,22 +120,19 @@ def map(self, *args, **kwargs): return [] -class ExtensionChainManager(ChainMixin, MapIgnoreEmptyMixin, - extension.ExtensionManager): +class ExtensionChainManager(ChainMixin, MapIgnoreEmptyMixin, extension.ExtensionManager): pass -class ExtensionMayEmptyManager(MapIgnoreEmptyMixin, - extension.ExtensionManager): +class ExtensionMayEmptyManager(MapIgnoreEmptyMixin, extension.ExtensionManager): pass @functools.lru_cache(None) def get_plugin_manager(): - return ExtensionChainManager( - namespace='eastern.plugin', invoke_on_load=True) + return ExtensionChainManager(namespace="eastern.plugin", invoke_on_load=True) @functools.lru_cache(None) def get_cli_manager(): - return ExtensionMayEmptyManager(namespace='eastern.cli') + return ExtensionMayEmptyManager(namespace="eastern.cli") diff --git a/eastern/timeout.py b/eastern/timeout.py index 292dccc..e1b86fc 100644 --- a/eastern/timeout.py +++ b/eastern/timeout.py @@ -8,8 +8,8 @@ class ProcessTimeout: _timed_out = False def __init__(self, timeout, *args, **kwargs): - assert 'stdin' not in kwargs - assert 'stdout' not in kwargs + assert "stdin" not in kwargs + assert "stdout" not in kwargs self.timeout = timeout self.args = args @@ -25,10 +25,8 @@ def stop(self): async def run(self, loop): self._subprocess = await asyncio.create_subprocess_exec( - stdout=asyncio.subprocess.PIPE, - stderr=sys.stderr, - *self.args, - **self.kwargs) + stdout=asyncio.subprocess.PIPE, stderr=sys.stderr, *self.args, **self.kwargs + ) terminated = asyncio.ensure_future(self._subprocess.wait()) try: self.set_timeout(loop) @@ -36,7 +34,7 @@ async def run(self, loop): last_line = None while not terminated.done() and not self._timed_out: try: - line = await self._subprocess.stdout.readuntil(b'\n') + line = await self._subprocess.stdout.readuntil(b"\n") except asyncio.IncompleteReadError as e: sys.stdout.buffer.write(e.partial) break diff --git a/eastern/yaml_formatter/formatter.py b/eastern/yaml_formatter/formatter.py index ba27910..8fe9070 100644 --- a/eastern/yaml_formatter/formatter.py +++ b/eastern/yaml_formatter/formatter.py @@ -9,7 +9,7 @@ from ..plugin import get_plugin_manager from ..formatter import BaseFormatter -DRIVER_NS = 'eastern.command' +DRIVER_NS = "eastern.command" class Formatter(BaseFormatter): @@ -18,13 +18,14 @@ class Formatter(BaseFormatter): :param str path: Path to template :param dict[str,str] env: List of variables """ + logger = logging.getLogger(__name__) - def __init__(self, raw, path='', env=None): + def __init__(self, raw, path="", env=None): super().__init__(raw, path, env) self.plugin = get_plugin_manager() - envs = self.plugin.map_method('env_hook', formatter=self) + envs = self.plugin.map_method("env_hook", formatter=self) for item in envs: self.env.update(**item) @@ -34,43 +35,40 @@ def format(self): :return: Formatted template """ self.body = self.raw - self.body = self.plugin.chain( - 'format_pre_hook', self.body, formatter=self) + self.body = self.plugin.chain("format_pre_hook", self.body, formatter=self) self.body = self.interpolate_env(self.body) self.body = self.parse_lines(self.body) - self.body = self.plugin.chain( - 'format_post_hook', self.body, formatter=self) + self.body = self.plugin.chain("format_post_hook", self.body, formatter=self) return self.body def interpolate_env(self, text): - return re.sub(r'\${([^}]+)}', self.replace_env, text) + return re.sub(r"\${([^}]+)}", self.replace_env, text) def replace_env(self, match): key = match.group(1) if key in self.env: return self.env[key] else: - self.logger.warning('Interpolated variable not found: %s', key) + self.logger.warning("Interpolated variable not found: %s", key) return match.group() def parse_lines(self, body): body_lines = body.split(os.linesep) - return os.linesep.join( - utils.flatten([self.parse_line(line) for line in body_lines])) + return os.linesep.join(utils.flatten([self.parse_line(line) for line in body_lines])) def parse_line(self, line): - if '#' not in line: + if "#" not in line: return line - line = self.plugin.chain('line_pre_hook', line, formatter=self) - before, after = line.split('#', 1) + line = self.plugin.chain("line_pre_hook", line, formatter=self) + before, after = line.split("#", 1) # line must only have precending spaces - if not re.match(r'^\s*$', before): + if not re.match(r"^\s*$", before): return line - splitted = after.strip().split(' ', 1) + splitted = after.strip().split(" ", 1) command = splitted[0] args = [] @@ -81,7 +79,7 @@ def parse_line(self, line): func = DriverManager(DRIVER_NS, command) func.propagate_map_exceptions = True except NoMatches: - self.logger.debug('Command not found %s', command, exc_info=True) + self.logger.debug("Command not found %s", command, exc_info=True) return line output = func(lambda ext: ext.plugin(args, line=line, formatter=self)) @@ -93,5 +91,5 @@ def parse_line(self, line): output = os.linesep.join([before + item for item in output]) - output = self.plugin.chain('line_post_hook', output, formatter=self) + output = self.plugin.chain("line_post_hook", output, formatter=self) return output diff --git a/eastern/yaml_formatter/overrides.py b/eastern/yaml_formatter/overrides.py index ce417d9..faaaf0c 100644 --- a/eastern/yaml_formatter/overrides.py +++ b/eastern/yaml_formatter/overrides.py @@ -5,17 +5,17 @@ def load(args, formatter, required=False, **kwargs): - file_list = args.split(',') + file_list = args.split(",") override_file = utils.resolve_file(file_list, formatter.path) if not override_file: - error = 'Cannot load ' + ', '.join(file_list) + error = "Cannot load " + ", ".join(file_list) if required == True: raise OSError(error) else: - return '# ' + error + return "# " + error - return fmt.format(override_file, env=formatter.env).rstrip('\r\n') + return fmt.format(override_file, env=formatter.env).rstrip("\r\n") def load_strict(args, **kwargs): diff --git a/eastern/yaml_formatter/utils.py b/eastern/yaml_formatter/utils.py index 96ea0ca..b1f7841 100644 --- a/eastern/yaml_formatter/utils.py +++ b/eastern/yaml_formatter/utils.py @@ -12,9 +12,9 @@ def flatten(l): def resolve_file(files, base): - '''Resolve a list of files to the first one that exists - - Returns a path object or :py:const:`None` if no file exists''' + """Resolve a list of files to the first one that exists + + Returns a path object or :py:const:`None` if no file exists""" if not isinstance(base, Path): base = Path(base) diff --git a/example/overrides/ingress-prod.yaml b/example/overrides/ingress-prod.yaml index bd7b478..6a8fbf4 100644 --- a/example/overrides/ingress-prod.yaml +++ b/example/overrides/ingress-prod.yaml @@ -4,10 +4,10 @@ metadata: name: sentry spec: rules: - - host: sentry-prod.mysite.com - http: - paths: - - path: / - backend: - serviceName: sentry - servicePort: 80 + - host: sentry-prod.mysite.com + http: + paths: + - path: / + backend: + serviceName: sentry + servicePort: 80 diff --git a/example/overrides/ingress.yaml b/example/overrides/ingress.yaml index 7fb6692..fa8cf49 100644 --- a/example/overrides/ingress.yaml +++ b/example/overrides/ingress.yaml @@ -4,10 +4,10 @@ metadata: name: sentry spec: rules: - - host: sentry.mysite.com - http: - paths: - - path: / - backend: - serviceName: sentry - servicePort: 80 + - host: sentry.mysite.com + http: + paths: + - path: / + backend: + serviceName: sentry + servicePort: 80 diff --git a/example/sentry.yaml b/example/sentry.yaml index b544b29..f71a98f 100644 --- a/example/sentry.yaml +++ b/example/sentry.yaml @@ -14,11 +14,11 @@ spec: app: sentry spec: containers: - - name: sentry - image: sentry:${IMAGE_TAG} - resources: - requests: - memory: "64Mi" - cpu: "1m" - env: - # load! overrides/env-${NAMESPACE}.yaml, overrides/env.yaml + - name: sentry + image: sentry:${IMAGE_TAG} + resources: + requests: + memory: "64Mi" + cpu: "1m" + env: + # load! overrides/env-${NAMESPACE}.yaml, overrides/env.yaml diff --git a/setup.py b/setup.py index 1a9f8a7..e3b3be0 100644 --- a/setup.py +++ b/setup.py @@ -3,35 +3,30 @@ from setuptools import setup, find_packages setup( - name='eastern', - description='Simple Kubernetes Deployment', - long_description=open(os.path.join(os.path.dirname(__file__), 'README.md')).read(), - long_description_content_type='text/markdown', - version='4.4.1', + name="eastern", + description="Simple Kubernetes Deployment", + long_description=open(os.path.join(os.path.dirname(__file__), "README.md")).read(), + long_description_content_type="text/markdown", + version="4.4.1", packages=find_packages(), - url='https://github.com/wongnai/eastern', - install_requires=[ - 'Click~=6.7', - 'click-log~=0.3.2', - 'PyYAML~=4.2b4', - 'stevedore~=1.29.0', - ], - setup_requires=['pytest-runner'], - tests_require=['pytest', 'pytest-asyncio'], + url="https://github.com/wongnai/eastern", + install_requires=["Click~=6.7", "click-log~=0.3.2", "PyYAML~=4.2b4", "stevedore~=1.29.0", "pre-commit~=1.18.3"], + setup_requires=["pytest-runner"], + tests_require=["pytest", "pytest-asyncio"], entry_points={ - 'console_scripts': ['eastern = eastern.cli:cli'], - 'eastern.command': [ - 'load? = eastern.yaml_formatter.overrides:load', - 'load! = eastern.yaml_formatter.overrides:load_strict', + "console_scripts": ["eastern = eastern.cli:cli"], + "eastern.command": [ + "load? = eastern.yaml_formatter.overrides:load", + "load! = eastern.yaml_formatter.overrides:load_strict", ], - 'eastern.formatter': ['yaml = eastern.yaml_formatter:Formatter'], + "eastern.formatter": ["yaml = eastern.yaml_formatter:Formatter"], }, classifiers=[ - 'Development Status :: 5 - Production/Stable', - 'Intended Audience :: System Administrators', - 'License :: OSI Approved :: MIT License', - 'Programming Language :: Python :: 3 :: Only', - 'Topic :: System :: Systems Administration', + "Development Status :: 5 - Production/Stable", + "Intended Audience :: System Administrators", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3 :: Only", + "Topic :: System :: Systems Administration", ], - license='MIT' + license="MIT", ) diff --git a/test_data/kubeyml_helper/sentry.yaml b/test_data/kubeyml_helper/sentry.yaml index f9ee155..89ed463 100644 --- a/test_data/kubeyml_helper/sentry.yaml +++ b/test_data/kubeyml_helper/sentry.yaml @@ -19,13 +19,13 @@ metadata: namespace: production spec: rules: - - host: sentry.mysite.com - http: - paths: - - path: / - backend: - serviceName: sentry - servicePort: 80 + - host: sentry.mysite.com + http: + paths: + - path: / + backend: + serviceName: sentry + servicePort: 80 --- apiVersion: apps/v1beta1 kind: Deployment @@ -40,21 +40,22 @@ spec: app: sentry spec: containers: - - name: sentry - image: sentry:1.0 - resources: - requests: - memory: "64Mi" - cpu: "1m" - env: - - name: SENTRY_POSTGRES_HOST - value: postgres - - name: SENTRY_DB_NAME - value: postgres - - name: SENTRY_DB_USER - value: postgres - - name: SENTRY_DB_PASSWORD - value: postgres - - name: SENTRY_SECRET_KEY - value: example + - name: sentry + image: sentry:1.0 + resources: + requests: + memory: "64Mi" + cpu: "1m" + env: + - name: SENTRY_POSTGRES_HOST + value: postgres + - name: SENTRY_DB_NAME + value: postgres + - name: SENTRY_DB_USER + value: postgres + - name: SENTRY_DB_PASSWORD + value: postgres + - name: SENTRY_SECRET_KEY + value: example --- + diff --git a/tests/test_job_manager.py b/tests/test_job_manager.py index 8f7f866..de3d580 100644 --- a/tests/test_job_manager.py +++ b/tests/test_job_manager.py @@ -8,20 +8,24 @@ MULTI_POD_NAMES = "test-job-9vmlg test-job-2abcd" SINGLE_POD_NAMES = "test-job-9vmlg" + @pytest.fixture def kubectl(): return MagicMock(Kubectl) + def test_get_pod_names(kubectl): kubectl.get_job_pod_name.return_value = MULTI_POD_NAMES job_manager = JobManager(kubectl, JOB_NAME) assert job_manager.get_pod_names() == ["test-job-2abcd", "test-job-9vmlg"] + def test_get_pod_names_with_single_pod(kubectl): kubectl.get_job_pod_name.return_value = SINGLE_POD_NAMES job_manager = JobManager(kubectl, JOB_NAME) assert job_manager.get_pod_names() == ["test-job-9vmlg"] + def test_get_pod_name(kubectl): kubectl.get_job_pod_name.return_value = MULTI_POD_NAMES job_manager = JobManager(kubectl, JOB_NAME) diff --git a/tests/test_kubectl.py b/tests/test_kubectl.py index 9e0b50a..b66b5c0 100644 --- a/tests/test_kubectl.py +++ b/tests/test_kubectl.py @@ -21,24 +21,23 @@ } """ -NO_STATUS_JOB_RESPONSE = '{}' +NO_STATUS_JOB_RESPONSE = "{}" -@patch('subprocess.check_output') +@patch("subprocess.check_output") def test_get_job_status(check_output): check_output.return_value = JOB_RESPONSE kubectl = Kubectl() status = kubectl.get_job_status("test-job") - check_output.assert_called_with( - ['kubectl', 'get', 'job', 'test-job', '-o', 'json']) + check_output.assert_called_with(["kubectl", "get", "job", "test-job", "-o", "json"]) assert status.succeeded == 1 assert status.active == 0 assert status.failed == 0 -@patch('subprocess.check_output') +@patch("subprocess.check_output") def test_get_job_status_failed(check_output): check_output.return_value = NO_STATUS_JOB_RESPONSE kubectl = Kubectl() diff --git a/tests/test_kubeyml_helper.py b/tests/test_kubeyml_helper.py index 40ea867..7612ff8 100644 --- a/tests/test_kubeyml_helper.py +++ b/tests/test_kubeyml_helper.py @@ -2,13 +2,12 @@ from eastern import kubeyml_helper -TEST_ROOT = (Path(__file__).parents[1]) / 'test_data' / 'kubeyml_helper' +TEST_ROOT = (Path(__file__).parents[1]) / "test_data" / "kubeyml_helper" def test_get_supported_rolling_resources(): - yaml_file = TEST_ROOT / 'sentry.yaml' - resources = kubeyml_helper.get_supported_rolling_resources( - yaml_file.open().read()) + yaml_file = TEST_ROOT / "sentry.yaml" + resources = kubeyml_helper.get_supported_rolling_resources(yaml_file.open().read()) assert len(resources) == 1 diff --git a/tests/test_overrides.py b/tests/test_overrides.py index fffb096..4d19285 100644 --- a/tests/test_overrides.py +++ b/tests/test_overrides.py @@ -4,7 +4,7 @@ from eastern import formatter -TEST_ROOT = (Path(__file__).parents[1]) / 'test_data' / 'overrides' +TEST_ROOT = (Path(__file__).parents[1]) / "test_data" / "overrides" def assert_format_equal(file_1, file_2, env={}): @@ -17,39 +17,31 @@ def assert_format_equal(file_1, file_2, env={}): def test_load(): - assert_format_equal(TEST_ROOT / 'load' / 'load.yaml', - TEST_ROOT / 'load' / 'expect.yaml') + assert_format_equal(TEST_ROOT / "load" / "load.yaml", TEST_ROOT / "load" / "expect.yaml") def test_load_env(): assert_format_equal( - TEST_ROOT / 'load_env' / 'load.yaml', - TEST_ROOT / 'load_env' / 'expect.yaml', - env={ - 'NAMESPACE': 'default' - }) + TEST_ROOT / "load_env" / "load.yaml", TEST_ROOT / "load_env" / "expect.yaml", env={"NAMESPACE": "default"} + ) def test_load_required(): - assert_format_equal(TEST_ROOT / 'load_required' / 'load.yaml', - TEST_ROOT / 'load_required' / 'expect.yaml') + assert_format_equal(TEST_ROOT / "load_required" / "load.yaml", TEST_ROOT / "load_required" / "expect.yaml") def test_load_required_not_found(): with pytest.raises(OSError): - result = formatter.format( - TEST_ROOT / 'load_required_not_found' / 'load.yaml') + result = formatter.format(TEST_ROOT / "load_required_not_found" / "load.yaml") def test_load_default_file(): - assert_format_equal(TEST_ROOT / 'load_default' / 'load.yaml', - TEST_ROOT / 'load_default' / 'expect.yaml') + assert_format_equal(TEST_ROOT / "load_default" / "load.yaml", TEST_ROOT / "load_default" / "expect.yaml") def test_load_not_exists(): assert_format_equal( - TEST_ROOT / 'load_not_exists' / 'load.yaml', - TEST_ROOT / 'load_not_exists' / 'expect.yaml', - env={ - 'NAMESPACE': 'notexists' - }) + TEST_ROOT / "load_not_exists" / "load.yaml", + TEST_ROOT / "load_not_exists" / "expect.yaml", + env={"NAMESPACE": "notexists"}, + ) diff --git a/tests/test_timeout.py b/tests/test_timeout.py index aada2c1..85b5a40 100644 --- a/tests/test_timeout.py +++ b/tests/test_timeout.py @@ -6,20 +6,23 @@ def test_passed(): - assert ProcessTimeout(0.01, 'echo').run_sync() == 0 + assert ProcessTimeout(0.01, "echo").run_sync() == 0 + def test_timeout_repeated_output(): - with pytest.raises(subprocess.TimeoutExpired): - ProcessTimeout(0.01, 'yes').run_sync() + with pytest.raises(subprocess.TimeoutExpired): + ProcessTimeout(0.01, "yes").run_sync() + def test_timeout(): - with pytest.raises(subprocess.TimeoutExpired): - ProcessTimeout(0.01, 'sleep', '10').run_sync() + with pytest.raises(subprocess.TimeoutExpired): + ProcessTimeout(0.01, "sleep", "10").run_sync() + @pytest.mark.asyncio async def test_never_timeout(event_loop): - process = ProcessTimeout(0.2, 'cat', '/dev/urandom') - process_status = asyncio.ensure_future(process.run(event_loop)) - await asyncio.sleep(0.01) - assert not process_status.done(), 'Process should be running, but got ' + repr(process_status.result()) - process_status.cancel() + process = ProcessTimeout(0.2, "cat", "/dev/urandom") + process_status = asyncio.ensure_future(process.run(event_loop)) + await asyncio.sleep(0.01) + assert not process_status.done(), "Process should be running, but got " + repr(process_status.result()) + process_status.cancel()