From fff70ad86173d200bc70331e8f3fc18c3a9e2eb7 Mon Sep 17 00:00:00 2001 From: "Ian H. Pittwood" Date: Thu, 26 Sep 2019 11:34:04 -0500 Subject: [PATCH] Refactors pegleg CLI to use single commands Debugging pegleg can currently be difficult and the Click CLI does not easily allow debuggers like pdb or PyCharm to use breakpoints. By moving all CLI command calls into singular functions, we can easily create an "if __name__ == '__main__'" entry point to call these functions and investigate any bugs that may arise. We also gain the ability to reuse more portions of our code by refactoring these methods. Change-Id: Ia9739931273eb6458f82dbb7e702a505ae397ae3 --- pegleg/cli/__init__.py | 0 pegleg/{cli.py => cli/commands.py} | 426 +++++------------- pegleg/cli/utils.py | 124 +++++ pegleg/engine/secrets.py | 3 +- pegleg/pegleg_main.py | 412 +++++++++++++++++ setup.py | 2 +- tests/unit/cli/__init__.py | 0 .../{test_cli.py => cli/test_commands.py} | 64 +-- tests/unit/engine/test_secrets.py | 2 +- 9 files changed, 680 insertions(+), 353 deletions(-) create mode 100644 pegleg/cli/__init__.py rename pegleg/{cli.py => cli/commands.py} (60%) create mode 100644 pegleg/cli/utils.py create mode 100644 pegleg/pegleg_main.py create mode 100644 tests/unit/cli/__init__.py rename tests/unit/{test_cli.py => cli/test_commands.py} (92%) diff --git a/pegleg/cli/__init__.py b/pegleg/cli/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/pegleg/cli.py b/pegleg/cli/commands.py similarity index 60% rename from pegleg/cli.py rename to pegleg/cli/commands.py index 697036a9..be5879b2 100644 --- a/pegleg/cli.py +++ b/pegleg/cli/commands.py @@ -1,4 +1,4 @@ -# Copyright 2018 AT&T Intellectual Property. All other rights reserved. +# Copyright 2019 AT&T Intellectual Property. All other rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -12,120 +12,21 @@ # See the License for the specific language governing permissions and # limitations under the License. -import functools import logging -import os import warnings import click -from pegleg import config -from pegleg import engine -from pegleg.engine import bundle -from pegleg.engine import catalog -from pegleg.engine.secrets import wrap_secret -from pegleg.engine.util import files -from pegleg.engine.util.shipyard_helper import ShipyardHelper +from pegleg.cli import utils +from pegleg import pegleg_main LOG = logging.getLogger(__name__) -LOG_FORMAT = '%(asctime)s %(levelname)-8s %(name)s:%(funcName)s [%(lineno)3d] %(message)s' # noqa - CONTEXT_SETTINGS = { 'help_option_names': ['-h', '--help'], } -def _process_repositories_callback(ctx, param, value): - """Convenient callback for ``@click.argument(site_name)``. - - Automatically processes repository information for the specified site. This - entails cloning all requires repositories and checking out specified - references for each repository. - """ - engine.repository.process_repositories(value) - return value - - -MAIN_REPOSITORY_OPTION = click.option( - '-r', - '--site-repository', - 'site_repository', - required=True, - help='Path or URL to the primary repository (containing ' - 'site_definition.yaml) repo.') - -EXTRA_REPOSITORY_OPTION = click.option( - '-e', - '--extra-repository', - 'extra_repositories', - multiple=True, - help='Path or URL of additional repositories. These should be named per ' - 'the site-definition file, e.g. -e global=/opt/global -e ' - 'secrets=/opt/secrets. By default, the revision specified in the ' - 'site-definition for the site will be leveraged but can be ' - 'overridden using -e global=/opt/global@revision.') - -REPOSITORY_KEY_OPTION = click.option( - '-k', - '--repo-key', - 'repo_key', - help='The SSH public key to use when cloning remote authenticated ' - 'repositories.') - -REPOSITORY_USERNAME_OPTION = click.option( - '-u', - '--repo-username', - 'repo_username', - help='The SSH username to use when cloning remote authenticated ' - 'repositories specified in the site-definition file. Any ' - 'occurrences of REPO_USERNAME will be replaced with this ' - 'value.\n' - 'Use only if REPO_USERNAME appears in a repo URL.') - -REPOSITORY_CLONE_PATH_OPTION = click.option( - '-p', - '--clone-path', - 'clone_path', - help='The path where the repo will be cloned. By default the repo will be ' - 'cloned to the /tmp path. If this option is ' - 'included and the repo already ' - 'exists, then the repo will not be cloned again and the ' - 'user must specify a new clone path or pass in the local copy ' - 'of the repository as the site repository. Suppose the repo ' - 'name is airship/treasuremap and the clone path is ' - '/tmp/mypath then the following directory is ' - 'created /tmp/mypath/airship/treasuremap ' - 'which will contain the contents of the repo') - -ALLOW_MISSING_SUBSTITUTIONS_OPTION = click.option( - '-f', - '--fail-on-missing-sub-src', - required=False, - type=click.BOOL, - default=True, - show_default=True, - help='Raise Deckhand exception on missing substition sources.') - -EXCLUDE_LINT_OPTION = click.option( - '-x', - '--exclude', - 'exclude_lint', - multiple=True, - help='Excludes specified linting checks. Warnings will still be issued. ' - '-w takes priority over -x.') - -WARN_LINT_OPTION = click.option( - '-w', - '--warn', - 'warn_lint', - multiple=True, - help='Warn if linting check fails. -w takes priority over -x.') - -SITE_REPOSITORY_ARGUMENT = click.argument( - 'site_name', callback=_process_repositories_callback) - - @click.group(context_settings=CONTEXT_SETTINGS) @click.option( '-v', @@ -153,70 +54,50 @@ def main(*, verbose, logging_level): * repo: repository-level actions """ - lvl = logging_level - if verbose: - lvl = logging.DEBUG - logging.basicConfig(format=LOG_FORMAT, level=int(lvl)) + pegleg_main.set_logging_level(verbose, logging_level) @main.group(help='Commands related to repositories') -@MAIN_REPOSITORY_OPTION -@REPOSITORY_CLONE_PATH_OPTION +@utils.MAIN_REPOSITORY_OPTION +@utils.REPOSITORY_CLONE_PATH_OPTION # TODO(felipemonteiro): Support EXTRA_REPOSITORY_OPTION as well to be # able to lint multiple repos together. -@REPOSITORY_USERNAME_OPTION -@REPOSITORY_KEY_OPTION +@utils.REPOSITORY_USERNAME_OPTION +@utils.REPOSITORY_KEY_OPTION def repo(*, site_repository, clone_path, repo_key, repo_username): """Group for repo-level actions, which include: * lint: lint all sites across the repository - """ - - config.set_site_repo(site_repository) - config.set_clone_path(clone_path) - config.set_repo_key(repo_key) - config.set_repo_username(repo_username) - config.set_umask() + pegleg_main.run_config( + site_repository, + clone_path, + repo_key, + repo_username, [], + run_umask=True) -def _lint_helper( - *, fail_on_missing_sub_src, exclude_lint, warn_lint, site_name=None): - """Helper for executing lint on specific site or all sites in repo.""" - if site_name: - func = functools.partial(engine.lint.site, site_name=site_name) - else: - func = engine.lint.full - warns = func( - fail_on_missing_sub_src=fail_on_missing_sub_src, - exclude_lint=exclude_lint, - warn_lint=warn_lint) +@repo.command('lint', help='Lint all sites in a repository') +@utils.ALLOW_MISSING_SUBSTITUTIONS_OPTION +@utils.EXCLUDE_LINT_OPTION +@utils.WARN_LINT_OPTION +def lint_repo(*, fail_on_missing_sub_src, exclude_lint, warn_lint): + """Lint all sites using checks defined in :mod:`pegleg.engine.errorcodes`. + """ + warns = pegleg_main.run_lint( + exclude_lint, fail_on_missing_sub_src, warn_lint) if warns: click.echo("Linting passed, but produced some warnings.") for w in warns: click.echo(w) -@repo.command('lint', help='Lint all sites in a repository') -@ALLOW_MISSING_SUBSTITUTIONS_OPTION -@EXCLUDE_LINT_OPTION -@WARN_LINT_OPTION -def lint_repo(*, fail_on_missing_sub_src, exclude_lint, warn_lint): - """Lint all sites using checks defined in :mod:`pegleg.engine.errorcodes`. - """ - engine.repository.process_site_repository(update_config=True) - _lint_helper( - fail_on_missing_sub_src=fail_on_missing_sub_src, - exclude_lint=exclude_lint, - warn_lint=warn_lint) - - @main.group(help='Commands related to sites') -@MAIN_REPOSITORY_OPTION -@REPOSITORY_CLONE_PATH_OPTION -@EXTRA_REPOSITORY_OPTION -@REPOSITORY_USERNAME_OPTION -@REPOSITORY_KEY_OPTION +@utils.MAIN_REPOSITORY_OPTION +@utils.REPOSITORY_CLONE_PATH_OPTION +@utils.EXTRA_REPOSITORY_OPTION +@utils.REPOSITORY_USERNAME_OPTION +@utils.REPOSITORY_KEY_OPTION def site( *, site_repository, clone_path, extra_repositories, repo_key, repo_username): @@ -228,13 +109,13 @@ def site( * show: show a site's files """ - - config.set_site_repo(site_repository) - config.set_clone_path(clone_path) - config.set_extra_repo_overrides(extra_repositories or []) - config.set_repo_key(repo_key) - config.set_repo_username(repo_username) - config.set_umask() + pegleg_main.run_config( + site_repository, + clone_path, + repo_key, + repo_username, + extra_repositories or [], + run_umask=True) @site.command(help='Output complete config for one site') @@ -253,20 +134,9 @@ def site( # compatibility concerns. default=False, help='Perform validations on documents prior to collection.') -@click.option( - '-x', - '--exclude', - 'exclude_lint', - multiple=True, - help='Excludes specified linting checks. Warnings will still be issued. ' - '-w takes priority over -x.') -@click.option( - '-w', - '--warn', - 'warn_lint', - multiple=True, - help='Warn if linting check fails. -w takes priority over -x.') -@SITE_REPOSITORY_ARGUMENT +@utils.EXCLUDE_LINT_OPTION +@utils.WARN_LINT_OPTION +@utils.SITE_REPOSITORY_ARGUMENT def collect(*, save_location, validate, exclude_lint, warn_lint, site_name): """Collects documents into a single site-definition.yaml file, which defines the entire site definition and contains all documents required @@ -278,32 +148,25 @@ def collect(*, save_location, validate, exclude_lint, warn_lint, site_name): Collect can lint documents prior to collection if the ``--validate`` flag is optionally included. """ - if validate: - # Lint the primary repo prior to document collection. - _lint_helper( - site_name=site_name, - fail_on_missing_sub_src=True, - exclude_lint=exclude_lint, - warn_lint=warn_lint) - engine.site.collect(site_name, save_location) + pegleg_main.run_collect( + exclude_lint, save_location, site_name, validate, warn_lint) @site.command('list', help='List known sites') -@click.option('-o', '--output', 'output_stream', help='Where to output.') +@utils.OUTPUT_STREAM_OPTION def list_sites(*, output_stream): - engine.repository.process_site_repository(update_config=True) - engine.site.list_(output_stream) + pegleg_main.run_list_sites(output_stream) @site.command(help='Show details for one site') -@click.option('-o', '--output', 'output_stream', help='Where to output.') -@SITE_REPOSITORY_ARGUMENT +@utils.OUTPUT_STREAM_OPTION +@utils.SITE_REPOSITORY_ARGUMENT def show(*, output_stream, site_name): - engine.site.show(site_name, output_stream) + pegleg_main.run_show(output_stream, site_name) @site.command('render', help='Render a site through the deckhand engine') -@click.option('-o', '--output', 'output_stream', help='Where to output.') +@utils.OUTPUT_STREAM_OPTION @click.option( '-v', '--validate', @@ -314,32 +177,26 @@ def show(*, output_stream, site_name): help='Whether to pre-validate documents using built-in schema validation. ' 'Skips over externally registered DataSchema documents to avoid ' 'false positives.') -@SITE_REPOSITORY_ARGUMENT +@utils.SITE_REPOSITORY_ARGUMENT def render(*, output_stream, site_name, validate): - engine.site.render(site_name, output_stream, validate) + pegleg_main.run_render(output_stream, site_name, validate) @site.command('lint', help='Lint a given site in a repository') -@ALLOW_MISSING_SUBSTITUTIONS_OPTION -@EXCLUDE_LINT_OPTION -@WARN_LINT_OPTION -@SITE_REPOSITORY_ARGUMENT +@utils.ALLOW_MISSING_SUBSTITUTIONS_OPTION +@utils.EXCLUDE_LINT_OPTION +@utils.WARN_LINT_OPTION +@utils.SITE_REPOSITORY_ARGUMENT def lint_site(*, fail_on_missing_sub_src, exclude_lint, warn_lint, site_name): """Lint a given site using checks defined in :mod:`pegleg.engine.errorcodes`. """ - _lint_helper( - site_name=site_name, - fail_on_missing_sub_src=fail_on_missing_sub_src, - exclude_lint=exclude_lint, - warn_lint=warn_lint) - - -def collection_default_callback(ctx, param, value): - LOG.debug('Evaluating %s: %s', param.name, value) - if not value: - return ctx.params['site_name'] - return value + warns = pegleg_main.run_lint_site( + exclude_lint, fail_on_missing_sub_src, site_name, warn_lint) + if warns: + click.echo("Linting passed, but produced some warnings.") + for w in warns: + click.echo(w) @site.command('upload', help='Upload documents to Shipyard') @@ -387,43 +244,18 @@ def collection_default_callback(ctx, param, value): 'collection', help='Specifies the name to use for the uploaded collection. ' 'Defaults to the specified `site_name`.', - callback=collection_default_callback) -@SITE_REPOSITORY_ARGUMENT + callback=utils.collection_default_callback) +@utils.SITE_REPOSITORY_ARGUMENT @click.pass_context def upload( ctx, *, os_domain_name, os_project_domain_name, os_user_domain_name, os_project_name, os_username, os_password, os_auth_url, os_auth_token, context_marker, site_name, buffer_mode, collection): - if not ctx.obj: - ctx.obj = {} - - # Build API parameters required by Shipyard API Client. - if os_auth_token: - os.environ['OS_AUTH_TOKEN'] = os_auth_token - auth_vars = {'token': os_auth_token, 'auth_url': os_auth_url} - else: - auth_vars = { - 'user_domain_name': os_user_domain_name, - 'project_name': os_project_name, - 'username': os_username, - 'password': os_password, - 'auth_url': os_auth_url - } - - # Domain-scoped params - if os_domain_name: - auth_vars['domain_name'] = os_domain_name - auth_vars['project_domain_name'] = None - # Project-scoped params - else: - auth_vars['project_domain_name'] = os_project_domain_name - - ctx.obj['API_PARAMETERS'] = {'auth_vars': auth_vars} - ctx.obj['context_marker'] = str(context_marker) - ctx.obj['site_name'] = site_name - ctx.obj['collection'] = collection - config.set_global_enc_keys(site_name) - click.echo(ShipyardHelper(ctx, buffer_mode).upload_documents()) + resp = pegleg_main.run_upload( + buffer_mode, collection, context_marker, ctx, os_auth_token, + os_auth_url, os_domain_name, os_password, os_project_domain_name, + os_project_name, os_user_domain_name, os_username, site_name) + click.echo(resp) @site.group(name='secrets', help='Commands to manage site secrets documents') @@ -433,7 +265,7 @@ def secrets(): @secrets.command( 'generate-pki', - short_help='[DEPRECATED - Use secrets generate certificates] \n' + short_help='[DEPRECATED - Use secrets generate certificates]\n' 'Generate certs and keys according to the site PKICatalog', help='[DEPRECATED - Use secrets generate certificates]\n' 'Generate certificates and keys according to all PKICatalog ' @@ -446,7 +278,7 @@ def secrets(): '-a', '--author', 'author', - help='Identifying name of the author generating new certificates. Used' + help='Identifying name of the author generating new certificates. Used ' 'for tracking provenance information in the PeglegManagedDocuments. ' 'An attempt is made to automatically determine this value, ' 'but should be provided.') @@ -472,11 +304,8 @@ def generate_pki_deprecated(site_name, author, days, regenerate_all): """ warnings.warn( "DEPRECATED - Use secrets generate certificates", DeprecationWarning) - engine.repository.process_repositories(site_name, overwrite_existing=True) - config.set_global_enc_keys(site_name) - pkigenerator = catalog.pki_generator.PKIGenerator( - site_name, author=author, duration=days, regenerate_all=regenerate_all) - output_paths = pkigenerator.generate() + output_paths = pegleg_main.run_generate_pki( + author, days, regenerate_all, site_name) click.echo("Generated PKI files written to:\n%s" % '\n'.join(output_paths)) @@ -525,21 +354,9 @@ def generate_pki_deprecated(site_name, author, days, regenerate_all): def wrap_secret_cli( *, site_name, author, filename, output_path, schema, name, layer, encrypt): - """Wrap a bare secrets file in a YAML and ManagedDocument. - - """ - - engine.repository.process_repositories(site_name, overwrite_existing=True) - config.set_global_enc_keys(site_name) - wrap_secret( - author, - filename, - output_path, - schema, - name, - layer, - encrypt, - site_name=site_name) + """Wrap a bare secrets file in a YAML and ManagedDocument""" + pegleg_main.run_wrap_secret( + author, encrypt, filename, layer, name, output_path, schema, site_name) @site.command( @@ -558,14 +375,9 @@ def wrap_secret_cli( default=False, help='A flag to request generate genesis validation scripts in addition ' 'to genesis.sh script.') -@SITE_REPOSITORY_ARGUMENT +@utils.SITE_REPOSITORY_ARGUMENT def genesis_bundle(*, build_dir, validators, site_name): - encryption_key = os.environ.get("PROMENADE_ENCRYPTION_KEY") - config.set_global_enc_keys(site_name) - - bundle.build_genesis( - build_dir, encryption_key, validators, - logging.DEBUG == LOG.getEffectiveLevel(), site_name) + pegleg_main.run_genesis_bundle(build_dir, site_name, validators) @secrets.command( @@ -581,14 +393,10 @@ def genesis_bundle(*, build_dir, validators, site_name): @click.argument('site_name') def check_pki_certs(site_name, days): """Check PKI certificates of a site for expiration.""" + expiring_certs_exist, cert_results = pegleg_main.run_check_pki_certs( + days, site_name) - engine.repository.process_repositories(site_name, overwrite_existing=True) - config.set_global_enc_keys(site_name) - - expired_certs_exist, cert_results = engine.secrets.check_cert_expiry( - site_name, duration=days) - - if expired_certs_exist: + if expiring_certs_exist: click.echo( "The following certs will expire within the next {} days: \n{}". format(days, cert_results)) @@ -601,11 +409,11 @@ def check_pki_certs(site_name, days): @main.group(help='Commands related to types') -@MAIN_REPOSITORY_OPTION -@REPOSITORY_CLONE_PATH_OPTION -@EXTRA_REPOSITORY_OPTION -@REPOSITORY_USERNAME_OPTION -@REPOSITORY_KEY_OPTION +@utils.MAIN_REPOSITORY_OPTION +@utils.REPOSITORY_CLONE_PATH_OPTION +@utils.EXTRA_REPOSITORY_OPTION +@utils.REPOSITORY_USERNAME_OPTION +@utils.REPOSITORY_KEY_OPTION def type( *, site_repository, clone_path, extra_repositories, repo_key, repo_username): @@ -614,19 +422,20 @@ def type( * list: list all types across the repository """ - config.set_site_repo(site_repository) - config.set_clone_path(clone_path) - config.set_extra_repo_overrides(extra_repositories or []) - config.set_repo_key(repo_key) - config.set_repo_username(repo_username) + pegleg_main.run_config( + site_repository, + clone_path, + repo_key, + repo_username, + extra_repositories or [], + run_umask=False) @type.command('list', help='List known types') -@click.option('-o', '--output', 'output_stream', help='Where to output.') +@utils.OUTPUT_STREAM_OPTION def list_types(*, output_stream): """List type names for a given repository.""" - engine.repository.process_site_repository(update_config=True) - engine.type.list_types(output_stream) + pegleg_main.run_list_types(output_stream) @secrets.group( @@ -682,17 +491,8 @@ def generate_pki(site_name, author, days, regenerate_all, save_location): site. """ - - engine.repository.process_repositories(site_name, overwrite_existing=True) - config.set_global_enc_keys(site_name) - pkigenerator = catalog.pki_generator.PKIGenerator( - site_name, - author=author, - duration=days, - regenerate_all=regenerate_all, - save_location=save_location) - output_paths = pkigenerator.generate() - + output_paths = pegleg_main.run_generate_pki( + author, days, regenerate_all, site_name, save_location) click.echo("Generated PKI files written to:\n%s" % '\n'.join(output_paths)) @@ -741,11 +541,9 @@ def generate_pki(site_name, author, days, regenerate_all, save_location): def generate_passphrases( *, site_name, save_location, author, passphrase_catalog, interactive, force_cleartext): - engine.repository.process_repositories(site_name) - config.set_global_enc_keys(site_name) - engine.secrets.generate_passphrases( - site_name, save_location, author, passphrase_catalog, interactive, - force_cleartext) + pegleg_main.run_generate_passphrases( + author, force_cleartext, interactive, save_location, site_name, + passphrase_catalog) @secrets.command( @@ -771,11 +569,7 @@ def generate_passphrases( 'documents') @click.argument('site_name') def encrypt(*, save_location, author, site_name): - engine.repository.process_repositories(site_name, overwrite_existing=True) - config.set_global_enc_keys(site_name) - if save_location is None: - save_location = config.get_site_repo() - engine.secrets.encrypt(save_location, author, site_name=site_name) + pegleg_main.run_encrypt(author, save_location, site_name) @secrets.command( @@ -805,21 +599,10 @@ def encrypt(*, save_location, author, site_name): 'Overrides --save-location option.') @click.argument('site_name') def decrypt(*, path, save_location, overwrite, site_name): - engine.repository.process_repositories(site_name) - config.set_global_enc_keys(site_name) - - decrypted = engine.secrets.decrypt(path, site_name=site_name) - if overwrite: - for path, data in decrypted.items(): - files.write(data, path) - elif save_location is None: - for data in decrypted.values(): - click.echo(data) - else: - for path, data in decrypted.items(): - file_name = os.path.split(path)[1] - file_save_location = os.path.join(save_location, file_name) - files.write(data, file_save_location) + data = pegleg_main.run_decrypt(overwrite, path, save_location, site_name) + if data: + for d in data: + click.echo(d) @main.group(help='Miscellaneous generate commands') @@ -841,7 +624,7 @@ def generate(): def generate_passphrase(length): click.echo( 'Generated Passhprase: {}'.format( - engine.secrets.generate_crypto_string(length))) + pegleg_main.run_generate_passphrase(length))) @generate.command( @@ -852,9 +635,8 @@ def generate_passphrase(length): 'length', default=24, show_default=True, - help='Generate a passphrase of the given length. ' + help='Generate a salt of the given length. ' 'Length is >= 24, no maximum length.') def generate_salt(length): click.echo( - "Generated Salt: {}".format( - engine.secrets.generate_crypto_string(length))) + "Generated Salt: {}".format(pegleg_main.run_generate_salt(length))) diff --git a/pegleg/cli/utils.py b/pegleg/cli/utils.py new file mode 100644 index 00000000..707d8cd5 --- /dev/null +++ b/pegleg/cli/utils.py @@ -0,0 +1,124 @@ +# Copyright 2019 AT&T Intellectual Property. All other rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging + +import click + +from pegleg import engine + +LOG = logging.getLogger(__name__) + + +# Callbacks # +def process_repositories_callback(ctx, param, value): + """Convenient callback for ``@click.argument(site_name)``. + + Automatically processes repository information for the specified site. This + entails cloning all requires repositories and checking out specified + references for each repository. + """ + engine.repository.process_repositories(value) + return value + + +def collection_default_callback(ctx, param, value): + LOG.debug('Evaluating %s: %s', param.name, value) + if not value: + return ctx.params['site_name'] + return value + + +# Arguments # +SITE_REPOSITORY_ARGUMENT = click.argument( + 'site_name', callback=process_repositories_callback) + +# Options # +ALLOW_MISSING_SUBSTITUTIONS_OPTION = click.option( + '-f', + '--fail-on-missing-sub-src', + required=False, + type=click.BOOL, + default=True, + show_default=True, + help='Raise Deckhand exception on missing substition sources.') + +EXCLUDE_LINT_OPTION = click.option( + '-x', + '--exclude', + 'exclude_lint', + multiple=True, + help='Excludes specified linting checks. Warnings will still be issued. ' + '-w takes priority over -x.') + +EXTRA_REPOSITORY_OPTION = click.option( + '-e', + '--extra-repository', + 'extra_repositories', + multiple=True, + help='Path or URL of additional repositories. These should be named per ' + 'the site-definition file, e.g. -e global=/opt/global -e ' + 'secrets=/opt/secrets. By default, the revision specified in the ' + 'site-definition for the site will be leveraged but can be ' + 'overridden using -e global=/opt/global@revision.') + +MAIN_REPOSITORY_OPTION = click.option( + '-r', + '--site-repository', + 'site_repository', + required=True, + help='Path or URL to the primary repository (containing ' + 'site_definition.yaml) repo.') + +OUTPUT_STREAM_OPTION = click.option( + '-o', '--output', 'output_stream', help='Where to output.') + +REPOSITORY_CLONE_PATH_OPTION = click.option( + '-p', + '--clone-path', + 'clone_path', + help='The path where the repo will be cloned. By default the repo will be ' + 'cloned to the /tmp path. If this option is ' + 'included and the repo already ' + 'exists, then the repo will not be cloned again and the ' + 'user must specify a new clone path or pass in the local copy ' + 'of the repository as the site repository. Suppose the repo ' + 'name is airship/treasuremap and the clone path is ' + '/tmp/mypath then the following directory is ' + 'created /tmp/mypath/airship/treasuremap ' + 'which will contain the contents of the repo') + +REPOSITORY_KEY_OPTION = click.option( + '-k', + '--repo-key', + 'repo_key', + help='The SSH public key to use when cloning remote authenticated ' + 'repositories.') + +REPOSITORY_USERNAME_OPTION = click.option( + '-u', + '--repo-username', + 'repo_username', + help='The SSH username to use when cloning remote authenticated ' + 'repositories specified in the site-definition file. Any ' + 'occurrences of REPO_USERNAME will be replaced with this ' + 'value.\n' + 'Use only if REPO_USERNAME appears in a repo URL.') + +WARN_LINT_OPTION = click.option( + '-w', + '--warn', + 'warn_lint', + multiple=True, + help='Warn if linting check fails. -w takes priority over -x.') diff --git a/pegleg/engine/secrets.py b/pegleg/engine/secrets.py index efaae7a1..222e77c5 100644 --- a/pegleg/engine/secrets.py +++ b/pegleg/engine/secrets.py @@ -11,6 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. + from collections import OrderedDict from glob import glob import logging @@ -31,7 +32,7 @@ from pegleg.engine.util.pegleg_managed_document import \ PeglegManagedSecretsDocument as PeglegManagedSecret from pegleg.engine.util.pegleg_secret_management import PeglegSecretManagement -__all__ = ('encrypt', 'decrypt', 'generate_passphrases') +__all__ = ('encrypt', 'decrypt', 'generate_passphrases', 'wrap_secret') LOG = logging.getLogger(__name__) diff --git a/pegleg/pegleg_main.py b/pegleg/pegleg_main.py new file mode 100644 index 00000000..6223cd46 --- /dev/null +++ b/pegleg/pegleg_main.py @@ -0,0 +1,412 @@ +# Copyright 2019. AT&T Intellectual Property. All other rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import functools +import logging +import os + +from pegleg import config +from pegleg import engine +from pegleg.engine import bundle +from pegleg.engine import catalog +from pegleg.engine.secrets import wrap_secret +from pegleg.engine.util import files +from pegleg.engine.util.shipyard_helper import ShipyardHelper + +LOG_FORMAT = '%(asctime)s %(levelname)-8s %(name)s:' \ + '%(funcName)s [%(lineno)3d] %(message)s' # noqa + +LOG = logging.getLogger(__name__) + + +def set_logging_level(verbose=False, logging_level=40): + """Sets logging level used for pegleg + + :param verbose: sets level to DEBUG when True + :param logging_level: specifies logging level by numbers + :return: + """ + lvl = logging_level + if verbose: + lvl = logging.DEBUG + logging.basicConfig(format=LOG_FORMAT, level=int(lvl)) + + +def run_config( + site_repository, + clone_path, + repo_key, + repo_username, + extra_repositories, + run_umask=True): + """Initializes pegleg configuration data + + :param site_repository: path or URL for site repository + :param clone_path: directory in which to clone the site_repository + :param repo_key: key for remote repository URL if needed + :param repo_username: username to replace REPO_USERNAME in repository URL + if needed + :param extra_repositories: list of extra repositories to read in documents + from, specified as "type=REPO_URL/PATH" + :param run_umask: if True, runs set_umask for os file output + :return: + """ + config.set_site_repo(site_repository) + config.set_clone_path(clone_path) + if extra_repositories: + config.set_extra_repo_overrides(extra_repositories) + config.set_repo_key(repo_key) + config.set_repo_username(repo_username) + if run_umask: + config.set_umask() + + +def _run_lint_helper( + *, fail_on_missing_sub_src, exclude_lint, warn_lint, site_name=None): + """Helper for executing lint on specific site or all sites in repo.""" + if site_name: + func = functools.partial(engine.lint.site, site_name=site_name) + else: + func = engine.lint.full + warns = func( + fail_on_missing_sub_src=fail_on_missing_sub_src, + exclude_lint=exclude_lint, + warn_lint=warn_lint) + return warns + + +def run_lint(exclude_lint, fail_on_missing_sub_src, warn_lint): + """Runs linting on a repository + + :param exclude_lint: exclude specified linting rules + :param fail_on_missing_sub_src: if True, fails when a substitution source + file is missing + :param warn_lint: output warnings for specified rules + :return: warnings developed from linting + :rtype: list + """ + engine.repository.process_site_repository(update_config=True) + warns = _run_lint_helper( + fail_on_missing_sub_src=fail_on_missing_sub_src, + exclude_lint=exclude_lint, + warn_lint=warn_lint) + return warns + + +def run_collect(exclude_lint, save_location, site_name, validate, warn_lint): + """Runs document collection to produce single file definitions, containing + all information and documents required by airship + + :param exclude_lint: exclude specified linting rules + :param save_location: path to output definition files to, ouputs to stdout + if None + :param site_name: site name to collect from repository + :param validate: validate documents prior to collection + :param warn_lint: output warnings for specified rules + :return: + """ + if validate: + # Lint the primary repo prior to document collection. + _run_lint_helper( + site_name=site_name, + fail_on_missing_sub_src=True, + exclude_lint=exclude_lint, + warn_lint=warn_lint) + engine.site.collect(site_name, save_location) + + +def run_list_sites(output_stream): + """Output list of known sites in repository + + :param output_stream: where to output site list + :return: + """ + engine.repository.process_site_repository(update_config=True) + engine.site.list_(output_stream) + + +def run_show(output_stream, site_name): + """Shows details for one site + + :param output_stream: where to output site information + :param site_name: site name to process + :return: + """ + engine.site.show(site_name, output_stream) + + +def run_render(output_stream, site_name, validate): + """Render a site through the deckhand engine + + :param output_stream: where to output rendered site data + :param site_name: site name to process + :param validate: if True, validate documents using schema validation + :return: + """ + engine.site.render(site_name, output_stream, validate) + + +def run_lint_site(exclude_lint, fail_on_missing_sub_src, site_name, warn_lint): + """Lints a specified site + + :param exclude_lint: exclude specified linting rules + :param fail_on_missing_sub_src: if True, fails when a substitution source + file is missing + :param site_name: site name to collect from repository + :param warn_lint: output warnings for specified rules + :return: + """ + return _run_lint_helper( + fail_on_missing_sub_src=fail_on_missing_sub_src, + exclude_lint=exclude_lint, + warn_lint=warn_lint, + site_name=site_name) + + +def run_upload( + buffer_mode, collection, context_marker, ctx, os_auth_token, + os_auth_url, os_domain_name, os_password, os_project_domain_name, + os_project_name, os_user_domain_name, os_username, site_name): + """Uploads a collection of documents to shipyard + + :param buffer_mode: mode used when uploading documents + :param collection: specifies the name to use for uploaded collection + :param context_marker: UUID used to correlate logs, transactions, etc... + :param ctx: dictionary containing various data used by shipyard + :param os_auth_token: authentication token + :param os_auth_url: authentication url + :param os_domain_name: domain name + :param os_password: password + :param os_project_domain_name: project domain name + :param os_project_name: project name + :param os_user_domain_name: user domain name + :param os_username: username + :param site_name: site name to process + :return: response from shipyard instance + """ + if not ctx.obj: + ctx.obj = {} + # Build API parameters required by Shipyard API Client. + if os_auth_token: + os.environ['OS_AUTH_TOKEN'] = os_auth_token + auth_vars = {'token': os_auth_token, 'auth_url': os_auth_url} + else: + auth_vars = { + 'user_domain_name': os_user_domain_name, + 'project_name': os_project_name, + 'username': os_username, + 'password': os_password, + 'auth_url': os_auth_url + } + # Domain-scoped params + if os_domain_name: + auth_vars['domain_name'] = os_domain_name + auth_vars['project_domain_name'] = None + # Project-scoped params + else: + auth_vars['project_domain_name'] = os_project_domain_name + ctx.obj['API_PARAMETERS'] = {'auth_vars': auth_vars} + ctx.obj['context_marker'] = str(context_marker) + ctx.obj['site_name'] = site_name + ctx.obj['collection'] = collection + config.set_global_enc_keys(site_name) + return ShipyardHelper(ctx, buffer_mode).upload_documents() + + +def run_generate_pki( + author, days, regenerate_all, site_name, save_location=None): + """Generates certificates from PKI catalog + + :param author: identifies author generating new certificates for + tracking information + :param days: duration in days for the certificate to be valid + :param regenerate_all: force regeneration of all certs, regardless of + expiration + :param site_name: site name to process + :param save_location: directory to store the generated site certificates in + :return: list of paths written to + """ + engine.repository.process_repositories(site_name, overwrite_existing=True) + pkigenerator = catalog.pki_generator.PKIGenerator( + site_name, + author=author, + duration=days, + regenerate_all=regenerate_all, + save_location=save_location) + output_paths = pkigenerator.generate() + return output_paths + + +def run_wrap_secret( + author, encrypt, filename, layer, name, output_path, schema, + site_name): + """Wraps a bare secrets file with identifying information for pegleg + + :param author: identifies author generating new certificates for + tracking information + :param encrypt: if False, leaves files in cleartext format + :param filename: path to file to be wrapped + :param layer: layer for document to be wrapped in, e.g. site or global + :param name: name for the docuemnt wrap + :param output_path: path to output wrapped document to + :param schema: schema for the document wrap + :param site_name: site name to process + :return: + """ + engine.repository.process_repositories(site_name, overwrite_existing=True) + config.set_global_enc_keys(site_name) + wrap_secret( + author, + filename, + output_path, + schema, + name, + layer, + encrypt, + site_name=site_name) + + +def run_genesis_bundle(build_dir, site_name, validators): + """Runs genesis bundle via promenade + + :param build_dir: output directory for the generated bundle + :param site_name: site name to process + :param validators: if True, runs validation scripts on genesis bundle + :return: + """ + encryption_key = os.environ.get("PROMENADE_ENCRYPTION_KEY") + config.set_global_enc_keys(site_name) + bundle.build_genesis( + build_dir, encryption_key, validators, + logging.DEBUG == LOG.getEffectiveLevel(), site_name) + + +def run_check_pki_certs(days, site_name): + """Checks PKI certificates for upcoming expiration + + :param days: number of days in advance to check for upcoming expirations + :param site_name: site name to process + :return: + """ + engine.repository.process_repositories(site_name, overwrite_existing=True) + config.set_global_enc_keys(site_name) + expiring_certs_exist, cert_results = engine.secrets.check_cert_expiry( + site_name, duration=days) + return expiring_certs_exist, cert_results + + +def run_list_types(output_stream): + """List type names for a repository + + :param output_stream: stream to output list + :return: + """ + engine.repository.process_site_repository(update_config=True) + engine.type.list_types(output_stream) + + +def run_generate_passphrases( + author, + force_cleartext, + interactive, + save_location, + site_name, + passphrase_catalog=None): + """Generates passphrases for site + + :param author: identifies author generating new certificates for + tracking information + :param force_cleartext: if True, forces cleartext output of passphrases + :param interactive: Enables input prompts for "prompt: true" passphrases + :param save_location: path to save generated passphrases to + :param site_name: site name to process + :param passphrase_catalog: path to a passphrase catalog to override other + discovered catalogs + :return: + """ + engine.repository.process_repositories(site_name) + config.set_global_enc_keys(site_name) + engine.secrets.generate_passphrases( + site_name, + save_location, + author, + passphrase_catalog=passphrase_catalog, + interactive=interactive, + force_cleartext=force_cleartext) + + +def run_encrypt(author, save_location, site_name): + """Wraps and encrypts site secret documents + + :param author: identifies author generating new certificates for + tracking information + :param save_location: path to save encrypted documents to, if None the + original documents are overwritten + :param site_name: site name to process + :return: + """ + engine.repository.process_repositories(site_name, overwrite_existing=True) + config.set_global_enc_keys(site_name) + if save_location is None: + save_location = config.get_site_repo() + engine.secrets.encrypt(save_location, author, site_name=site_name) + + +def run_decrypt(overwrite, path, save_location, site_name): + """Unwraps and decrypts secret documents for a site + + :param overwrite: if True, overwrites original files with decrypted + :param path: file or directory to decrypt + :param save_location: if specified saves to the given path, otherwise + returns list of decrypted information + :param site_name: site name to process + :return: decrypted data list if save_location is None + :rtype: list + """ + decrypted_data = [] + engine.repository.process_repositories(site_name) + config.set_global_enc_keys(site_name) + decrypted = engine.secrets.decrypt(path, site_name=site_name) + if overwrite: + for path, data in decrypted.items(): + files.write(data, path) + elif save_location is None: + for data in decrypted.values(): + decrypted_data.append(data) + else: + for path, data in decrypted.items(): + file_name = os.path.split(path)[1] + file_save_location = os.path.join(save_location, file_name) + files.write(data, file_save_location) + return decrypted_data + + +def run_generate_passphrase(length=24): + """Generates a single passphrase + + :param length: length of passphrase + :return: generated passphrase + :rtype: str + """ + return engine.secrets.generate_crypto_string(length) + + +def run_generate_salt(length=24): + """Generates a single salt + + :param length: length of salt + :return: generated salt + :rtype: str + """ + return engine.secrets.generate_crypto_string(length) diff --git a/setup.py b/setup.py index 462e5207..18ff421d 100644 --- a/setup.py +++ b/setup.py @@ -28,7 +28,7 @@ setup( packages=find_packages(), entry_points={ 'console_scripts': [ - 'pegleg=pegleg.cli:main', + 'pegleg=pegleg.cli.commands:main', ]}, include_package_data=True, package_dir={'pegleg': 'pegleg'}, diff --git a/tests/unit/cli/__init__.py b/tests/unit/cli/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/unit/test_cli.py b/tests/unit/cli/test_commands.py similarity index 92% rename from tests/unit/test_cli.py rename to tests/unit/cli/test_commands.py index a25487d7..0a73330c 100644 --- a/tests/unit/test_cli.py +++ b/tests/unit/cli/test_commands.py @@ -19,7 +19,7 @@ from click.testing import CliRunner import pytest import yaml -from pegleg import cli +from pegleg.cli import commands from pegleg.engine import errorcodes from pegleg.engine.catalog import pki_utility from pegleg.engine.util import git @@ -97,7 +97,7 @@ class TestSiteCLIOptions(BaseCLIActionTest): # Note that the -p option is used to specify the clone_folder site_list = self.runner.invoke( - cli.site, ['-p', tmpdir, '-r', repo_url, 'list']) + commands.site, ['-p', tmpdir, '-r', repo_url, 'list']) assert site_list.exit_code == 0 # Verify that the repo was cloned into the clone_path @@ -118,7 +118,7 @@ class TestSiteCLIOptions(BaseCLIActionTest): # Note that the -p option is used to specify the clone_folder site_list = self.runner.invoke( - cli.site, ['-p', tmpdir, '-r', repo_path, 'list']) + commands.site, ['-p', tmpdir, '-r', repo_path, 'list']) assert site_list.exit_code == 0 # Verify that passing in clone_path when using local repo has no effect @@ -146,14 +146,14 @@ class TestSiteCLIOptionsNegative(BaseCLIActionTest): # Note that the -p option is used to specify the clone_folder site_list = self.runner.invoke( - cli.site, ['-p', tmpdir, '-r', repo_url, 'list']) + commands.site, ['-p', tmpdir, '-r', repo_url, 'list']) assert git.is_repository(os.path.join(tmpdir, self.repo_name)) # Run site list for a second time to validate that the repo can't be # cloned twice in the same clone_path site_list = self.runner.invoke( - cli.site, ['-p', tmpdir, '-r', repo_url, 'list']) + commands.site, ['-p', tmpdir, '-r', repo_url, 'list']) assert site_list.exit_code == 1 assert 'File exists' in site_list.output @@ -166,7 +166,7 @@ class TestSiteCliActions(BaseCLIActionTest): def _validate_collect_site_action(self, repo_path_or_url, save_location): result = self.runner.invoke( - cli.site, [ + commands.site, [ '-r', repo_path_or_url, 'collect', self.site_name, '-s', save_location ]) @@ -228,7 +228,7 @@ class TestSiteCliActions(BaseCLIActionTest): with mock.patch('pegleg.engine.site.util.deckhand') as mock_deckhand: mock_deckhand.deckhand_render.return_value = ([], []) result = self.runner.invoke( - cli.site, lint_command + exclude_lint_command) + commands.site, lint_command + exclude_lint_command) assert result.exit_code == 0, result.output @@ -275,7 +275,7 @@ class TestSiteCliActions(BaseCLIActionTest): def _validate_list_site_action(self, repo_path_or_url, tmpdir): mock_output = os.path.join(tmpdir, 'output') result = self.runner.invoke( - cli.site, ['-r', repo_path_or_url, 'list', '-o', mock_output]) + commands.site, ['-r', repo_path_or_url, 'list', '-o', mock_output]) assert result.exit_code == 0, result.output with open(mock_output, 'r') as f: @@ -308,7 +308,7 @@ class TestSiteCliActions(BaseCLIActionTest): def _validate_site_show_action(self, repo_path_or_url, tmpdir): mock_output = os.path.join(tmpdir, 'output') result = self.runner.invoke( - cli.site, [ + commands.site, [ '-r', repo_path_or_url, 'show', self.site_name, '-o', mock_output ]) @@ -346,7 +346,7 @@ class TestSiteCliActions(BaseCLIActionTest): with mock.patch( 'pegleg.engine.site.util.deckhand') as mock_deckhand: mock_deckhand.deckhand_render.return_value = ([], []) - result = self.runner.invoke(cli.site, render_command) + result = self.runner.invoke(commands.site, render_command) assert result.exit_code == 0 mock_yaml.dump_all.assert_called_once() @@ -387,9 +387,9 @@ class TestSiteCliActions(BaseCLIActionTest): repo_path = self.treasuremap_path - with mock.patch('pegleg.cli.ShipyardHelper') as mock_obj: + with mock.patch('pegleg.pegleg_main.ShipyardHelper') as mock_obj: result = self.runner.invoke( - cli.site, [ + commands.site, [ '-r', repo_path, 'upload', self.site_name, '--collection', 'collection' ]) @@ -411,21 +411,21 @@ class TestSiteCliActions(BaseCLIActionTest): # site_name repo_path = self.treasuremap_path - with mock.patch('pegleg.cli.ShipyardHelper') as mock_obj: + with mock.patch('pegleg.pegleg_main.ShipyardHelper') as mock_obj: result = self.runner.invoke( - cli.site, ['-r', repo_path, 'upload', self.site_name]) + commands.site, ['-r', repo_path, 'upload', self.site_name]) assert result.exit_code == 0 mock_obj.assert_called_once() class TestGenerateActions(BaseCLIActionTest): def test_generate_passphrase(self): - result = self.runner.invoke(cli.generate, ['passphrase']) + result = self.runner.invoke(commands.generate, ['passphrase']) assert result.exit_code == 0, result.output def test_generate_salt(self): - result = self.runner.invoke(cli.generate, ['salt']) + result = self.runner.invoke(commands.generate, ['salt']) assert result.exit_code == 0, result.output @@ -454,7 +454,7 @@ class TestRepoCliActions(BaseCLIActionTest): with mock.patch('pegleg.engine.site.util.deckhand') as mock_deckhand: mock_deckhand.deckhand_render.return_value = ([], []) result = self.runner.invoke( - cli.repo, lint_command + exclude_lint_command) + commands.repo, lint_command + exclude_lint_command) assert result.exit_code == 0, result.output # A successful result (while setting lint checks to exclude) should @@ -478,7 +478,7 @@ class TestRepoCliActions(BaseCLIActionTest): with mock.patch('pegleg.engine.site.util.deckhand') as mock_deckhand: mock_deckhand.deckhand_render.return_value = ([], []) result = self.runner.invoke( - cli.repo, lint_command + exclude_lint_command) + commands.repo, lint_command + exclude_lint_command) assert result.exit_code == 0, result.output # A successful result (while setting lint checks to exclude) should @@ -526,7 +526,8 @@ class TestSiteSecretsActions(BaseCLIActionTest): secrets_opts = ['secrets', 'generate', 'certificates', self.site_name] - result = self.runner.invoke(cli.site, ['-r', repo_url] + secrets_opts) + result = self.runner.invoke( + commands.site, ['-r', repo_url] + secrets_opts) self._validate_generate_pki_action(result) @pytest.mark.skipif( @@ -541,7 +542,8 @@ class TestSiteSecretsActions(BaseCLIActionTest): repo_path = self.treasuremap_path secrets_opts = ['secrets', 'generate', 'certificates', self.site_name] - result = self.runner.invoke(cli.site, ['-r', repo_path] + secrets_opts) + result = self.runner.invoke( + commands.site, ['-r', repo_path] + secrets_opts) self._validate_generate_pki_action(result) @pytest.mark.skipif( @@ -571,7 +573,8 @@ class TestSiteSecretsActions(BaseCLIActionTest): yaml.dump(ceph_fsid, ceph_fsid_fi) secrets_opts = ['secrets', 'encrypt', '-a', 'test', self.site_name] - result = self.runner.invoke(cli.site, ['-r', repo_path] + secrets_opts) + result = self.runner.invoke( + commands.site, ['-r', repo_path] + secrets_opts) assert result.exit_code == 0 @@ -586,7 +589,8 @@ class TestSiteSecretsActions(BaseCLIActionTest): secrets_opts = [ 'secrets', 'decrypt', '--path', file_path, self.site_name ] - result = self.runner.invoke(cli.site, ['-r', repo_path] + secrets_opts) + result = self.runner.invoke( + commands.site, ['-r', repo_path] + secrets_opts) assert result.exit_code == 0, result.output @pytest.mark.skipif( @@ -595,7 +599,8 @@ class TestSiteSecretsActions(BaseCLIActionTest): def test_check_pki_certs_expired(self): repo_path = self.treasuremap_path secrets_opts = ['secrets', 'check-pki-certs', self.site_name] - result = self.runner.invoke(cli.site, ['-r', repo_path] + secrets_opts) + result = self.runner.invoke( + commands.site, ['-r', repo_path] + secrets_opts) assert result.exit_code == 1, result.output @pytest.mark.skipif( @@ -604,7 +609,8 @@ class TestSiteSecretsActions(BaseCLIActionTest): def test_check_pki_certs(self): repo_path = self.treasuremap_path secrets_opts = ['secrets', 'check-pki-certs', 'airsloop'] - result = self.runner.invoke(cli.site, ['-r', repo_path] + secrets_opts) + result = self.runner.invoke( + commands.site, ['-r', repo_path] + secrets_opts) assert result.exit_code == 0, result.output @mock.patch.dict( @@ -631,7 +637,8 @@ class TestSiteSecretsActions(BaseCLIActionTest): "deckhand/Certificate/v1", "-n", "test-certificate", "-l", "site", "--no-encrypt", self.site_name ] - result = self.runner.invoke(cli.site, ["-r", repo_path] + secrets_opts) + result = self.runner.invoke( + commands.site, ["-r", repo_path] + secrets_opts) assert result.exit_code == 0 with open(output_path, "r") as output_fi: @@ -652,7 +659,8 @@ class TestSiteSecretsActions(BaseCLIActionTest): output_path, "-s", "deckhand/Certificate/v1", "-n", "test-certificate", "-l", "site", self.site_name ] - result = self.runner.invoke(cli.site, ["-r", repo_path] + secrets_opts) + result = self.runner.invoke( + commands.site, ["-r", repo_path] + secrets_opts) assert result.exit_code == 0 with open(output_path, "r") as output_fi: @@ -673,7 +681,7 @@ class TestTypeCliActions(BaseCLIActionTest): def _validate_type_list_action(self, repo_path_or_url, tmpdir): mock_output = os.path.join(tmpdir, 'output') result = self.runner.invoke( - cli.type, ['-r', repo_path_or_url, 'list', '-o', mock_output]) + commands.type, ['-r', repo_path_or_url, 'list', '-o', mock_output]) with open(mock_output, 'r') as f: table_output = f.read() @@ -712,7 +720,7 @@ class TestSiteCliActionsWithSubdirectory(BaseCLIActionTest): def _validate_list_site_action(self, repo_path_or_url, tmpdir): mock_output = os.path.join(tmpdir, 'output') result = self.runner.invoke( - cli.site, ['-r', repo_path_or_url, 'list', '-o', mock_output]) + commands.site, ['-r', repo_path_or_url, 'list', '-o', mock_output]) with open(mock_output, 'r') as f: table_output = f.read() diff --git a/tests/unit/engine/test_secrets.py b/tests/unit/engine/test_secrets.py index 0d6374d2..3f5ebc92 100644 --- a/tests/unit/engine/test_secrets.py +++ b/tests/unit/engine/test_secrets.py @@ -30,7 +30,7 @@ from pegleg.engine.util.pegleg_managed_document import \ PeglegManagedSecretsDocument from pegleg.engine.util.pegleg_secret_management import PeglegSecretManagement from tests.unit import test_utils -from tests.unit.test_cli import TEST_PARAMS +from tests.unit.cli.test_commands import TEST_PARAMS TEST_DATA = """ ---