From 0d4bd4a423618aa534d7728cdc14164e3cdbb44b Mon Sep 17 00:00:00 2001 From: Daniel Palmer Date: Mon, 14 Dec 2020 15:49:29 -0500 Subject: [PATCH 01/19] Make group-level functions into callbacks for commands so auth code is run lazily. Signed-off-by: Daniel Palmer --- anchorecli/cli/__init__.py | 3 +- anchorecli/cli/account.py | 121 ++++++++++++++++++++++----------- anchorecli/cli/archives.py | 78 +++++++++++++++------ anchorecli/cli/evaluate.py | 26 ++++--- anchorecli/cli/event.py | 35 +++++++--- anchorecli/cli/image.py | 67 +++++++++++++----- anchorecli/cli/policy.py | 84 ++++++++++++++++------- anchorecli/cli/query.py | 30 +++++--- anchorecli/cli/registry.py | 44 ++++++++---- anchorecli/cli/repo.py | 51 ++++++++++---- anchorecli/cli/subscription.py | 36 ++++++---- anchorecli/cli/system.py | 68 ++++++++++++------ anchorecli/cli/utils.py | 9 +++ 13 files changed, 454 insertions(+), 198 deletions(-) diff --git a/anchorecli/cli/__init__.py b/anchorecli/cli/__init__.py index 11ff647..81902b8 100644 --- a/anchorecli/cli/__init__.py +++ b/anchorecli/cli/__init__.py @@ -19,6 +19,7 @@ from anchorecli import version import anchorecli.clients # noqa +from .utils import ContextObject @click.group(context_settings=dict(help_option_names=["-h", "--help", "help"])) @@ -73,7 +74,7 @@ def main_entry( if config["debug"]: logging.basicConfig(level=logging.DEBUG) - ctx.obj = config + ctx.obj = ContextObject(config, None) class Help(click.Command): diff --git a/anchorecli/cli/account.py b/anchorecli/cli/account.py index 261696b..e434c83 100644 --- a/anchorecli/cli/account.py +++ b/anchorecli/cli/account.py @@ -10,41 +10,48 @@ @click.group(name="account", short_help="Account operations") -@click.pass_obj -def account(ctx_config): - global config, whoami - config = ctx_config +@click.pass_context +def account(ctx): + def execute(): + global config, whoami + config = ctx.parent.obj.config - try: - anchorecli.cli.utils.check_access(config) - except Exception as err: - print(anchorecli.cli.utils.format_error_output(config, "account", {}, err)) - sys.exit(2) + try: + anchorecli.cli.utils.check_access(config) + except Exception as err: + print(anchorecli.cli.utils.format_error_output(config, "account", {}, err)) + sys.exit(2) - try: - ret = anchorecli.clients.apiexternal.get_account(config) - if ret["success"]: - whoami["account"] = ret["payload"] - else: - raise Exception(json.dumps(ret["error"], indent=4)) - except Exception as err: - print(anchorecli.cli.utils.format_error_output(config, "account", {}, err)) - sys.exit(2) + try: + ret = anchorecli.clients.apiexternal.get_account(config) + if ret["success"]: + whoami["account"] = ret["payload"] + else: + raise Exception(json.dumps(ret["error"], indent=4)) + except Exception as err: + print(anchorecli.cli.utils.format_error_output(config, "account", {}, err)) + sys.exit(2) - try: - ret = anchorecli.clients.apiexternal.get_user(config) - if ret["success"]: - whoami["user"] = ret["payload"] - else: - raise Exception(json.dumps(ret["error"], indent=4)) - except Exception as err: - print(anchorecli.cli.utils.format_error_output(config, "account", {}, err)) - sys.exit(2) + try: + ret = anchorecli.clients.apiexternal.get_user(config) + if ret["success"]: + whoami["user"] = ret["payload"] + else: + raise Exception(json.dumps(ret["error"], indent=4)) + except Exception as err: + print(anchorecli.cli.utils.format_error_output(config, "account", {}, err)) + sys.exit(2) + + ctx.obj = anchorecli.cli.utils.ContextObject(ctx.parent.obj.config, execute) @account.command(name="whoami", short_help="Get current account/user information") -def get_current_user(): +@click.pass_context +def get_current_user(ctx): global whoami + + ctx.parent.obj.execute_callback() + ecode = 0 print(anchorecli.cli.utils.format_output(config, "account_whoami", {}, whoami)) anchorecli.cli.utils.doexit(ecode) @@ -55,13 +62,16 @@ def get_current_user(): ) @click.argument("account_name", nargs=1, required=True) @click.option("--email", help="Optional email address to associate with account") -def add(account_name, email): +@click.pass_context +def add(ctx, account_name, email): """ ACCOUNT_NAME: name of new account to create EMAIL: email address associated with account (optional) """ + ctx.parent.obj.execute_callback() + ecode = 0 try: @@ -88,11 +98,14 @@ def add(account_name, email): @account.command(name="get", short_help="Get account information") @click.argument("account_name", nargs=1, required=True) -def get(account_name): +@click.pass_context +def get(ctx, account_name): """ ACCOUNT_NAME: name of new account to create """ + ctx.parent.obj.execute_callback() + ecode = 0 try: @@ -120,8 +133,11 @@ def get(account_name): @account.command( name="list", short_help="List information about all accounts (admin only)" ) -def list_accounts(): - """ """ +@click.pass_context +def list_accounts(ctx): + """""" + ctx.parent.obj.execute_callback() + ecode = 0 try: ret = anchorecli.clients.apiexternal.list_accounts(config) @@ -148,12 +164,15 @@ def list_accounts(): @click.option( "--dontask", is_flag=True, help="Do not prompt for confirmation of account deletion" ) -def delete(account_name, dontask): +@click.pass_context +def delete(ctx, account_name, dontask): global input """ ACCOUNT_NAME: name of account to delete (must be disabled first) """ + ctx.parent.obj.execute_callback() + ecode = 0 answer = "n" @@ -202,11 +221,14 @@ def delete(account_name, dontask): @account.command(name="enable", short_help="Enable a disabled account") @click.argument("account_name", nargs=1, required=True) -def enable(account_name): +@click.pass_context +def enable(ctx, account_name): """ ACCOUNT_NAME: name of account to enable """ + ctx.parent.obj.execute_callback() + ecode = 0 try: @@ -235,11 +257,14 @@ def enable(account_name): @account.command(name="disable", short_help="Disable an enabled account") @click.argument("account_name", nargs=1, required=True) -def disable(account_name): +@click.pass_context +def disable(ctx, account_name): """ ACCOUNT_NAME: name of account to disable """ + ctx.parent.obj.execute_callback() + ecode = 0 try: @@ -270,20 +295,26 @@ def disable(account_name): @account.group(name="user", short_help="Account user operations") -def user(): +@click.pass_context +def user(ctx): global config, whoami + # since there's nothing to execute here, just pass the parent config and callback down + ctx.obj = anchorecli.cli.utils.ContextObject(ctx.parent.obj.config, ctx.parent.obj.execute_callback) + @user.command(name="add", short_help="Add a new user") @click.argument("user_name", nargs=1, required=True) @click.argument("user_password", nargs=1, required=True) @click.option("--account", help="Optional account name") -def user_add(user_name, user_password, account): +@click.pass_context +def user_add(ctx, user_name, user_password, account): global whoami """ ACCOUNT: optional name of the account to act as """ + ctx.parent.obj.execute_callback() if not account: account = whoami.get("account", {}).get("name", None) @@ -324,12 +355,14 @@ def user_add(user_name, user_password, account): @user.command(name="del", short_help="Delete a user") @click.argument("user_name", nargs=1, required=True) @click.option("--account", help="Optional account name") -def user_delete(user_name, account): +@click.pass_context +def user_delete(ctx, user_name, account): global whoami """ ACCOUNT: optional name of the account to act as """ + ctx.parent.obj.execute_callback() if not account: account = whoami.get("account", {}).get("name", None) @@ -361,12 +394,14 @@ def user_delete(user_name, account): @user.command(name="get", short_help="Get information about a user") @click.argument("user_name", nargs=1, required=True) @click.option("--account", help="Optional account name") -def user_get(user_name, account): +@click.pass_context +def user_get(ctx, user_name, account): global whoami """ ACCOUNT: optional name of the account to act as """ + ctx.parent.obj.execute_callback() if not account: account = whoami.get("account", {}).get("name", None) @@ -397,12 +432,14 @@ def user_get(user_name, account): @user.command(name="list", short_help="Get a list of account users") @click.option("--account", help="Optional account name") -def user_list(account): +@click.pass_context +def user_list(ctx, account): global whoami """ ACCOUNT: optional name of the account to act as """ + ctx.parent.obj.execute_callback() if not account: account = whoami.get("account", {}).get("name", None) @@ -433,12 +470,14 @@ def user_list(account): @click.argument("user_password", nargs=1, required=True) @click.option("--username", help="Optional user name") @click.option("--account", help="Optional account name") -def user_setpassword(user_password, username, account): +@click.pass_context +def user_setpassword(ctx, user_password, username, account): global whoami """ ACCOUNT: optional name of the account to act as """ + ctx.parent.obj.execute_callback() if not account: account = whoami.get("account", {}).get("name", None) diff --git a/anchorecli/cli/archives.py b/anchorecli/cli/archives.py index 88a56f8..09c16c8 100644 --- a/anchorecli/cli/archives.py +++ b/anchorecli/cli/archives.py @@ -12,32 +12,39 @@ @click.group(name="analysis-archive", short_help="Archive operations") -@click.pass_obj -def archive(ctx_config): - global config - config = ctx_config +@click.pass_context +def archive(ctx): + def execute(): + global config + config = ctx.parent.obj.config - try: - anchorecli.cli.utils.check_access(config) - except Exception as err: - print(anchorecli.cli.utils.format_error_output(config, "image", {}, err)) - sys.exit(2) + try: + anchorecli.cli.utils.check_access(config) + except Exception as err: + print(anchorecli.cli.utils.format_error_output(config, "image", {}, err)) + sys.exit(2) + + ctx.obj = anchorecli.cli.utils.ContextObject(ctx.parent.obj.config, execute) @archive.group(name="images", short_help="Archive operations") -@click.pass_obj -def images(ctx_config): - pass +@click.pass_context +def images(ctx): + # since there's nothing to execute here, just pass the parent config and callback down + ctx.obj = anchorecli.cli.utils.ContextObject(ctx.parent.obj.config, ctx.parent.obj.execute_callback) @images.command( name="restore", short_help="Restore an image to active status from the archive" ) @click.argument("image_digest") -def image_restore(image_digest): +@click.pass_context +def image_restore(ctx, image_digest): """ Add an analyzed image to the analysis archive """ + ctx.parent.obj.execute_callback() + ecode = 0 try: @@ -74,10 +81,13 @@ def image_restore(image_digest): short_help="Add an image analysis to the archive. NOTE: this does not remove the image from the engine.", ) @click.argument("image_digests", nargs=-1) -def image_add(image_digests): +@click.pass_context +def image_add(ctx, image_digests): """ Add an analyzed image to the analysis archive """ + ctx.parent.obj.execute_callback() + ecode = 0 try: @@ -114,10 +124,13 @@ def image_add(image_digests): @images.command(name="get", short_help="Get metadata for an archived image analysis") @click.argument("digest", nargs=1) -def image_get(digest): +@click.pass_context +def image_get(ctx, digest): """ INPUT_IMAGE: Input Image Digest (ex. sha256:95c9a61d949bbc622a444202e7faf9529f0dab5773023f173f602151f3a107b3) """ + ctx.parent.obj.execute_callback() + ecode = 0 try: @@ -153,7 +166,10 @@ def image_get(digest): @images.command(name="list", short_help="List all archived image analyses") -def list_archived_analyses(): +@click.pass_context +def list_archived_analyses(ctx): + ctx.parent.obj.execute_callback() + ecode = 0 try: @@ -183,10 +199,13 @@ def list_archived_analyses(): @images.command(name="del", short_help="Delete an archived analysis") @click.argument("digest") @click.option("--force", is_flag=True, help="Force deletion of archived analysis") -def image_delete(digest, force): +@click.pass_context +def image_delete(ctx, digest, force): """ INPUT_IMAGE: Input Image Digest (ex. sha256:95c9a61d949bbc622a444202e7faf9529f0dab5773023f173f602151f3a107b3) """ + ctx.parent.obj.execute_callback() + ecode = 0 try: @@ -216,8 +235,10 @@ def image_delete(digest, force): @archive.group(name="rules", short_help="Archive operations") -def rules(): - pass +@click.pass_context +def rules(ctx): + # since there's nothing to execute here, just pass the parent config and callback down + ctx.obj = anchorecli.cli.utils.ContextObject(ctx.parent.obj.config, ctx.parent.obj.execute_callback) @rules.command(name="add", short_help="Add a new transition rule") @@ -259,7 +280,9 @@ def rules(): help="Days until the exclude block expires", type=int, ) +@click.pass_context def rule_add( + ctx, days_old, tag_versions_newer, transition, @@ -290,6 +313,8 @@ def rule_add( exclude_expiration_days: Number of days until exclude block expires """ + ctx.parent.obj.execute_callback() + ecode = 0 if days_old == 0 and tag_versions_newer == 0: @@ -368,7 +393,10 @@ def is_exclude_default(repo, registry, tag): @rules.command(name="get", short_help="Show detail for a specific transition rule") @click.argument("rule_id", nargs=1) -def rule_get(rule_id): +@click.pass_context +def rule_get(ctx, rule_id): + ctx.parent.obj.execute_callback() + ecode = 0 try: @@ -400,7 +428,10 @@ def rule_get(rule_id): @rules.command(name="list", short_help="List all transition rules for the account") -def list_transition_rules(): +@click.pass_context +def list_transition_rules(ctx): + ctx.parent.obj.execute_callback() + ecode = 0 try: @@ -429,7 +460,10 @@ def list_transition_rules(): @rules.command(name="del", short_help="Delete a transition rule") @click.argument("rule_id") -def rule_delete(rule_id): +@click.pass_context +def rule_delete(ctx, rule_id): + ctx.parent.obj.execute_callback() + ecode = 0 try: diff --git a/anchorecli/cli/evaluate.py b/anchorecli/cli/evaluate.py index a20bb2f..b1563ac 100644 --- a/anchorecli/cli/evaluate.py +++ b/anchorecli/cli/evaluate.py @@ -9,16 +9,19 @@ @click.group(name="evaluate", short_help="Policy evaluation operations") -@click.pass_obj -def evaluate(ctx_config): - global config - config = ctx_config +@click.pass_context +def evaluate(ctx): + def execute(): + global config + config = ctx.parent.obj.config - try: - anchorecli.cli.utils.check_access(config) - except Exception as err: - print(anchorecli.cli.utils.format_error_output(config, "evaluate", {}, err)) - sys.exit(2) + try: + anchorecli.cli.utils.check_access(config) + except Exception as err: + print(anchorecli.cli.utils.format_error_output(config, "evaluate", {}, err)) + sys.exit(2) + + ctx.obj = anchorecli.cli.utils.ContextObject(ctx.parent.obj.config, execute) @evaluate.command( @@ -36,10 +39,13 @@ def evaluate(ctx_config): help="Specify which POLICY to use for evaluate (defaults currently active policy)", ) @click.argument("input_image", nargs=1) -def check(input_image, show_history, detail, tag, policy): +@click.pass_context +def check(ctx, input_image, show_history, detail, tag, policy): """ INPUT_IMAGE: Input image can be in the following formats: Image Digest, ImageID or registry/repo:tag """ + ctx.parent.obj.execute_callback() + ecode = 0 try: diff --git a/anchorecli/cli/event.py b/anchorecli/cli/event.py index c7f4aa4..0c3caa6 100644 --- a/anchorecli/cli/event.py +++ b/anchorecli/cli/event.py @@ -9,16 +9,19 @@ @click.group(name="event", short_help="Event operations") -@click.pass_obj -def event(ctx_config): - global config - config = ctx_config +@click.pass_context +def event(ctx): + def execute(): + global config + config = ctx.parent.obj.config - try: - anchorecli.cli.utils.check_access(config) - except Exception as err: - print(anchorecli.cli.utils.format_error_output(config, "event", {}, err)) - sys.exit(2) + try: + anchorecli.cli.utils.check_access(config) + except Exception as err: + print(anchorecli.cli.utils.format_error_output(config, "event", {}, err)) + sys.exit(2) + + ctx.obj = anchorecli.cli.utils.ContextObject(ctx.parent.obj.config, execute) @event.command(name="list", short_help="List events") @@ -79,7 +82,9 @@ def event(ctx_config): help="Display all columns for wider output.", ) @click.argument("resource", nargs=1, required=False) +@click.pass_context def list( + ctx, since=None, before=None, level=None, @@ -94,6 +99,8 @@ def list( """ RESOURCE: Value can be a tag, image digest or repository name. Displays results related to the specific resource """ + ctx.parent.obj.execute_callback() + ecode = 0 try: @@ -145,10 +152,13 @@ def list( @event.command(name="get", short_help="Get an event") @click.argument("event_id", nargs=1) -def get(event_id): +@click.pass_context +def get(ctx, event_id): """ EVENT_ID: ID of the event to be fetched """ + ctx.parent.obj.execute_callback() + ecode = 0 try: @@ -191,13 +201,16 @@ def get(event_id): ) @click.argument("event_id", nargs=1, required=False) @click.option("--all", help="Delete all events", is_flag=True, default=False) -def delete(since=None, before=None, dontask=False, event_id=None, all=False): +@click.pass_context +def delete(ctx, since=None, before=None, dontask=False, event_id=None, all=False): global input """ EVENT_ID: ID of the event to be deleted. --since and --before options will be ignored if this is specified NOTE: if no options are provided, delete (clear) all events in the engine. To skip the prompt in this case, use the --dontask flag. """ + ctx.parent.obj.execute_callback() + ecode = 0 try: diff --git a/anchorecli/cli/image.py b/anchorecli/cli/image.py index 7842d0f..12a3959 100644 --- a/anchorecli/cli/image.py +++ b/anchorecli/cli/image.py @@ -13,16 +13,20 @@ @click.group(name="image", short_help="Image operations") -@click.pass_obj -def image(ctx_config): - global config - config = ctx_config +@click.pass_context +def image(ctx): - try: - anchorecli.cli.utils.check_access(config) - except Exception as err: - print(anchorecli.cli.utils.format_error_output(config, "image", {}, err)) - sys.exit(2) + def execute(): + global config + config = ctx.parent.obj.config + + try: + anchorecli.cli.utils.check_access(config) + except Exception as err: + print(anchorecli.cli.utils.format_error_output(config, "image", {}, err)) + sys.exit(2) + + ctx.obj = anchorecli.cli.utils.ContextObject(ctx.parent.obj.config, execute) @image.command(short_help="Wait for an image to analyze") @@ -39,7 +43,8 @@ def image(ctx_config): default=5.0, help="Interval between checks, in seconds (default=5)", ) -def wait(input_image, timeout, interval): +@click.pass_context +def wait(ctx, input_image, timeout, interval): """ Wait for an image to go to analyzed or analysis_failed status with a specific timeout @@ -47,6 +52,8 @@ def wait(input_image, timeout, interval): :param timeout: :return: """ + ctx.parent.obj.execute_callback() + ecode = 0 try: @@ -135,10 +142,13 @@ def wait(input_image, timeout, interval): is_flag=True, help="If set, instruct the engine to disable tag_update subscription for the added tag.", ) -def add(input_image, force, dockerfile, annotation, noautosubscribe): +@click.pass_context +def add(ctx, input_image, force, dockerfile, annotation, noautosubscribe): """ INPUT_IMAGE: Input image can be in the following formats: registry/repo:tag """ + ctx.parent.obj.execute_callback() + ecode = 0 try: @@ -202,7 +212,10 @@ def add(input_image, force, dockerfile, annotation, noautosubscribe): @click.option( "--infile", required=True, type=click.Path(exists=True), metavar="" ) -def import_image(infile): +@click.pass_context +def import_image(ctx, infile): + ctx.parent.obj.execute_callback() + ecode = 0 try: @@ -237,10 +250,13 @@ def import_image(infile): is_flag=True, help="Show history of images that match the input image, if input image is of the form registry/repo:tag", ) -def get(input_image, show_history): +@click.pass_context +def get(ctx, input_image, show_history): """ INPUT_IMAGE: Input image can be in the following formats: Image Digest, ImageID or registry/repo:tag """ + ctx.parent.obj.execute_callback() + ecode = 0 try: @@ -292,7 +308,10 @@ def get(input_image, show_history): is_flag=True, help="Show all images in the system instead of just the latest for a given tag", ) -def imagelist(full, show_all): +@click.pass_context +def imagelist(ctx, full, show_all): + ctx.parent.obj.execute_callback() + ecode = 0 try: @@ -321,7 +340,8 @@ def imagelist(full, show_all): @image.command(name="content", short_help="Get contents of image") @click.argument("input_image", nargs=1) @click.argument("content_type", nargs=1, required=False) -def query_content(input_image, content_type): +@click.pass_context +def query_content(ctx, input_image, content_type): """ INPUT_IMAGE: Input image can be in the following formats: Image Digest, ImageID or registry/repo:tag @@ -329,6 +349,8 @@ def query_content(input_image, content_type): available for an image, run the following command: $ anchore-cli image content """ + ctx.parent.obj.execute_callback() + ecode = 0 try: @@ -385,13 +407,16 @@ def query_content(input_image, content_type): @image.command(name="metadata", short_help="Get metadata about an image") @click.argument("input_image", nargs=1) @click.argument("metadata_type", nargs=1, required=False) -def query_metadata(input_image, metadata_type): +@click.pass_context +def query_metadata(ctx, input_image, metadata_type): """ INPUT_IMAGE: Input image can be in the following formats: Image Digest, ImageID or registry/repo:tag METADATA_TYPE: The metadata type can be one of the types returned by running without a type specified """ + ctx.parent.obj.execute_callback() + ecode = 0 try: @@ -450,7 +475,8 @@ def query_metadata(input_image, metadata_type): type=bool, help="Show only vulnerabilities marked by upstream vendor as applicable (default=True)", ) -def query_vuln(input_image, vuln_type, vendor_only): +@click.pass_context +def query_vuln(ctx, input_image, vuln_type, vendor_only): """ INPUT_IMAGE: Input image can be in the following formats: Image Digest, ImageID or registry/repo:tag @@ -458,6 +484,8 @@ def query_vuln(input_image, vuln_type, vendor_only): - os: CVE/distro vulnerabilities against operating system packages """ + ctx.parent.obj.execute_callback() + ecode = 0 try: itype, image, imageDigest = anchorecli.cli.utils.discover_inputimage( @@ -510,10 +538,13 @@ def query_vuln(input_image, vuln_type, vendor_only): help="Force deletion of image by cancelling any subscription/notification settings prior to image delete", ) @click.option("--all", is_flag=True, help="Delete all images") -def delete(input_image, force, all): +@click.pass_context +def delete(ctx, input_image, force, all): """ INPUT_IMAGE: Input image can be in the following formats: Image Digest, ImageID or registry/repo:tag """ + ctx.parent.obj.execute_callback() + ecode = 0 if all: diff --git a/anchorecli/cli/policy.py b/anchorecli/cli/policy.py index c3fc078..36d1296 100644 --- a/anchorecli/cli/policy.py +++ b/anchorecli/cli/policy.py @@ -10,17 +10,19 @@ @click.group(name="policy", short_help="Policy operations") @click.pass_context -@click.pass_obj -def policy(ctx_config, ctx): - global config - config = ctx_config +def policy(ctx): + def execute(): + global config + config = ctx.parent.obj.config - if ctx.invoked_subcommand not in ["hub"]: - try: - anchorecli.cli.utils.check_access(config) - except Exception as err: - print(anchorecli.cli.utils.format_error_output(config, "policy", {}, err)) - sys.exit(2) + if ctx.invoked_subcommand not in ["hub"]: + try: + anchorecli.cli.utils.check_access(config) + except Exception as err: + print(anchorecli.cli.utils.format_error_output(config, "policy", {}, err)) + sys.exit(2) + + ctx.obj = anchorecli.cli.utils.ContextObject(ctx.parent.obj.config, execute) @policy.command(name="add", short_help="Add a policy bundle") @@ -30,7 +32,10 @@ def policy(ctx_config, ctx): type=click.Path(exists=True), metavar="", ) -def add(input_policy): +@click.pass_context +def add(ctx, input_policy): + ctx.parent.obj.execute_callback() + ecode = 0 try: @@ -61,10 +66,13 @@ def add(input_policy): @policy.command(name="get", short_help="Get a policy bundle") @click.argument("policyid", nargs=1) @click.option("--detail", is_flag=True, help="Get policy bundle as JSON") -def get(policyid, detail): +@click.pass_context +def get(ctx, policyid, detail): """ POLICYID: Policy ID to get """ + ctx.parent.obj.execute_callback() + ecode = 0 try: @@ -90,7 +98,10 @@ def get(policyid, detail): @policy.command(name="list", short_help="List all policies") -def policylist(): +@click.pass_context +def policylist(ctx): + ctx.parent.obj.execute_callback() + ecode = 0 try: @@ -115,10 +126,13 @@ def policylist(): @policy.command(name="activate", short_help="Activate a policyid") @click.argument("policyid", nargs=1) -def activate(policyid): +@click.pass_context +def activate(ctx, policyid): """ POLICYID: Policy ID to be activated """ + ctx.parent.obj.execute_callback() + ecode = 0 try: @@ -164,10 +178,13 @@ def activate(policyid): @policy.command(name="del", short_help="Delete a policy bundle") @click.argument("policyid", nargs=1) -def delete(policyid): +@click.pass_context +def delete(ctx, policyid): """ POLICYID: Policy ID to delete """ + ctx.parent.obj.execute_callback() + ecode = 0 try: @@ -206,7 +223,10 @@ def delete(policyid): "--trigger", help="Pick a specific trigger to describe instead of all, requires the --gate option to be specified", ) -def describe(all=False, gate=None, trigger=None): +@click.pass_context +def describe(ctx, all=False, gate=None, trigger=None): + ctx.parent.obj.execute_callback() + ecode = 0 try: ret = anchorecli.clients.apiexternal.describe_policy_spec(config) @@ -256,16 +276,24 @@ def describe(all=False, gate=None, trigger=None): @policy.group(name="hub", short_help="Anchore Hub Operations") @click.pass_context def hub(ctx): - if ctx.invoked_subcommand not in ["list", "get"]: - try: - anchorecli.cli.utils.check_access(config) - except Exception as err: - print(anchorecli.cli.utils.format_error_output(config, "policy", {}, err)) - sys.exit(2) + def execute(): + ctx.parent.obj.execute_callback() + + if ctx.invoked_subcommand not in ["list", "get"]: + try: + anchorecli.cli.utils.check_access(config) + except Exception as err: + print(anchorecli.cli.utils.format_error_output(config, "policy", {}, err)) + sys.exit(2) + + ctx.obj = anchorecli.cli.utils.ContextObject(ctx.parent.obj.config, execute) @hub.command(name="list") -def hublist(): +@click.pass_context +def hublist(ctx): + ctx.parent.obj.execute_callback() + ecode = 0 try: @@ -291,7 +319,10 @@ def hublist(): @hub.command(name="get") @click.argument("bundlename", nargs=1) -def hubget(bundlename): +@click.pass_context +def hubget(ctx, bundlename): + ctx.parent.obj.execute_callback() + ecode = 0 try: @@ -323,7 +354,10 @@ def hubget(bundlename): help="Install specified bundleid in place of existing policy bundle with same ID, if present", is_flag=True, ) -def hubinstall(bundlename, target_id, force): +@click.pass_context +def hubinstall(ctx, bundlename, target_id, force): + ctx.parent.obj.execute_callback() + ecode = 0 try: diff --git a/anchorecli/cli/query.py b/anchorecli/cli/query.py index 9020b78..a34ae77 100644 --- a/anchorecli/cli/query.py +++ b/anchorecli/cli/query.py @@ -9,17 +9,19 @@ @click.group(name="query", short_help="Query operations") -@click.pass_obj -def query(ctx_config): - global config - config = ctx_config +@click.pass_context +def query(ctx): + def execute(): + global config + config = ctx.parent.obj.config - try: - anchorecli.cli.utils.check_access(config) - except Exception as err: - print(anchorecli.cli.utils.format_error_output(config, "query", {}, err)) - sys.exit(2) + try: + anchorecli.cli.utils.check_access(config) + except Exception as err: + print(anchorecli.cli.utils.format_error_output(config, "query", {}, err)) + sys.exit(2) + ctx.obj = anchorecli.cli.utils.ContextObject(ctx.parent.obj.config, execute) @query.command( name="images-by-vulnerability", @@ -47,10 +49,13 @@ def query(ctx_config): is_flag=True, help="Only show images with vulnerabilities explicitly deemed applicable by upstream OS vendor, if present", ) +@click.pass_context def images_by_vulnerability( - vulnerability_id, namespace, package, severity, vendor_only + ctx, vulnerability_id, namespace, package, severity, vendor_only ): """ """ + ctx.parent.obj.execute_callback() + ecode = 0 try: @@ -100,8 +105,11 @@ def images_by_vulnerability( @click.option( "--package-type", help="Filter results to only packages of given type (e.g. dpkg)" ) -def images_by_package(name, version, package_type): +@click.pass_context +def images_by_package(ctx, name, version, package_type): """ """ + ctx.parent.obj.execute_callback() + ecode = 0 try: diff --git a/anchorecli/cli/registry.py b/anchorecli/cli/registry.py index 706d38c..2550461 100644 --- a/anchorecli/cli/registry.py +++ b/anchorecli/cli/registry.py @@ -9,16 +9,19 @@ @click.group(name="registry", short_help="Registry operations") -@click.pass_obj -def registry(ctx_config): - global config - config = ctx_config +@click.pass_context +def registry(ctx): + def execute(): + global config + config = ctx.parent.obj.config - try: - anchorecli.cli.utils.check_access(config) - except Exception as err: - print(anchorecli.cli.utils.format_error_output(config, "registry", {}, err)) - sys.exit(2) + try: + anchorecli.cli.utils.check_access(config) + except Exception as err: + print(anchorecli.cli.utils.format_error_output(config, "registry", {}, err)) + sys.exit(2) + + ctx.obj = anchorecli.cli.utils.ContextObject(ctx.parent.obj.config, execute) @registry.command(name="add", short_help="Add a registry") @@ -41,7 +44,9 @@ def registry(ctx_config): "--registry-name", help="Specify a human name for this registry (default=same as 'registry')", ) +@click.pass_context def add( + ctx, registry, registry_user, registry_pass, @@ -57,6 +62,8 @@ def add( REGISTRY_PASS: Password """ + ctx.parent.obj.execute_callback() + ecode = 0 registry_types = ["docker_v2", "awsecr"] @@ -133,7 +140,9 @@ def add( "--registry-name", help="Specify a human name for this registry (default=same as 'registry')", ) +@click.pass_context def upd( + ctx, registry, registry_user, registry_pass, @@ -149,6 +158,8 @@ def upd( REGISTRY_PASS: Password """ + ctx.parent.obj.execute_callback() + ecode = 0 try: @@ -186,10 +197,13 @@ def upd( @registry.command(name="del", short_help="Delete a registry") @click.argument("registry", nargs=1, required=True) -def delete(registry): +@click.pass_context +def delete(ctx, registry): """ REGISTRY: Full hostname/port of registry. Eg. myrepo.example.com:5000 """ + ctx.parent.obj.execute_callback() + ecode = 0 try: @@ -214,7 +228,10 @@ def delete(registry): @registry.command(name="list", short_help="List all current registries") -def registrylist(): +@click.pass_context +def registrylist(ctx): + ctx.parent.obj.execute_callback() + ecode = 0 try: @@ -240,10 +257,13 @@ def registrylist(): @registry.command(name="get", short_help="Get a registry") @click.argument("registry", nargs=1, required=True) -def get(registry): +@click.pass_context +def get(ctx, registry): """ REGISTRY: Full hostname/port of registry. Eg. myrepo.example.com:5000 """ + ctx.parent.obj.execute_callback() + ecode = 0 try: diff --git a/anchorecli/cli/repo.py b/anchorecli/cli/repo.py index 0b2fe96..2029dee 100644 --- a/anchorecli/cli/repo.py +++ b/anchorecli/cli/repo.py @@ -10,16 +10,19 @@ @click.group(name="repo", short_help="Repository operations") -@click.pass_obj -def repo(ctx_config): - global config - config = ctx_config +@click.pass_context +def repo(ctx): + def execute(): + global config + config = ctx.parent.obj.config - try: - anchorecli.cli.utils.check_access(config) - except Exception as err: - print(anchorecli.cli.utils.format_error_output(config, "repo", {}, err)) - sys.exit(2) + try: + anchorecli.cli.utils.check_access(config) + except Exception as err: + print(anchorecli.cli.utils.format_error_output(config, "repo", {}, err)) + sys.exit(2) + + ctx.obj = anchorecli.cli.utils.ContextObject(ctx.parent.obj.config, execute) @repo.command(name="add", short_help="Add a repository") @@ -38,10 +41,13 @@ def repo(ctx_config): help="List which tags would actually be watched if this repo was added (without actually adding the repo)", ) @click.argument("input_repo", nargs=1) -def add(input_repo, noautosubscribe, lookuptag, dryrun): +@click.pass_context +def add(ctx, input_repo, noautosubscribe, lookuptag, dryrun): """ INPUT_REPO: Input repository can be in the following formats: registry/repo """ + ctx.parent.obj.execute_callback() + response_code = 0 auto_subscribe = not noautosubscribe @@ -75,7 +81,10 @@ def add(input_repo, noautosubscribe, lookuptag, dryrun): @repo.command(name="list", short_help="List added repositories") -def listrepos(): +@click.pass_context +def listrepos(ctx): + ctx.parent.obj.execute_callback() + ecode = 0 try: @@ -100,10 +109,13 @@ def listrepos(): @repo.command(name="get", short_help="Get a repository") @click.argument("input_repo", nargs=1) -def get(input_repo): +@click.pass_context +def get(ctx, input_repo): """ INPUT_REPO: Input repository can be in the following formats: registry/repo """ + ctx.parent.obj.execute_callback() + ecode = 0 image_info = anchorecli.cli.utils.parse_dockerimage_string(input_repo) @@ -137,10 +149,13 @@ def get(input_repo): short_help="Delete a repository from the watch list (does not delete already analyzed images)", ) @click.argument("input_repo", nargs=1) -def delete(input_repo): +@click.pass_context +def delete(ctx, input_repo): """ INPUT_REPO: Input repo can be in the following formats: registry/repo """ + ctx.parent.obj.execute_callback() + ecode = 0 image_info = anchorecli.cli.utils.parse_dockerimage_string(input_repo) @@ -174,10 +189,13 @@ def delete(input_repo): short_help="Instruct engine to stop automatically watching the repo for image updates", ) @click.argument("input_repo", nargs=1) -def unwatch(input_repo): +@click.pass_context +def unwatch(ctx, input_repo): """ INPUT_REPO: Input repo can be in the following formats: registry/repo """ + ctx.parent.obj.execute_callback() + ecode = 0 image_info = anchorecli.cli.utils.parse_dockerimage_string(input_repo) @@ -211,10 +229,13 @@ def unwatch(input_repo): short_help="Instruct engine to start automatically watching the repo for image updates", ) @click.argument("input_repo", nargs=1) -def watch(input_repo): +@click.pass_context +def watch(ctx, input_repo): """ INPUT_REPO: Input repo can be in the following formats: registry/repo """ + ctx.parent.obj.execute_callback() + ecode = 0 image_info = anchorecli.cli.utils.parse_dockerimage_string(input_repo) diff --git a/anchorecli/cli/subscription.py b/anchorecli/cli/subscription.py index 98530ca..7d82570 100644 --- a/anchorecli/cli/subscription.py +++ b/anchorecli/cli/subscription.py @@ -8,22 +8,26 @@ @click.group(name="subscription", short_help="Subscription operations") -@click.pass_obj -def subscription(ctx_config): - global config - config = ctx_config +@click.pass_context +def subscription(ctx): + def execute(): + global config + config = ctx.parent.obj.config - try: - anchorecli.cli.utils.check_access(config) - except Exception as err: - print(anchorecli.cli.utils.format_error_output(config, "subscription", {}, err)) - sys.exit(2) + try: + anchorecli.cli.utils.check_access(config) + except Exception as err: + print(anchorecli.cli.utils.format_error_output(config, "subscription", {}, err)) + sys.exit(2) + + ctx.obj = anchorecli.cli.utils.ContextObject(ctx.parent.obj.config, execute) @subscription.command(name="activate", short_help="Activate a subscription") @click.argument("subscription_type", nargs=1, required=True) @click.argument("subscription_key", nargs=1, required=True) -def activate(subscription_type, subscription_key): +@click.pass_context +def activate(ctx, subscription_type, subscription_key): """ SUBSCRIPTION_TYPE: Type of subscription. Valid options: @@ -35,6 +39,8 @@ def activate(subscription_type, subscription_key): SUBSCRIPTION_KEY: Fully qualified name of tag to subscribe to. Eg. docker.io/library/alpine:latest """ + ctx.parent.obj.execute_callback() + ecode = 0 try: @@ -66,7 +72,8 @@ def activate(subscription_type, subscription_key): @subscription.command(name="deactivate", short_help="Deactivate a subscription") @click.argument("subscription_type", nargs=1, required=True) @click.argument("subscription_key", nargs=1, required=True) -def deactivate(subscription_type, subscription_key): +@click.pass_context +def deactivate(ctx, subscription_type, subscription_key): """ SUBSCRIPTION_TYPE: Type of subscription. Valid options: @@ -78,6 +85,8 @@ def deactivate(subscription_type, subscription_key): SUBSCRIPTION_KEY: Fully qualified name of tag to subscribe to. Eg. docker.io/library/alpine:latest """ + ctx.parent.obj.execute_callback() + ecode = 0 try: @@ -112,7 +121,10 @@ def deactivate(subscription_type, subscription_key): is_flag=True, help="Print additional details about the subscriptions as they're being listed", ) -def list_subscriptions(full): +@click.pass_context +def list_subscriptions(ctx, full): + ctx.parent.obj.execute_callback() + ecode = 0 try: ret = anchorecli.clients.apiexternal.get_subscription(config) diff --git a/anchorecli/cli/system.py b/anchorecli/cli/system.py index 8ba1011..f2db6d4 100644 --- a/anchorecli/cli/system.py +++ b/anchorecli/cli/system.py @@ -17,21 +17,26 @@ class WaitOnDisabledFeedError(Exception): @click.group(name="system", short_help="System operations") @click.pass_context -@click.pass_obj -def system(ctx_config, ctx): - global config - config = ctx_config +def system(ctx): + def execute(): + global config + config = ctx.parent.obj.config - if ctx.invoked_subcommand not in ["wait"]: - try: - anchorecli.cli.utils.check_access(config) - except Exception as err: - print(anchorecli.cli.utils.format_error_output(config, "system", {}, err)) - sys.exit(2) + if ctx.invoked_subcommand not in ["wait"]: + try: + anchorecli.cli.utils.check_access(config) + except Exception as err: + print(anchorecli.cli.utils.format_error_output(config, "system", {}, err)) + sys.exit(2) + + ctx.obj = anchorecli.cli.utils.ContextObject(ctx.parent.obj.config, execute) @system.command(name="status", short_help="Check current anchore-engine system status") -def status(): +@click.pass_context +def status(ctx): + ctx.parent.obj.execute_callback() + ecode = 0 try: @@ -59,7 +64,10 @@ def status(): name="errorcodes", short_help="Describe available anchore system error code names and descriptions", ) -def describe_errorcodes(): +@click.pass_context +def describe_errorcodes(ctx): + ctx.parent.obj.execute_callback() + ecode = 0 try: @@ -111,7 +119,8 @@ def describe_errorcodes(): default="catalog,apiext,policy_engine,simplequeue,analyzer", help='Wait for the specified CSV list of anchore-engine services to have at least one service reporting as available (default="catalog,apiext,policy_engine,simplequeue,analyzer")', ) -def wait(timeout, interval, feedsready, servicesready): +@click.pass_context +def wait(ctx, timeout, interval, feedsready, servicesready): """ Wait for an image to go to analyzed or analysis_failed status with a specific timeout @@ -120,6 +129,8 @@ def wait(timeout, interval, feedsready, servicesready): :param feedsready: :return: """ + ctx.parent.obj.execute_callback() + global config ecode = 0 @@ -290,7 +301,10 @@ def wait(timeout, interval, feedsready, servicesready): ) @click.argument("host_id", nargs=1) @click.argument("servicename", nargs=1) -def delete(host_id, servicename): +@click.pass_context +def delete(ctx, host_id, servicename): + ctx.parent.obj.execute_callback() + ecode = 0 try: @@ -319,12 +333,17 @@ def delete(host_id, servicename): @system.group(name="feeds", short_help="Feed data operations") -def feeds(): - pass +@click.pass_context +def feeds(ctx): + # since there's nothing to execute here, just pass the parent config and callback down + ctx.obj = anchorecli.cli.utils.ContextObject(ctx.parent.obj.config, ctx.parent.obj.execute_callback) @feeds.command(name="list", short_help="Get a list of loaded data feeds.") -def list(): +@click.pass_context +def list(ctx): + ctx.parent.obj.execute_callback() + ecode = 0 try: @@ -356,7 +375,10 @@ def list(): is_flag=True, help="Flush all previous data, including CVE matches, and resync from scratch", ) -def feedsync(flush): +@click.pass_context +def feedsync(ctx, flush): + ctx.parent.obj.execute_callback() + global input ecode = 0 @@ -407,7 +429,10 @@ def feedsync(flush): @click.option("--enable", help="Enable the feed/group", is_flag=True) @click.option("--disable", help="Disable the feed/group", is_flag=True) @click.argument("feed") -def toggle_enabled(feed, group=None, enable=None, disable=None): +@click.pass_context +def toggle_enabled(ctx, feed, group=None, enable=None, disable=None): + ctx.parent.obj.execute_callback() + ecode = 0 try: @@ -463,7 +488,10 @@ def toggle_enabled(feed, group=None, enable=None, disable=None): ) @click.option("--group", help="Delete data for a specific group only") @click.argument("feed") -def delete_data(feed, group=None): +@click.pass_context +def delete_data(ctx, feed, group=None): + ctx.parent.obj.execute_callback() + ecode = 0 try: if group: diff --git a/anchorecli/cli/utils.py b/anchorecli/cli/utils.py index a7e0211..42b6b20 100644 --- a/anchorecli/cli/utils.py +++ b/anchorecli/cli/utils.py @@ -1731,3 +1731,12 @@ def parse_dockerimage_string(instr): ret["pullstring"] = None return ret + + +class ContextObject: + config = None + execute_callback = None + + def __init__(self, config, execute_callback): + self.config = config + self.execute_callback = execute_callback \ No newline at end of file From 3f456f508faab34dfae4450067380bf09d4029ea Mon Sep 17 00:00:00 2001 From: Daniel Palmer Date: Tue, 15 Dec 2020 12:26:00 -0500 Subject: [PATCH 02/19] Add unit tests to confirm subcommand help is not dependent on passing api auth. Signed-off-by: Daniel Palmer --- tests/unit/cli/test_account.py | 28 ++++++++++++++++++++++++++++ tests/unit/cli/test_archives.py | 26 ++++++++++++++++++++++++++ tests/unit/cli/test_evaluate.py | 16 ++++++++++++++++ tests/unit/cli/test_event.py | 16 ++++++++++++++++ tests/unit/cli/test_image.py | 23 +++++++++++++++++++++++ tests/unit/cli/test_policy.py | 25 +++++++++++++++++++++++++ tests/unit/cli/test_query.py | 17 +++++++++++++++++ tests/unit/cli/test_registry.py | 20 ++++++++++++++++++++ tests/unit/cli/test_repo.py | 21 +++++++++++++++++++++ tests/unit/cli/test_subscription.py | 18 ++++++++++++++++++ tests/unit/cli/test_system.py | 22 ++++++++++++++++++++++ 11 files changed, 232 insertions(+) diff --git a/tests/unit/cli/test_account.py b/tests/unit/cli/test_account.py index bb2f0c1..ff57e73 100644 --- a/tests/unit/cli/test_account.py +++ b/tests/unit/cli/test_account.py @@ -1 +1,29 @@ +import pytest from anchorecli.cli import account +from click.testing import CliRunner + + +class TestAccountSubcommandHelp: + @pytest.mark.parametrize( + "subcommand, output_start", + [ + (account.get_current_user, "Usage: whoami"), + (account.add, "Usage: add"), + (account.get, "Usage: get"), + (account.list_accounts, "Usage: list"), + (account.delete, "Usage: del"), + (account.enable, "Usage: enable"), + (account.disable, "Usage: disable"), + (account.user, "Usage: user"), + (account.user_add, "Usage: add"), + (account.user_delete, "Usage: del"), + (account.user_get, "Usage: get"), + (account.user_list, "Usage: list"), + (account.user_setpassword, "Usage: setpassword"), + ] + ) + def test_event_subcommand_help(self, subcommand, output_start): + runner = CliRunner() + result = runner.invoke(subcommand, ["--help"]) + assert result.exit_code == 0 + assert result.output.startswith(output_start) diff --git a/tests/unit/cli/test_archives.py b/tests/unit/cli/test_archives.py index 98fda31..25e1d20 100644 --- a/tests/unit/cli/test_archives.py +++ b/tests/unit/cli/test_archives.py @@ -1 +1,27 @@ +import pytest from anchorecli.cli import archives +from click.testing import CliRunner + + +class TestArchiveSubcommandHelp: + @pytest.mark.parametrize( + "subcommand, output_start", + [ + (archives.images, "Usage: images"), + (archives.image_restore, "Usage: restore"), + (archives.image_add, "Usage: add"), + (archives.image_get, "Usage: get"), + (archives.list_archived_analyses, "Usage: list"), + (archives.image_delete, "Usage: del"), + (archives.rules, "Usage: rules"), + (archives.rule_add, "Usage: add"), + (archives.rule_get, "Usage: get"), + (archives.list_transition_rules, "Usage: list"), + (archives.rule_delete, "Usage: del"), + ] + ) + def test_event_subcommand_help(self, subcommand, output_start): + runner = CliRunner() + result = runner.invoke(subcommand, ["--help"]) + assert result.exit_code == 0 + assert result.output.startswith(output_start) diff --git a/tests/unit/cli/test_evaluate.py b/tests/unit/cli/test_evaluate.py index da42145..40edc6e 100644 --- a/tests/unit/cli/test_evaluate.py +++ b/tests/unit/cli/test_evaluate.py @@ -1 +1,17 @@ +import pytest from anchorecli.cli import evaluate +from click.testing import CliRunner + + +class TestEvaluateSubcommandHelp: + @pytest.mark.parametrize( + "subcommand, output_start", + [ + (evaluate.check, "Usage: check"), + ] + ) + def test_event_subcommand_help(self, subcommand, output_start): + runner = CliRunner() + result = runner.invoke(subcommand, ["--help"]) + assert result.exit_code == 0 + assert result.output.startswith(output_start) diff --git a/tests/unit/cli/test_event.py b/tests/unit/cli/test_event.py index 0b15946..2cf764e 100644 --- a/tests/unit/cli/test_event.py +++ b/tests/unit/cli/test_event.py @@ -43,3 +43,19 @@ def mock_method( assert result.exit_code == expected_code else: assert normalized_level == [expected_level] + + +class TestEventSubcommandHelp: + @pytest.mark.parametrize( + "subcommand, output_start", + [ + (event.list, "Usage: list"), + (event.get, "Usage: get"), + (event.delete, "Usage: delete"), + ] + ) + def test_event_subcommand_help(self, subcommand, output_start): + runner = CliRunner() + result = runner.invoke(subcommand, ["--help"]) + assert result.exit_code == 0 + assert result.output.startswith(output_start) diff --git a/tests/unit/cli/test_image.py b/tests/unit/cli/test_image.py index fd49741..9d19389 100644 --- a/tests/unit/cli/test_image.py +++ b/tests/unit/cli/test_image.py @@ -93,6 +93,7 @@ def test_is_analyzing(self, monkeypatch, response): runner = CliRunner() response(success=False) result = runner.invoke(image.query_vuln, ["centos/centos:8", "all"]) + assert result.exc_info == "" assert result.exit_code == 100 def test_not_yet_analyzed(self, monkeypatch, response): @@ -235,3 +236,25 @@ def test_delete_failed(self, monkeypatch, response): response(success=True) result = runner.invoke(image.delete, ["centos/centos:8"]) assert result.exit_code == 1 + + +class TestImageSubcommandHelp: + @pytest.mark.parametrize( + "subcommand, output_start", + [ + (image.wait, "Usage: wait"), + (image.add, "Usage: add"), + (image.import_image, "Usage: import"), + (image.get, "Usage: get"), + (image.imagelist, "Usage: list"), + (image.query_content, "Usage: content"), + (image.query_metadata, "Usage: metadata"), + (image.query_vuln, "Usage: vuln"), + (image.delete, "Usage: del"), + ] + ) + def test_image_subcommand_help(self, subcommand, output_start): + runner = CliRunner() + result = runner.invoke(subcommand, ["--help"]) + assert result.exit_code == 0 + assert result.output.startswith(output_start) \ No newline at end of file diff --git a/tests/unit/cli/test_policy.py b/tests/unit/cli/test_policy.py index 561a91f..a798596 100644 --- a/tests/unit/cli/test_policy.py +++ b/tests/unit/cli/test_policy.py @@ -1 +1,26 @@ +import pytest from anchorecli.cli import policy +from click.testing import CliRunner + + +class TestPolicySubcommandHelp: + @pytest.mark.parametrize( + "subcommand, output_start", + [ + (policy.add, "Usage: add"), + (policy.get, "Usage: get"), + (policy.policylist, "Usage: list"), + (policy.activate, "Usage: activate"), + (policy.delete, "Usage: del"), + (policy.describe, "Usage: describe"), + (policy.hub, "Usage: hub"), + (policy.hublist, "Usage: list"), + (policy.hubget, "Usage: get"), + (policy.hubinstall, "Usage: install"), + ] + ) + def test_policy_subcommand_help(self, subcommand, output_start): + runner = CliRunner() + result = runner.invoke(subcommand, ["--help"]) + assert result.exit_code == 0 + assert result.output.startswith(output_start) diff --git a/tests/unit/cli/test_query.py b/tests/unit/cli/test_query.py index 7c65b8d..c84b2ff 100644 --- a/tests/unit/cli/test_query.py +++ b/tests/unit/cli/test_query.py @@ -1 +1,18 @@ +import pytest from anchorecli.cli import query +from click.testing import CliRunner + + +class TestQuerySubcommandHelp: + @pytest.mark.parametrize( + "subcommand, output_start", + [ + (query.images_by_vulnerability, "Usage: images-by-vulnerability"), + (query.images_by_package, "Usage: images-by-package"), + ] + ) + def test_query_subcommand_help(self, subcommand, output_start): + runner = CliRunner() + result = runner.invoke(subcommand, ["--help"]) + assert result.exit_code == 0 + assert result.output.startswith(output_start) diff --git a/tests/unit/cli/test_registry.py b/tests/unit/cli/test_registry.py index 30a0d0c..f762940 100644 --- a/tests/unit/cli/test_registry.py +++ b/tests/unit/cli/test_registry.py @@ -1 +1,21 @@ +import pytest from anchorecli.cli import registry +from click.testing import CliRunner + + +class TestRegistrySubcommandHelp: + @pytest.mark.parametrize( + "subcommand, output_start", + [ + (registry.add, "Usage: add"), + (registry.upd, "Usage: update"), + (registry.delete, "Usage: del"), + (registry.registrylist, "Usage: list"), + (registry.get, "Usage: get"), + ] + ) + def test_registry_subcommand_help(self, subcommand, output_start): + runner = CliRunner() + result = runner.invoke(subcommand, ["--help"]) + assert result.exit_code == 0 + assert result.output.startswith(output_start) diff --git a/tests/unit/cli/test_repo.py b/tests/unit/cli/test_repo.py index f783069..20c74e4 100644 --- a/tests/unit/cli/test_repo.py +++ b/tests/unit/cli/test_repo.py @@ -1 +1,22 @@ +import pytest from anchorecli.cli import repo +from click.testing import CliRunner + + +class TestRepoSubcommandHelp: + @pytest.mark.parametrize( + "subcommand, output_start", + [ + (repo.add, "Usage: add"), + (repo.listrepos, "Usage: list"), + (repo.get, "Usage: get"), + (repo.delete, "Usage: del"), + (repo.unwatch, "Usage: unwatch"), + (repo.watch, "Usage: watch"), + ] + ) + def test_repo_subcommand_help(self, subcommand, output_start): + runner = CliRunner() + result = runner.invoke(subcommand, ["--help"]) + assert result.exit_code == 0 + assert result.output.startswith(output_start) diff --git a/tests/unit/cli/test_subscription.py b/tests/unit/cli/test_subscription.py index 51a3561..a3a184d 100644 --- a/tests/unit/cli/test_subscription.py +++ b/tests/unit/cli/test_subscription.py @@ -1 +1,19 @@ +import pytest from anchorecli.cli import subscription +from click.testing import CliRunner + + +class TestSubscriptionSubcommandHelp: + @pytest.mark.parametrize( + "subcommand, output_start", + [ + (subscription.activate, "Usage: activate"), + (subscription.deactivate, "Usage: deactivate"), + (subscription.subscriptionlist, "Usage: list"), + ] + ) + def test_subscription_subcommand_help(self, subcommand, output_start): + runner = CliRunner() + result = runner.invoke(subcommand, ["--help"]) + assert result.exit_code == 0 + assert result.output.startswith(output_start) diff --git a/tests/unit/cli/test_system.py b/tests/unit/cli/test_system.py index fb663e0..9470e94 100644 --- a/tests/unit/cli/test_system.py +++ b/tests/unit/cli/test_system.py @@ -124,3 +124,25 @@ def test_wait_for_enabled_feed(monkeypatch, make_feed_response, patch_for_feeds_ result = runner.invoke(system.wait, ["--servicesready", "", "--timeout", "5"]) assert result.exit_code == 0 assert "Feed sync: Success" in result.output + + +class TestSystemSubcommandHelp: + @pytest.mark.parametrize( + "subcommand, output_start", + [ + (system.status, "Usage: status"), + (system.describe_errorcodes, "Usage: errorcodes"), + (system.wait, "Usage: wait"), + (system.delete, "Usage: del"), + (system.feeds, "Usage: feeds"), + (system.list, "Usage: list"), + (system.feedsync, "Usage: sync"), + (system.toggle_enabled, "Usage: config"), + (system.delete_data, "Usage: delete"), + ] + ) + def test_event_subcommand_help(self, subcommand, output_start): + runner = CliRunner() + result = runner.invoke(subcommand, ["--help"]) + assert result.exit_code == 0 + assert result.output.startswith(output_start) From 1da84756f39eb677f06d64939dc60d029227a494 Mon Sep 17 00:00:00 2001 From: Daniel Palmer Date: Tue, 15 Dec 2020 14:01:50 -0500 Subject: [PATCH 03/19] Remove unintended extra assert. Signed-off-by: Daniel Palmer --- tests/unit/cli/test_image.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/unit/cli/test_image.py b/tests/unit/cli/test_image.py index 9d19389..7b80071 100644 --- a/tests/unit/cli/test_image.py +++ b/tests/unit/cli/test_image.py @@ -93,7 +93,6 @@ def test_is_analyzing(self, monkeypatch, response): runner = CliRunner() response(success=False) result = runner.invoke(image.query_vuln, ["centos/centos:8", "all"]) - assert result.exc_info == "" assert result.exit_code == 100 def test_not_yet_analyzed(self, monkeypatch, response): From c5bfc1919ec60c4a4217d899601cee35713c8d17 Mon Sep 17 00:00:00 2001 From: Daniel Palmer Date: Wed, 16 Dec 2020 11:58:38 -0500 Subject: [PATCH 04/19] Fix failing tests. Signed-off-by: Daniel Palmer --- tests/unit/cli/test_image.py | 31 ++++++++++++++++++++++--------- 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/tests/unit/cli/test_image.py b/tests/unit/cli/test_image.py index 7b80071..a2e1fb9 100644 --- a/tests/unit/cli/test_image.py +++ b/tests/unit/cli/test_image.py @@ -1,5 +1,7 @@ import pytest -from anchorecli.cli import image + +from anchorecli.cli import image, utils +from click import Context from click.testing import CliRunner @@ -81,6 +83,14 @@ def apply(success=True, httpcode=200, has_error=None): "/usr/local/lib64/python3.6/site-packages/aubio", ] +def mock_empty_callback(): + pass + +def get_mock_parent_ctx(): + # The command supplied here will not be used in a way tht impacts the tests, and can + # be any arbitrary, non-root command + return Context(image.query_vuln, obj=utils.ContextObject(None, mock_empty_callback)) + class TestQueryVuln: def test_is_analyzing(self, monkeypatch, response): @@ -92,7 +102,10 @@ def test_is_analyzing(self, monkeypatch, response): monkeypatch.setattr(image, "config", {"jsonmode": False}) runner = CliRunner() response(success=False) - result = runner.invoke(image.query_vuln, ["centos/centos:8", "all"]) + # image.query_vuln expects a context containing a parent context with a callback for it to run. + # In production this is provided by the image group command, and should never be skipped. + # Since that doesn't exist here, just mock it with a no-op callback + result = runner.invoke(image.query_vuln, ["centos/centos:8", "all"], parent=get_mock_parent_ctx()) assert result.exit_code == 100 def test_not_yet_analyzed(self, monkeypatch, response): @@ -111,7 +124,7 @@ def test_not_yet_analyzed(self, monkeypatch, response): "message": "image is not analyzed - analysis_status: not_analyzed", }, ) - result = runner.invoke(image.query_vuln, ["centos/centos:8", "all"]) + result = runner.invoke(image.query_vuln, ["centos/centos:8", "all"], parent=get_mock_parent_ctx()) assert result.exit_code == 101 @pytest.mark.parametrize("item", headers) @@ -124,7 +137,7 @@ def test_success_headers(self, monkeypatch, response, item): monkeypatch.setattr(image, "config", {"jsonmode": False}) runner = CliRunner() response(success=True) - result = runner.invoke(image.query_vuln, ["centos/centos:8", "all"]) + result = runner.invoke(image.query_vuln, ["centos/centos:8", "all"], parent=get_mock_parent_ctx()) assert result.exit_code == 0 assert item in result.stdout @@ -138,7 +151,7 @@ def test_success_info(self, monkeypatch, response, item): monkeypatch.setattr(image, "config", {"jsonmode": False}) runner = CliRunner() response(success=True) - result = runner.invoke(image.query_vuln, ["centos/centos:8", "all"]) + result = runner.invoke(image.query_vuln, ["centos/centos:8", "all"], parent=get_mock_parent_ctx()) assert result.exit_code == 0 assert item in result.stdout @@ -163,7 +176,7 @@ def test_deleted_pre_v080(self, monkeypatch, response): ) runner = CliRunner() response(success=True) - result = runner.invoke(image.delete, ["centos/centos:8"]) + result = runner.invoke(image.delete, ["centos/centos:8"], parent=get_mock_parent_ctx()) assert result.exit_code == 0 def test_delete_failed_pre_v080(self, monkeypatch, response): @@ -185,7 +198,7 @@ def test_delete_failed_pre_v080(self, monkeypatch, response): ) runner = CliRunner() response(success=True) - result = runner.invoke(image.delete, ["centos/centos:8"]) + result = runner.invoke(image.delete, ["centos/centos:8"], parent=get_mock_parent_ctx()) assert result.exit_code == 1 def test_is_deleting(self, monkeypatch, response): @@ -207,7 +220,7 @@ def test_is_deleting(self, monkeypatch, response): ) runner = CliRunner() response(success=True) - result = runner.invoke(image.delete, ["centos/centos:8"]) + result = runner.invoke(image.delete, ["centos/centos:8"], parent=get_mock_parent_ctx()) assert result.exit_code == 0 def test_delete_failed(self, monkeypatch, response): @@ -233,7 +246,7 @@ def test_delete_failed(self, monkeypatch, response): ) runner = CliRunner() response(success=True) - result = runner.invoke(image.delete, ["centos/centos:8"]) + result = runner.invoke(image.delete, ["centos/centos:8"], parent=get_mock_parent_ctx()) assert result.exit_code == 1 From 6cc1c308efd42254bce047a9158d34ee507a20c7 Mon Sep 17 00:00:00 2001 From: Daniel Palmer Date: Wed, 16 Dec 2020 11:59:32 -0500 Subject: [PATCH 05/19] Throw exception and gracefully fail if parent callback is missing from context. Signed-off-by: Daniel Palmer --- anchorecli/cli/account.py | 101 ++++++++++++++++++++------------- anchorecli/cli/archives.py | 46 +++++++++------ anchorecli/cli/evaluate.py | 4 +- anchorecli/cli/event.py | 12 ++-- anchorecli/cli/image.py | 39 ++++++------- anchorecli/cli/policy.py | 47 ++++++++------- anchorecli/cli/query.py | 9 ++- anchorecli/cli/registry.py | 23 ++++---- anchorecli/cli/repo.py | 56 +++++++++--------- anchorecli/cli/subscription.py | 13 +++-- anchorecli/cli/system.py | 32 +++++------ anchorecli/cli/utils.py | 9 ++- 12 files changed, 220 insertions(+), 171 deletions(-) diff --git a/anchorecli/cli/account.py b/anchorecli/cli/account.py index e434c83..5294335 100644 --- a/anchorecli/cli/account.py +++ b/anchorecli/cli/account.py @@ -49,10 +49,19 @@ def execute(): @click.pass_context def get_current_user(ctx): global whoami + ecode = 0 - ctx.parent.obj.execute_callback() + try: + anchorecli.cli.utils.handle_parent_callback(ctx) + except RuntimeError as err: + print( + anchorecli.cli.utils.format_error_output( + config, "get_current_user", {}, err + ) + ) + ecode = 2 + anchorecli.cli.utils.doexit(ecode) - ecode = 0 print(anchorecli.cli.utils.format_output(config, "account_whoami", {}, whoami)) anchorecli.cli.utils.doexit(ecode) @@ -70,11 +79,11 @@ def add(ctx, account_name, email): EMAIL: email address associated with account (optional) """ - ctx.parent.obj.execute_callback() - ecode = 0 try: + anchorecli.cli.utils.handle_parent_callback(ctx) + ret = anchorecli.clients.apiexternal.add_account( config, account_name=account_name, email=email ) @@ -104,11 +113,11 @@ def get(ctx, account_name): ACCOUNT_NAME: name of new account to create """ - ctx.parent.obj.execute_callback() - ecode = 0 try: + anchorecli.cli.utils.handle_parent_callback(ctx) + ret = anchorecli.clients.apiexternal.get_account( config, account_name=account_name ) @@ -136,10 +145,11 @@ def get(ctx, account_name): @click.pass_context def list_accounts(ctx): """""" - ctx.parent.obj.execute_callback() - ecode = 0 + try: + anchorecli.cli.utils.handle_parent_callback(ctx) + ret = anchorecli.clients.apiexternal.list_accounts(config) ecode = anchorecli.cli.utils.get_ecode(ret) if ret["success"]: @@ -171,10 +181,19 @@ def delete(ctx, account_name, dontask): ACCOUNT_NAME: name of account to delete (must be disabled first) """ - ctx.parent.obj.execute_callback() - ecode = 0 + try: + anchorecli.cli.utils.handle_parent_callback(ctx) + except RuntimeError as err: + print( + anchorecli.cli.utils.format_error_output( + config, "account_delete", {}, err + ) + ) + ecode = 2 + anchorecli.cli.utils.doexit(ecode) + answer = "n" if dontask: answer = "y" @@ -227,11 +246,11 @@ def enable(ctx, account_name): ACCOUNT_NAME: name of account to enable """ - ctx.parent.obj.execute_callback() - ecode = 0 try: + anchorecli.cli.utils.handle_parent_callback(ctx) + ret = anchorecli.clients.apiexternal.enable_account( config, account_name=account_name ) @@ -263,11 +282,11 @@ def disable(ctx, account_name): ACCOUNT_NAME: name of account to disable """ - ctx.parent.obj.execute_callback() - ecode = 0 try: + anchorecli.cli.utils.handle_parent_callback(ctx) + ret = anchorecli.clients.apiexternal.disable_account( config, account_name=account_name ) @@ -314,14 +333,14 @@ def user_add(ctx, user_name, user_password, account): ACCOUNT: optional name of the account to act as """ - ctx.parent.obj.execute_callback() - - if not account: - account = whoami.get("account", {}).get("name", None) - ecode = 0 try: + anchorecli.cli.utils.handle_parent_callback(ctx) + + if not account: + account = whoami.get("account", {}).get("name", None) + # do some input validation if not re.match(".{6,128}$", user_password): raise Exception( @@ -362,14 +381,14 @@ def user_delete(ctx, user_name, account): ACCOUNT: optional name of the account to act as """ - ctx.parent.obj.execute_callback() - - if not account: - account = whoami.get("account", {}).get("name", None) - ecode = 0 try: + anchorecli.cli.utils.handle_parent_callback(ctx) + + if not account: + account = whoami.get("account", {}).get("name", None) + ret = anchorecli.clients.apiexternal.del_user( config, account_name=account, user_name=user_name ) @@ -401,14 +420,14 @@ def user_get(ctx, user_name, account): ACCOUNT: optional name of the account to act as """ - ctx.parent.obj.execute_callback() - - if not account: - account = whoami.get("account", {}).get("name", None) - ecode = 0 try: + anchorecli.cli.utils.handle_parent_callback(ctx) + + if not account: + account = whoami.get("account", {}).get("name", None) + ret = anchorecli.clients.apiexternal.get_user( config, account_name=account, user_name=user_name ) @@ -439,14 +458,14 @@ def user_list(ctx, account): ACCOUNT: optional name of the account to act as """ - ctx.parent.obj.execute_callback() - - if not account: - account = whoami.get("account", {}).get("name", None) - ecode = 0 try: + anchorecli.cli.utils.handle_parent_callback(ctx) + + if not account: + account = whoami.get("account", {}).get("name", None) + ret = anchorecli.clients.apiexternal.list_users(config, account_name=account) ecode = anchorecli.cli.utils.get_ecode(ret) if ret["success"]: @@ -477,16 +496,16 @@ def user_setpassword(ctx, user_password, username, account): ACCOUNT: optional name of the account to act as """ - ctx.parent.obj.execute_callback() - - if not account: - account = whoami.get("account", {}).get("name", None) - if not username: - username = whoami.get("user", {}).get("username", None) - ecode = 0 try: + anchorecli.cli.utils.handle_parent_callback(ctx) + + if not account: + account = whoami.get("account", {}).get("name", None) + if not username: + username = whoami.get("user", {}).get("username", None) + ret = anchorecli.clients.apiexternal.update_user_password( config, account_name=account, diff --git a/anchorecli/cli/archives.py b/anchorecli/cli/archives.py index 09c16c8..94a2236 100644 --- a/anchorecli/cli/archives.py +++ b/anchorecli/cli/archives.py @@ -43,11 +43,11 @@ def image_restore(ctx, image_digest): """ Add an analyzed image to the analysis archive """ - ctx.parent.obj.execute_callback() - ecode = 0 try: + anchorecli.cli.utils.handle_parent_callback(ctx) + if not re.match(digest_regex, image_digest): raise Exception( "Invalid image digest {}. Must conform to regex: {}".format( @@ -86,11 +86,11 @@ def image_add(ctx, image_digests): """ Add an analyzed image to the analysis archive """ - ctx.parent.obj.execute_callback() - ecode = 0 try: + anchorecli.cli.utils.handle_parent_callback(ctx) + for digest in image_digests: if not re.match(digest_regex, digest): raise Exception( @@ -129,11 +129,11 @@ def image_get(ctx, digest): """ INPUT_IMAGE: Input Image Digest (ex. sha256:95c9a61d949bbc622a444202e7faf9529f0dab5773023f173f602151f3a107b3) """ - ctx.parent.obj.execute_callback() - ecode = 0 try: + anchorecli.cli.utils.handle_parent_callback(ctx) + ret = anchorecli.clients.apiexternal.get_archived_analysis(config, digest) if ret: @@ -168,11 +168,11 @@ def image_get(ctx, digest): @images.command(name="list", short_help="List all archived image analyses") @click.pass_context def list_archived_analyses(ctx): - ctx.parent.obj.execute_callback() - ecode = 0 try: + anchorecli.cli.utils.handle_parent_callback(ctx) + ret = anchorecli.clients.apiexternal.list_archived_analyses(config) ecode = anchorecli.cli.utils.get_ecode(ret) if ret["success"]: @@ -204,11 +204,11 @@ def image_delete(ctx, digest, force): """ INPUT_IMAGE: Input Image Digest (ex. sha256:95c9a61d949bbc622a444202e7faf9529f0dab5773023f173f602151f3a107b3) """ - ctx.parent.obj.execute_callback() - ecode = 0 try: + anchorecli.cli.utils.handle_parent_callback(ctx) + ret = anchorecli.clients.apiexternal.delete_archived_analysis(config, digest) if ret: @@ -313,8 +313,6 @@ def rule_add( exclude_expiration_days: Number of days until exclude block expires """ - ctx.parent.obj.execute_callback() - ecode = 0 if days_old == 0 and tag_versions_newer == 0: @@ -346,6 +344,18 @@ def rule_add( anchorecli.cli.utils.doexit(2) try: + anchorecli.cli.utils.handle_parent_callback(ctx) + + if days_old == 0 and tag_versions_newer == 0: + resp = click.prompt( + "Are you sure you want to use 0 for both days old limit and number of tag versions newer? WARNING: This will archive all images that match the registry/repo/tag selectors as soon as they are analyzed", + type=click.Choice(["y", "n"]), + default="n", + ) + if resp.lower() != "y": + ecode = 0 + anchorecli.cli.utils.doexit(ecode) + ret = anchorecli.clients.apiexternal.add_transition_rule( config, days_old, @@ -395,11 +405,11 @@ def is_exclude_default(repo, registry, tag): @click.argument("rule_id", nargs=1) @click.pass_context def rule_get(ctx, rule_id): - ctx.parent.obj.execute_callback() - ecode = 0 try: + anchorecli.cli.utils.handle_parent_callback(ctx) + ret = anchorecli.clients.apiexternal.get_transition_rule(config, rule_id) if ret: @@ -430,11 +440,11 @@ def rule_get(ctx, rule_id): @rules.command(name="list", short_help="List all transition rules for the account") @click.pass_context def list_transition_rules(ctx): - ctx.parent.obj.execute_callback() - ecode = 0 try: + anchorecli.cli.utils.handle_parent_callback(ctx) + ret = anchorecli.clients.apiexternal.list_transition_rules(config) ecode = anchorecli.cli.utils.get_ecode(ret) if ret["success"]: @@ -462,11 +472,11 @@ def list_transition_rules(ctx): @click.argument("rule_id") @click.pass_context def rule_delete(ctx, rule_id): - ctx.parent.obj.execute_callback() - ecode = 0 try: + anchorecli.cli.utils.handle_parent_callback(ctx) + ret = anchorecli.clients.apiexternal.delete_transition_rule(config, rule_id) if ret: diff --git a/anchorecli/cli/evaluate.py b/anchorecli/cli/evaluate.py index b1563ac..8c62599 100644 --- a/anchorecli/cli/evaluate.py +++ b/anchorecli/cli/evaluate.py @@ -44,11 +44,11 @@ def check(ctx, input_image, show_history, detail, tag, policy): """ INPUT_IMAGE: Input image can be in the following formats: Image Digest, ImageID or registry/repo:tag """ - ctx.parent.obj.execute_callback() - ecode = 0 try: + anchorecli.cli.utils.handle_parent_callback(ctx) + itype, image, imageDigest = anchorecli.cli.utils.discover_inputimage( config, input_image ) diff --git a/anchorecli/cli/event.py b/anchorecli/cli/event.py index 0c3caa6..617bf0d 100644 --- a/anchorecli/cli/event.py +++ b/anchorecli/cli/event.py @@ -99,11 +99,11 @@ def list( """ RESOURCE: Value can be a tag, image digest or repository name. Displays results related to the specific resource """ - ctx.parent.obj.execute_callback() - ecode = 0 try: + anchorecli.cli.utils.handle_parent_callback(ctx) + if level: if level.lower() not in ["info", "error"]: raise Exception( @@ -157,11 +157,11 @@ def get(ctx, event_id): """ EVENT_ID: ID of the event to be fetched """ - ctx.parent.obj.execute_callback() - ecode = 0 try: + anchorecli.cli.utils.handle_parent_callback(ctx) + ret = anchorecli.clients.apiexternal.get_event(config, event_id=event_id) ecode = anchorecli.cli.utils.get_ecode(ret) if ret["success"]: @@ -209,11 +209,11 @@ def delete(ctx, since=None, before=None, dontask=False, event_id=None, all=False NOTE: if no options are provided, delete (clear) all events in the engine. To skip the prompt in this case, use the --dontask flag. """ - ctx.parent.obj.execute_callback() - ecode = 0 try: + anchorecli.cli.utils.handle_parent_callback(ctx) + if event_id: ret = anchorecli.clients.apiexternal.delete_event(config, event_id=event_id) else: diff --git a/anchorecli/cli/image.py b/anchorecli/cli/image.py index 12a3959..0482703 100644 --- a/anchorecli/cli/image.py +++ b/anchorecli/cli/image.py @@ -15,7 +15,6 @@ @click.group(name="image", short_help="Image operations") @click.pass_context def image(ctx): - def execute(): global config config = ctx.parent.obj.config @@ -52,11 +51,11 @@ def wait(ctx, input_image, timeout, interval): :param timeout: :return: """ - ctx.parent.obj.execute_callback() - ecode = 0 try: + anchorecli.cli.utils.handle_parent_callback(ctx) + itype = anchorecli.cli.utils.discover_inputimage_format(config, input_image) image = input_image # timeout = float(timeout) @@ -147,11 +146,11 @@ def add(ctx, input_image, force, dockerfile, annotation, noautosubscribe): """ INPUT_IMAGE: Input image can be in the following formats: registry/repo:tag """ - ctx.parent.obj.execute_callback() - ecode = 0 try: + anchorecli.cli.utils.handle_parent_callback(ctx) + itype = anchorecli.cli.utils.discover_inputimage_format(config, input_image) dockerfile_contents = None @@ -214,11 +213,11 @@ def add(ctx, input_image, force, dockerfile, annotation, noautosubscribe): ) @click.pass_context def import_image(ctx, infile): - ctx.parent.obj.execute_callback() - ecode = 0 try: + anchorecli.cli.utils.handle_parent_callback(ctx) + with open(infile, "r") as FH: anchore_data = json.loads(FH.read()) @@ -255,11 +254,11 @@ def get(ctx, input_image, show_history): """ INPUT_IMAGE: Input image can be in the following formats: Image Digest, ImageID or registry/repo:tag """ - ctx.parent.obj.execute_callback() - ecode = 0 try: + anchorecli.cli.utils.handle_parent_callback(ctx) + itype = anchorecli.cli.utils.discover_inputimage_format(config, input_image) image = input_image @@ -310,11 +309,11 @@ def get(ctx, input_image, show_history): ) @click.pass_context def imagelist(ctx, full, show_all): - ctx.parent.obj.execute_callback() - ecode = 0 try: + anchorecli.cli.utils.handle_parent_callback(ctx) + ret = anchorecli.clients.apiexternal.get_images(config) ecode = anchorecli.cli.utils.get_ecode(ret) if ret["success"]: @@ -349,11 +348,11 @@ def query_content(ctx, input_image, content_type): available for an image, run the following command: $ anchore-cli image content """ - ctx.parent.obj.execute_callback() - ecode = 0 try: + anchorecli.cli.utils.handle_parent_callback(ctx) + itype, image, imageDigest = anchorecli.cli.utils.discover_inputimage( config, input_image ) @@ -415,11 +414,11 @@ def query_metadata(ctx, input_image, metadata_type): METADATA_TYPE: The metadata type can be one of the types returned by running without a type specified """ - ctx.parent.obj.execute_callback() - ecode = 0 try: + anchorecli.cli.utils.handle_parent_callback(ctx) + itype, image, imageDigest = anchorecli.cli.utils.discover_inputimage( config, input_image ) @@ -484,10 +483,10 @@ def query_vuln(ctx, input_image, vuln_type, vendor_only): - os: CVE/distro vulnerabilities against operating system packages """ - ctx.parent.obj.execute_callback() - ecode = 0 try: + anchorecli.cli.utils.handle_parent_callback(ctx) + itype, image, imageDigest = anchorecli.cli.utils.discover_inputimage( config, input_image ) @@ -543,12 +542,12 @@ def delete(ctx, input_image, force, all): """ INPUT_IMAGE: Input image can be in the following formats: Image Digest, ImageID or registry/repo:tag """ - ctx.parent.obj.execute_callback() - ecode = 0 if all: try: + anchorecli.cli.utils.handle_parent_callback(ctx) + ret = anchorecli.clients.apiexternal.get_images(config) ecode = anchorecli.cli.utils.get_ecode(ret) if not ret["success"]: @@ -589,6 +588,8 @@ def delete(ctx, input_image, force, all): ecode = 2 else: try: + anchorecli.cli.utils.handle_parent_callback(ctx) + if input_image is None: raise Exception("Missing argument INPUT_IMAGE") diff --git a/anchorecli/cli/policy.py b/anchorecli/cli/policy.py index 36d1296..8f45499 100644 --- a/anchorecli/cli/policy.py +++ b/anchorecli/cli/policy.py @@ -34,11 +34,11 @@ def execute(): ) @click.pass_context def add(ctx, input_policy): - ctx.parent.obj.execute_callback() - ecode = 0 try: + anchorecli.cli.utils.handle_parent_callback(ctx) + with open(input_policy, "r") as FH: policybundle = json.loads(FH.read()) @@ -71,11 +71,11 @@ def get(ctx, policyid, detail): """ POLICYID: Policy ID to get """ - ctx.parent.obj.execute_callback() - ecode = 0 try: + anchorecli.cli.utils.handle_parent_callback(ctx) + ret = anchorecli.clients.apiexternal.get_policy( config, policyId=policyid, detail=detail ) @@ -100,11 +100,11 @@ def get(ctx, policyid, detail): @policy.command(name="list", short_help="List all policies") @click.pass_context def policylist(ctx): - ctx.parent.obj.execute_callback() - ecode = 0 try: + anchorecli.cli.utils.handle_parent_callback(ctx) + ret = anchorecli.clients.apiexternal.get_policies(config, detail=False) ecode = anchorecli.cli.utils.get_ecode(ret) if ret["success"]: @@ -131,11 +131,11 @@ def activate(ctx, policyid): """ POLICYID: Policy ID to be activated """ - ctx.parent.obj.execute_callback() - ecode = 0 try: + anchorecli.cli.utils.handle_parent_callback(ctx) + ret = anchorecli.clients.apiexternal.get_policy( config, policyId=policyid, detail=True ) @@ -183,11 +183,11 @@ def delete(ctx, policyid): """ POLICYID: Policy ID to delete """ - ctx.parent.obj.execute_callback() - ecode = 0 try: + anchorecli.cli.utils.handle_parent_callback(ctx) + ret = anchorecli.clients.apiexternal.delete_policy(config, policyId=policyid) ecode = anchorecli.cli.utils.get_ecode(ret) if ret["success"]: @@ -225,10 +225,10 @@ def delete(ctx, policyid): ) @click.pass_context def describe(ctx, all=False, gate=None, trigger=None): - ctx.parent.obj.execute_callback() - ecode = 0 try: + anchorecli.cli.utils.handle_parent_callback(ctx) + ret = anchorecli.clients.apiexternal.describe_policy_spec(config) if ret["success"]: @@ -277,7 +277,16 @@ def describe(ctx, all=False, gate=None, trigger=None): @click.pass_context def hub(ctx): def execute(): - ctx.parent.obj.execute_callback() + try: + anchorecli.cli.utils.handle_parent_callback(ctx) + except RuntimeError as err: + print( + anchorecli.cli.utils.format_error_output( + config, "policy_hub", {}, err + ) + ) + ecode = 2 + anchorecli.cli.utils.doexit(ecode) if ctx.invoked_subcommand not in ["list", "get"]: try: @@ -292,11 +301,11 @@ def execute(): @hub.command(name="list") @click.pass_context def hublist(ctx): - ctx.parent.obj.execute_callback() - ecode = 0 try: + anchorecli.cli.utils.handle_parent_callback(ctx) + ret = anchorecli.clients.hub.get_policies(config) if ret["success"]: print( @@ -321,11 +330,11 @@ def hublist(ctx): @click.argument("bundlename", nargs=1) @click.pass_context def hubget(ctx, bundlename): - ctx.parent.obj.execute_callback() - ecode = 0 try: + anchorecli.cli.utils.handle_parent_callback(ctx) + ret = anchorecli.clients.hub.get_policy(config, bundlename) if ret["success"]: print( @@ -356,11 +365,11 @@ def hubget(ctx, bundlename): ) @click.pass_context def hubinstall(ctx, bundlename, target_id, force): - ctx.parent.obj.execute_callback() - ecode = 0 try: + anchorecli.cli.utils.handle_parent_callback(ctx) + ret = anchorecli.clients.hub.install_policy( config, bundlename, target_id=target_id, force=force ) diff --git a/anchorecli/cli/query.py b/anchorecli/cli/query.py index a34ae77..3146465 100644 --- a/anchorecli/cli/query.py +++ b/anchorecli/cli/query.py @@ -53,12 +53,14 @@ def execute(): def images_by_vulnerability( ctx, vulnerability_id, namespace, package, severity, vendor_only ): - """ """ + """""" ctx.parent.obj.execute_callback() ecode = 0 try: + anchorecli.cli.utils.handle_parent_callback(ctx) + ret = anchorecli.clients.apiexternal.query_images_by_vulnerability( config, vulnerability_id, @@ -107,12 +109,13 @@ def images_by_vulnerability( ) @click.pass_context def images_by_package(ctx, name, version, package_type): - """ """ + """""" ctx.parent.obj.execute_callback() - ecode = 0 try: + anchorecli.cli.utils.handle_parent_callback(ctx) + ret = anchorecli.clients.apiexternal.query_images_by_package( config, name, version=version, package_type=package_type ) diff --git a/anchorecli/cli/registry.py b/anchorecli/cli/registry.py index 2550461..13ffeda 100644 --- a/anchorecli/cli/registry.py +++ b/anchorecli/cli/registry.py @@ -62,13 +62,12 @@ def add( REGISTRY_PASS: Password """ - ctx.parent.obj.execute_callback() - ecode = 0 - registry_types = ["docker_v2", "awsecr"] - try: + anchorecli.cli.utils.handle_parent_callback(ctx) + + registry_types = ["docker_v2", "awsecr"] if registry_type and registry_type not in registry_types: raise Exception( "input registry type not supported (supported registry_types: " @@ -158,11 +157,11 @@ def upd( REGISTRY_PASS: Password """ - ctx.parent.obj.execute_callback() - ecode = 0 try: + anchorecli.cli.utils.handle_parent_callback(ctx) + if not registry_name: registry_name = registry @@ -202,11 +201,11 @@ def delete(ctx, registry): """ REGISTRY: Full hostname/port of registry. Eg. myrepo.example.com:5000 """ - ctx.parent.obj.execute_callback() - ecode = 0 try: + anchorecli.cli.utils.handle_parent_callback(ctx) + ret = anchorecli.clients.apiexternal.delete_registry(config, registry) ecode = anchorecli.cli.utils.get_ecode(ret) if ret["success"]: @@ -230,11 +229,11 @@ def delete(ctx, registry): @registry.command(name="list", short_help="List all current registries") @click.pass_context def registrylist(ctx): - ctx.parent.obj.execute_callback() - ecode = 0 try: + anchorecli.cli.utils.handle_parent_callback(ctx) + ret = anchorecli.clients.apiexternal.get_registry(config) ecode = anchorecli.cli.utils.get_ecode(ret) if ret["success"]: @@ -262,11 +261,11 @@ def get(ctx, registry): """ REGISTRY: Full hostname/port of registry. Eg. myrepo.example.com:5000 """ - ctx.parent.obj.execute_callback() - ecode = 0 try: + anchorecli.cli.utils.handle_parent_callback(ctx) + ret = anchorecli.clients.apiexternal.get_registry(config, registry=registry) ecode = anchorecli.cli.utils.get_ecode(ret) if ret["success"]: diff --git a/anchorecli/cli/repo.py b/anchorecli/cli/repo.py index 2029dee..c4aa59f 100644 --- a/anchorecli/cli/repo.py +++ b/anchorecli/cli/repo.py @@ -46,15 +46,15 @@ def add(ctx, input_repo, noautosubscribe, lookuptag, dryrun): """ INPUT_REPO: Input repository can be in the following formats: registry/repo """ - ctx.parent.obj.execute_callback() - response_code = 0 - auto_subscribe = not noautosubscribe - image_info = anchorecli.cli.utils.parse_dockerimage_string(input_repo) - input_repo = image_info["registry"] + "/" + image_info["repo"] - try: + anchorecli.cli.utils.handle_parent_callback(ctx) + + autosubscribe = not noautosubscribe + image_info = anchorecli.cli.utils.parse_dockerimage_string(input_repo) + input_repo = image_info["registry"] + "/" + image_info["repo"] + ret = anchorecli.clients.apiexternal.add_repo( config, input_repo, @@ -83,11 +83,11 @@ def add(ctx, input_repo, noautosubscribe, lookuptag, dryrun): @repo.command(name="list", short_help="List added repositories") @click.pass_context def listrepos(ctx): - ctx.parent.obj.execute_callback() - ecode = 0 try: + anchorecli.cli.utils.handle_parent_callback(ctx) + ret = anchorecli.clients.apiexternal.get_repo(config) ecode = anchorecli.cli.utils.get_ecode(ret) if ret["success"]: @@ -114,14 +114,14 @@ def get(ctx, input_repo): """ INPUT_REPO: Input repository can be in the following formats: registry/repo """ - ctx.parent.obj.execute_callback() - ecode = 0 - image_info = anchorecli.cli.utils.parse_dockerimage_string(input_repo) - input_repo = image_info["registry"] + "/" + image_info["repo"] - try: + anchorecli.cli.utils.handle_parent_callback(ctx) + + image_info = anchorecli.cli.utils.parse_dockerimage_string(input_repo) + input_repo = image_info["registry"] + "/" + image_info["repo"] + ret = anchorecli.clients.apiexternal.get_repo(config, input_repo=input_repo) if ret: ecode = anchorecli.cli.utils.get_ecode(ret) @@ -154,14 +154,14 @@ def delete(ctx, input_repo): """ INPUT_REPO: Input repo can be in the following formats: registry/repo """ - ctx.parent.obj.execute_callback() - ecode = 0 - image_info = anchorecli.cli.utils.parse_dockerimage_string(input_repo) - input_repo = image_info["registry"] + "/" + image_info["repo"] - try: + anchorecli.cli.utils.handle_parent_callback(ctx) + + image_info = anchorecli.cli.utils.parse_dockerimage_string(input_repo) + input_repo = image_info["registry"] + "/" + image_info["repo"] + ret = anchorecli.clients.apiexternal.delete_repo(config, input_repo) ecode = anchorecli.cli.utils.get_ecode(ret) if ret: @@ -194,14 +194,14 @@ def unwatch(ctx, input_repo): """ INPUT_REPO: Input repo can be in the following formats: registry/repo """ - ctx.parent.obj.execute_callback() - ecode = 0 - image_info = anchorecli.cli.utils.parse_dockerimage_string(input_repo) - input_repo = image_info["registry"] + "/" + image_info["repo"] - try: + anchorecli.cli.utils.handle_parent_callback(ctx) + + image_info = anchorecli.cli.utils.parse_dockerimage_string(input_repo) + input_repo = image_info["registry"] + "/" + image_info["repo"] + ret = anchorecli.clients.apiexternal.unwatch_repo(config, input_repo) ecode = anchorecli.cli.utils.get_ecode(ret) if ret: @@ -234,14 +234,14 @@ def watch(ctx, input_repo): """ INPUT_REPO: Input repo can be in the following formats: registry/repo """ - ctx.parent.obj.execute_callback() - ecode = 0 - image_info = anchorecli.cli.utils.parse_dockerimage_string(input_repo) - input_repo = image_info["registry"] + "/" + image_info["repo"] - try: + anchorecli.cli.utils.handle_parent_callback(ctx) + + image_info = anchorecli.cli.utils.parse_dockerimage_string(input_repo) + input_repo = image_info["registry"] + "/" + image_info["repo"] + ret = anchorecli.clients.apiexternal.watch_repo(config, input_repo) ecode = anchorecli.cli.utils.get_ecode(ret) if ret: diff --git a/anchorecli/cli/subscription.py b/anchorecli/cli/subscription.py index 7d82570..29d76b4 100644 --- a/anchorecli/cli/subscription.py +++ b/anchorecli/cli/subscription.py @@ -39,11 +39,11 @@ def activate(ctx, subscription_type, subscription_key): SUBSCRIPTION_KEY: Fully qualified name of tag to subscribe to. Eg. docker.io/library/alpine:latest """ - ctx.parent.obj.execute_callback() - ecode = 0 try: + anchorecli.cli.utils.handle_parent_callback(ctx) + ret = anchorecli.clients.apiexternal.activate_subscription( config, subscription_type, subscription_key ) @@ -85,11 +85,11 @@ def deactivate(ctx, subscription_type, subscription_key): SUBSCRIPTION_KEY: Fully qualified name of tag to subscribe to. Eg. docker.io/library/alpine:latest """ - ctx.parent.obj.execute_callback() - ecode = 0 try: + anchorecli.cli.utils.handle_parent_callback(ctx) + ret = anchorecli.clients.apiexternal.deactivate_subscription( config, subscription_type, subscription_key ) @@ -123,10 +123,11 @@ def deactivate(ctx, subscription_type, subscription_key): ) @click.pass_context def list_subscriptions(ctx, full): - ctx.parent.obj.execute_callback() - ecode = 0 + try: + anchorecli.cli.utils.handle_parent_callback(ctx) + ret = anchorecli.clients.apiexternal.get_subscription(config) ecode = anchorecli.cli.utils.get_ecode(ret) if ret["success"]: diff --git a/anchorecli/cli/system.py b/anchorecli/cli/system.py index f2db6d4..93d8026 100644 --- a/anchorecli/cli/system.py +++ b/anchorecli/cli/system.py @@ -35,11 +35,11 @@ def execute(): @system.command(name="status", short_help="Check current anchore-engine system status") @click.pass_context def status(ctx): - ctx.parent.obj.execute_callback() - ecode = 0 try: + anchorecli.cli.utils.handle_parent_callback(ctx) + ret = anchorecli.clients.apiexternal.system_status(config) ecode = anchorecli.cli.utils.get_ecode(ret) if ret["success"]: @@ -66,11 +66,11 @@ def status(ctx): ) @click.pass_context def describe_errorcodes(ctx): - ctx.parent.obj.execute_callback() - ecode = 0 try: + anchorecli.cli.utils.handle_parent_callback(ctx) + ret = anchorecli.clients.apiexternal.describe_error_codes(config) ecode = anchorecli.cli.utils.get_ecode(ret) if ret["success"]: @@ -129,12 +129,12 @@ def wait(ctx, timeout, interval, feedsready, servicesready): :param feedsready: :return: """ - ctx.parent.obj.execute_callback() - global config ecode = 0 try: + anchorecli.cli.utils.handle_parent_callback(ctx) + sys.stderr.write( "Starting checks to wait for anchore-engine to be available timeout={} interval={}\n".format( timeout, interval @@ -303,11 +303,11 @@ def wait(ctx, timeout, interval, feedsready, servicesready): @click.argument("servicename", nargs=1) @click.pass_context def delete(ctx, host_id, servicename): - ctx.parent.obj.execute_callback() - ecode = 0 try: + anchorecli.cli.utils.handle_parent_callback(ctx) + ret = anchorecli.clients.apiexternal.delete_system_service( config, host_id, servicename ) @@ -342,11 +342,11 @@ def feeds(ctx): @feeds.command(name="list", short_help="Get a list of loaded data feeds.") @click.pass_context def list(ctx): - ctx.parent.obj.execute_callback() - ecode = 0 try: + anchorecli.cli.utils.handle_parent_callback(ctx) + ret = anchorecli.clients.apiexternal.system_feeds_list(config) ecode = anchorecli.cli.utils.get_ecode(ret) if ret["success"]: @@ -377,12 +377,12 @@ def list(ctx): ) @click.pass_context def feedsync(ctx, flush): - ctx.parent.obj.execute_callback() - global input ecode = 0 try: + anchorecli.cli.utils.handle_parent_callback(ctx) + answer = "n" try: print( @@ -431,11 +431,11 @@ def feedsync(ctx, flush): @click.argument("feed") @click.pass_context def toggle_enabled(ctx, feed, group=None, enable=None, disable=None): - ctx.parent.obj.execute_callback() - ecode = 0 try: + anchorecli.cli.utils.handle_parent_callback(ctx) + if not enable and not disable: raise Exception("Must set one of --enable or --disable") elif enable and disable: @@ -490,10 +490,10 @@ def toggle_enabled(ctx, feed, group=None, enable=None, disable=None): @click.argument("feed") @click.pass_context def delete_data(ctx, feed, group=None): - ctx.parent.obj.execute_callback() - ecode = 0 try: + anchorecli.cli.utils.handle_parent_callback(ctx) + if group: ret = anchorecli.clients.apiexternal.system_feed_group_delete( config, feed, group diff --git a/anchorecli/cli/utils.py b/anchorecli/cli/utils.py index 42b6b20..b2cd0d3 100644 --- a/anchorecli/cli/utils.py +++ b/anchorecli/cli/utils.py @@ -1739,4 +1739,11 @@ class ContextObject: def __init__(self, config, execute_callback): self.config = config - self.execute_callback = execute_callback \ No newline at end of file + self.execute_callback = execute_callback + +def handle_parent_callback(ctx): + if (ctx.parent and ctx.parent.obj and ctx.parent.obj.execute_callback): + ctx.parent.obj.execute_callback() + else: + # This error should never be raised, and would likely be indicative of a regression. + raise RuntimeError("Unable to execute parent callback. This is an unexpected logical error.") \ No newline at end of file From ed14c0c87f7dc9711096c02ea0a16131bf635f57 Mon Sep 17 00:00:00 2001 From: Daniel Palmer Date: Wed, 16 Dec 2020 17:46:55 -0500 Subject: [PATCH 06/19] Fix test after rebase changed a function name. Signed-off-by: Daniel Palmer --- tests/unit/cli/test_subscription.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/cli/test_subscription.py b/tests/unit/cli/test_subscription.py index a3a184d..4b3caf1 100644 --- a/tests/unit/cli/test_subscription.py +++ b/tests/unit/cli/test_subscription.py @@ -9,7 +9,7 @@ class TestSubscriptionSubcommandHelp: [ (subscription.activate, "Usage: activate"), (subscription.deactivate, "Usage: deactivate"), - (subscription.subscriptionlist, "Usage: list"), + (subscription.list_subscriptions, "Usage: list"), ] ) def test_subscription_subcommand_help(self, subcommand, output_start): From aa633af126a83804b881ebb6f6fdaaa19853cc95 Mon Sep 17 00:00:00 2001 From: Daniel Palmer Date: Wed, 16 Dec 2020 18:01:22 -0500 Subject: [PATCH 07/19] Linting changes. Signed-off-by: Daniel Palmer --- anchorecli/cli/account.py | 8 ++++---- anchorecli/cli/archives.py | 8 ++++++-- anchorecli/cli/policy.py | 16 ++++++++-------- anchorecli/cli/query.py | 1 + anchorecli/cli/subscription.py | 6 +++++- anchorecli/cli/system.py | 8 ++++++-- anchorecli/cli/utils.py | 7 +++++-- tests/unit/cli/test_account.py | 2 +- tests/unit/cli/test_archives.py | 2 +- tests/unit/cli/test_evaluate.py | 2 +- tests/unit/cli/test_event.py | 2 +- tests/unit/cli/test_image.py | 2 +- tests/unit/cli/test_policy.py | 2 +- tests/unit/cli/test_query.py | 2 +- tests/unit/cli/test_registry.py | 2 +- tests/unit/cli/test_repo.py | 2 +- tests/unit/cli/test_subscription.py | 2 +- tests/unit/cli/test_system.py | 2 +- 18 files changed, 46 insertions(+), 30 deletions(-) diff --git a/anchorecli/cli/account.py b/anchorecli/cli/account.py index 5294335..db99fd2 100644 --- a/anchorecli/cli/account.py +++ b/anchorecli/cli/account.py @@ -55,9 +55,7 @@ def get_current_user(ctx): anchorecli.cli.utils.handle_parent_callback(ctx) except RuntimeError as err: print( - anchorecli.cli.utils.format_error_output( - config, "get_current_user", {}, err - ) + anchorecli.cli.utils.format_error_output(config, "get_current_user", {}, err) ) ecode = 2 anchorecli.cli.utils.doexit(ecode) @@ -319,7 +317,9 @@ def user(ctx): global config, whoami # since there's nothing to execute here, just pass the parent config and callback down - ctx.obj = anchorecli.cli.utils.ContextObject(ctx.parent.obj.config, ctx.parent.obj.execute_callback) + ctx.obj = anchorecli.cli.utils.ContextObject( + ctx.parent.obj.config, ctx.parent.obj.execute_callback + ) @user.command(name="add", short_help="Add a new user") diff --git a/anchorecli/cli/archives.py b/anchorecli/cli/archives.py index 94a2236..0cff648 100644 --- a/anchorecli/cli/archives.py +++ b/anchorecli/cli/archives.py @@ -24,7 +24,9 @@ def execute(): print(anchorecli.cli.utils.format_error_output(config, "image", {}, err)) sys.exit(2) - ctx.obj = anchorecli.cli.utils.ContextObject(ctx.parent.obj.config, execute) + ctx.obj = anchorecli.cli.utils.ContextObject( + ctx.parent.obj.config, execute + ) @archive.group(name="images", short_help="Archive operations") @@ -238,7 +240,9 @@ def image_delete(ctx, digest, force): @click.pass_context def rules(ctx): # since there's nothing to execute here, just pass the parent config and callback down - ctx.obj = anchorecli.cli.utils.ContextObject(ctx.parent.obj.config, ctx.parent.obj.execute_callback) + ctx.obj = anchorecli.cli.utils.ContextObject( + ctx.parent.obj.config, ctx.parent.obj.execute_callback + ) @rules.command(name="add", short_help="Add a new transition rule") diff --git a/anchorecli/cli/policy.py b/anchorecli/cli/policy.py index 8f45499..b149d22 100644 --- a/anchorecli/cli/policy.py +++ b/anchorecli/cli/policy.py @@ -19,7 +19,9 @@ def execute(): try: anchorecli.cli.utils.check_access(config) except Exception as err: - print(anchorecli.cli.utils.format_error_output(config, "policy", {}, err)) + print( + anchorecli.cli.utils.format_error_output(config, "policy", {}, err) + ) sys.exit(2) ctx.obj = anchorecli.cli.utils.ContextObject(ctx.parent.obj.config, execute) @@ -281,9 +283,7 @@ def execute(): anchorecli.cli.utils.handle_parent_callback(ctx) except RuntimeError as err: print( - anchorecli.cli.utils.format_error_output( - config, "policy_hub", {}, err - ) + anchorecli.cli.utils.format_error_output(config, "policy_hub", {}, err) ) ecode = 2 anchorecli.cli.utils.doexit(ecode) @@ -292,7 +292,9 @@ def execute(): try: anchorecli.cli.utils.check_access(config) except Exception as err: - print(anchorecli.cli.utils.format_error_output(config, "policy", {}, err)) + print( + anchorecli.cli.utils.format_error_output(config, "policy", {}, err) + ) sys.exit(2) ctx.obj = anchorecli.cli.utils.ContextObject(ctx.parent.obj.config, execute) @@ -384,9 +386,7 @@ def hubinstall(ctx, bundlename, target_id, force): except Exception as err: print( - anchorecli.cli.utils.format_error_output( - config, "policy_hub_install", {}, err - ) + anchorecli.cli.utils.format_error_output(config, "policy_hub_install", {}, err) ) if not ecode: ecode = 2 diff --git a/anchorecli/cli/query.py b/anchorecli/cli/query.py index 3146465..2a9dcc9 100644 --- a/anchorecli/cli/query.py +++ b/anchorecli/cli/query.py @@ -23,6 +23,7 @@ def execute(): ctx.obj = anchorecli.cli.utils.ContextObject(ctx.parent.obj.config, execute) + @query.command( name="images-by-vulnerability", short_help="Search system for images with the given vulnerability ID present", diff --git a/anchorecli/cli/subscription.py b/anchorecli/cli/subscription.py index 29d76b4..6b2f3a8 100644 --- a/anchorecli/cli/subscription.py +++ b/anchorecli/cli/subscription.py @@ -17,7 +17,11 @@ def execute(): try: anchorecli.cli.utils.check_access(config) except Exception as err: - print(anchorecli.cli.utils.format_error_output(config, "subscription", {}, err)) + print( + anchorecli.cli.utils.format_error_output( + config, "subscription", {}, err + ) + ) sys.exit(2) ctx.obj = anchorecli.cli.utils.ContextObject(ctx.parent.obj.config, execute) diff --git a/anchorecli/cli/system.py b/anchorecli/cli/system.py index 93d8026..8dcbdf0 100644 --- a/anchorecli/cli/system.py +++ b/anchorecli/cli/system.py @@ -26,7 +26,9 @@ def execute(): try: anchorecli.cli.utils.check_access(config) except Exception as err: - print(anchorecli.cli.utils.format_error_output(config, "system", {}, err)) + print( + anchorecli.cli.utils.format_error_output(config, "system", {}, err) + ) sys.exit(2) ctx.obj = anchorecli.cli.utils.ContextObject(ctx.parent.obj.config, execute) @@ -336,7 +338,9 @@ def delete(ctx, host_id, servicename): @click.pass_context def feeds(ctx): # since there's nothing to execute here, just pass the parent config and callback down - ctx.obj = anchorecli.cli.utils.ContextObject(ctx.parent.obj.config, ctx.parent.obj.execute_callback) + ctx.obj = anchorecli.cli.utils.ContextObject( + ctx.parent.obj.config, ctx.parent.obj.execute_callback + ) @feeds.command(name="list", short_help="Get a list of loaded data feeds.") diff --git a/anchorecli/cli/utils.py b/anchorecli/cli/utils.py index b2cd0d3..26e2598 100644 --- a/anchorecli/cli/utils.py +++ b/anchorecli/cli/utils.py @@ -1741,9 +1741,12 @@ def __init__(self, config, execute_callback): self.config = config self.execute_callback = execute_callback + def handle_parent_callback(ctx): - if (ctx.parent and ctx.parent.obj and ctx.parent.obj.execute_callback): + if ctx.parent and ctx.parent.obj and ctx.parent.obj.execute_callback: ctx.parent.obj.execute_callback() else: # This error should never be raised, and would likely be indicative of a regression. - raise RuntimeError("Unable to execute parent callback. This is an unexpected logical error.") \ No newline at end of file + raise RuntimeError( + "Unable to execute parent callback. This is an unexpected logical error." + ) \ No newline at end of file diff --git a/tests/unit/cli/test_account.py b/tests/unit/cli/test_account.py index ff57e73..00448f8 100644 --- a/tests/unit/cli/test_account.py +++ b/tests/unit/cli/test_account.py @@ -20,7 +20,7 @@ class TestAccountSubcommandHelp: (account.user_get, "Usage: get"), (account.user_list, "Usage: list"), (account.user_setpassword, "Usage: setpassword"), - ] + ], ) def test_event_subcommand_help(self, subcommand, output_start): runner = CliRunner() diff --git a/tests/unit/cli/test_archives.py b/tests/unit/cli/test_archives.py index 25e1d20..9fcfb57 100644 --- a/tests/unit/cli/test_archives.py +++ b/tests/unit/cli/test_archives.py @@ -18,7 +18,7 @@ class TestArchiveSubcommandHelp: (archives.rule_get, "Usage: get"), (archives.list_transition_rules, "Usage: list"), (archives.rule_delete, "Usage: del"), - ] + ], ) def test_event_subcommand_help(self, subcommand, output_start): runner = CliRunner() diff --git a/tests/unit/cli/test_evaluate.py b/tests/unit/cli/test_evaluate.py index 40edc6e..959e05b 100644 --- a/tests/unit/cli/test_evaluate.py +++ b/tests/unit/cli/test_evaluate.py @@ -8,7 +8,7 @@ class TestEvaluateSubcommandHelp: "subcommand, output_start", [ (evaluate.check, "Usage: check"), - ] + ], ) def test_event_subcommand_help(self, subcommand, output_start): runner = CliRunner() diff --git a/tests/unit/cli/test_event.py b/tests/unit/cli/test_event.py index 2cf764e..976cd3b 100644 --- a/tests/unit/cli/test_event.py +++ b/tests/unit/cli/test_event.py @@ -52,7 +52,7 @@ class TestEventSubcommandHelp: (event.list, "Usage: list"), (event.get, "Usage: get"), (event.delete, "Usage: delete"), - ] + ], ) def test_event_subcommand_help(self, subcommand, output_start): runner = CliRunner() diff --git a/tests/unit/cli/test_image.py b/tests/unit/cli/test_image.py index a2e1fb9..e74adb6 100644 --- a/tests/unit/cli/test_image.py +++ b/tests/unit/cli/test_image.py @@ -263,7 +263,7 @@ class TestImageSubcommandHelp: (image.query_metadata, "Usage: metadata"), (image.query_vuln, "Usage: vuln"), (image.delete, "Usage: del"), - ] + ], ) def test_image_subcommand_help(self, subcommand, output_start): runner = CliRunner() diff --git a/tests/unit/cli/test_policy.py b/tests/unit/cli/test_policy.py index a798596..e57af97 100644 --- a/tests/unit/cli/test_policy.py +++ b/tests/unit/cli/test_policy.py @@ -17,7 +17,7 @@ class TestPolicySubcommandHelp: (policy.hublist, "Usage: list"), (policy.hubget, "Usage: get"), (policy.hubinstall, "Usage: install"), - ] + ], ) def test_policy_subcommand_help(self, subcommand, output_start): runner = CliRunner() diff --git a/tests/unit/cli/test_query.py b/tests/unit/cli/test_query.py index c84b2ff..6e00996 100644 --- a/tests/unit/cli/test_query.py +++ b/tests/unit/cli/test_query.py @@ -9,7 +9,7 @@ class TestQuerySubcommandHelp: [ (query.images_by_vulnerability, "Usage: images-by-vulnerability"), (query.images_by_package, "Usage: images-by-package"), - ] + ], ) def test_query_subcommand_help(self, subcommand, output_start): runner = CliRunner() diff --git a/tests/unit/cli/test_registry.py b/tests/unit/cli/test_registry.py index f762940..cf105c1 100644 --- a/tests/unit/cli/test_registry.py +++ b/tests/unit/cli/test_registry.py @@ -12,7 +12,7 @@ class TestRegistrySubcommandHelp: (registry.delete, "Usage: del"), (registry.registrylist, "Usage: list"), (registry.get, "Usage: get"), - ] + ], ) def test_registry_subcommand_help(self, subcommand, output_start): runner = CliRunner() diff --git a/tests/unit/cli/test_repo.py b/tests/unit/cli/test_repo.py index 20c74e4..d7e83e0 100644 --- a/tests/unit/cli/test_repo.py +++ b/tests/unit/cli/test_repo.py @@ -13,7 +13,7 @@ class TestRepoSubcommandHelp: (repo.delete, "Usage: del"), (repo.unwatch, "Usage: unwatch"), (repo.watch, "Usage: watch"), - ] + ], ) def test_repo_subcommand_help(self, subcommand, output_start): runner = CliRunner() diff --git a/tests/unit/cli/test_subscription.py b/tests/unit/cli/test_subscription.py index 4b3caf1..adc5318 100644 --- a/tests/unit/cli/test_subscription.py +++ b/tests/unit/cli/test_subscription.py @@ -10,7 +10,7 @@ class TestSubscriptionSubcommandHelp: (subscription.activate, "Usage: activate"), (subscription.deactivate, "Usage: deactivate"), (subscription.list_subscriptions, "Usage: list"), - ] + ], ) def test_subscription_subcommand_help(self, subcommand, output_start): runner = CliRunner() diff --git a/tests/unit/cli/test_system.py b/tests/unit/cli/test_system.py index 9470e94..5c94b64 100644 --- a/tests/unit/cli/test_system.py +++ b/tests/unit/cli/test_system.py @@ -139,7 +139,7 @@ class TestSystemSubcommandHelp: (system.feedsync, "Usage: sync"), (system.toggle_enabled, "Usage: config"), (system.delete_data, "Usage: delete"), - ] + ], ) def test_event_subcommand_help(self, subcommand, output_start): runner = CliRunner() From 56c4396a7d789836523ac2d553b3f7b4aace3fcb Mon Sep 17 00:00:00 2001 From: Daniel Palmer Date: Wed, 16 Dec 2020 18:13:31 -0500 Subject: [PATCH 08/19] More linting. Signed-off-by: Daniel Palmer --- anchorecli/cli/account.py | 8 ++++---- anchorecli/cli/archives.py | 8 ++++---- anchorecli/cli/policy.py | 4 +++- tests/unit/cli/test_image.py | 34 ++++++++++++++++++++++++++-------- 4 files changed, 37 insertions(+), 17 deletions(-) diff --git a/anchorecli/cli/account.py b/anchorecli/cli/account.py index db99fd2..c9099c1 100644 --- a/anchorecli/cli/account.py +++ b/anchorecli/cli/account.py @@ -55,7 +55,9 @@ def get_current_user(ctx): anchorecli.cli.utils.handle_parent_callback(ctx) except RuntimeError as err: print( - anchorecli.cli.utils.format_error_output(config, "get_current_user", {}, err) + anchorecli.cli.utils.format_error_output( + config, "get_current_user", {}, err + ) ) ecode = 2 anchorecli.cli.utils.doexit(ecode) @@ -185,9 +187,7 @@ def delete(ctx, account_name, dontask): anchorecli.cli.utils.handle_parent_callback(ctx) except RuntimeError as err: print( - anchorecli.cli.utils.format_error_output( - config, "account_delete", {}, err - ) + anchorecli.cli.utils.format_error_output(config, "account_delete", {}, err) ) ecode = 2 anchorecli.cli.utils.doexit(ecode) diff --git a/anchorecli/cli/archives.py b/anchorecli/cli/archives.py index 0cff648..5dc9046 100644 --- a/anchorecli/cli/archives.py +++ b/anchorecli/cli/archives.py @@ -24,16 +24,16 @@ def execute(): print(anchorecli.cli.utils.format_error_output(config, "image", {}, err)) sys.exit(2) - ctx.obj = anchorecli.cli.utils.ContextObject( - ctx.parent.obj.config, execute - ) + ctx.obj = anchorecli.cli.utils.ContextObject(ctx.parent.obj.config, execute) @archive.group(name="images", short_help="Archive operations") @click.pass_context def images(ctx): # since there's nothing to execute here, just pass the parent config and callback down - ctx.obj = anchorecli.cli.utils.ContextObject(ctx.parent.obj.config, ctx.parent.obj.execute_callback) + ctx.obj = anchorecli.cli.utils.ContextObject( + ctx.parent.obj.config, ctx.parent.obj.execute_callback + ) @images.command( diff --git a/anchorecli/cli/policy.py b/anchorecli/cli/policy.py index b149d22..79b2c66 100644 --- a/anchorecli/cli/policy.py +++ b/anchorecli/cli/policy.py @@ -386,7 +386,9 @@ def hubinstall(ctx, bundlename, target_id, force): except Exception as err: print( - anchorecli.cli.utils.format_error_output(config, "policy_hub_install", {}, err) + anchorecli.cli.utils.format_error_output( + config, "policy_hub_install", {}, err + ) ) if not ecode: ecode = 2 diff --git a/tests/unit/cli/test_image.py b/tests/unit/cli/test_image.py index e74adb6..bd06530 100644 --- a/tests/unit/cli/test_image.py +++ b/tests/unit/cli/test_image.py @@ -83,9 +83,11 @@ def apply(success=True, httpcode=200, has_error=None): "/usr/local/lib64/python3.6/site-packages/aubio", ] + def mock_empty_callback(): pass + def get_mock_parent_ctx(): # The command supplied here will not be used in a way tht impacts the tests, and can # be any arbitrary, non-root command @@ -105,7 +107,9 @@ def test_is_analyzing(self, monkeypatch, response): # image.query_vuln expects a context containing a parent context with a callback for it to run. # In production this is provided by the image group command, and should never be skipped. # Since that doesn't exist here, just mock it with a no-op callback - result = runner.invoke(image.query_vuln, ["centos/centos:8", "all"], parent=get_mock_parent_ctx()) + result = runner.invoke( + image.query_vuln, ["centos/centos:8", "all"], parent=get_mock_parent_ctx() + ) assert result.exit_code == 100 def test_not_yet_analyzed(self, monkeypatch, response): @@ -124,7 +128,9 @@ def test_not_yet_analyzed(self, monkeypatch, response): "message": "image is not analyzed - analysis_status: not_analyzed", }, ) - result = runner.invoke(image.query_vuln, ["centos/centos:8", "all"], parent=get_mock_parent_ctx()) + result = runner.invoke( + image.query_vuln, ["centos/centos:8", "all"], parent=get_mock_parent_ctx() + ) assert result.exit_code == 101 @pytest.mark.parametrize("item", headers) @@ -137,7 +143,9 @@ def test_success_headers(self, monkeypatch, response, item): monkeypatch.setattr(image, "config", {"jsonmode": False}) runner = CliRunner() response(success=True) - result = runner.invoke(image.query_vuln, ["centos/centos:8", "all"], parent=get_mock_parent_ctx()) + result = runner.invoke( + image.query_vuln, ["centos/centos:8", "all"], parent=get_mock_parent_ctx() + ) assert result.exit_code == 0 assert item in result.stdout @@ -151,7 +159,9 @@ def test_success_info(self, monkeypatch, response, item): monkeypatch.setattr(image, "config", {"jsonmode": False}) runner = CliRunner() response(success=True) - result = runner.invoke(image.query_vuln, ["centos/centos:8", "all"], parent=get_mock_parent_ctx()) + result = runner.invoke( + image.query_vuln, ["centos/centos:8", "all"], parent=get_mock_parent_ctx() + ) assert result.exit_code == 0 assert item in result.stdout @@ -176,7 +186,9 @@ def test_deleted_pre_v080(self, monkeypatch, response): ) runner = CliRunner() response(success=True) - result = runner.invoke(image.delete, ["centos/centos:8"], parent=get_mock_parent_ctx()) + result = runner.invoke( + image.delete, ["centos/centos:8"], parent=get_mock_parent_ctx() + ) assert result.exit_code == 0 def test_delete_failed_pre_v080(self, monkeypatch, response): @@ -198,7 +210,9 @@ def test_delete_failed_pre_v080(self, monkeypatch, response): ) runner = CliRunner() response(success=True) - result = runner.invoke(image.delete, ["centos/centos:8"], parent=get_mock_parent_ctx()) + result = runner.invoke( + image.delete, ["centos/centos:8"], parent=get_mock_parent_ctx() + ) assert result.exit_code == 1 def test_is_deleting(self, monkeypatch, response): @@ -220,7 +234,9 @@ def test_is_deleting(self, monkeypatch, response): ) runner = CliRunner() response(success=True) - result = runner.invoke(image.delete, ["centos/centos:8"], parent=get_mock_parent_ctx()) + result = runner.invoke( + image.delete, ["centos/centos:8"], parent=get_mock_parent_ctx() + ) assert result.exit_code == 0 def test_delete_failed(self, monkeypatch, response): @@ -246,7 +262,9 @@ def test_delete_failed(self, monkeypatch, response): ) runner = CliRunner() response(success=True) - result = runner.invoke(image.delete, ["centos/centos:8"], parent=get_mock_parent_ctx()) + result = runner.invoke( + image.delete, ["centos/centos:8"], parent=get_mock_parent_ctx() + ) assert result.exit_code == 1 From 6309077f666a213b5e5c3f0f3d1ca459aaffd790 Mon Sep 17 00:00:00 2001 From: Daniel Palmer Date: Wed, 16 Dec 2020 18:22:16 -0500 Subject: [PATCH 09/19] A bit more linting. Signed-off-by: Daniel Palmer --- anchorecli/cli/utils.py | 2 +- tests/unit/cli/test_image.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/anchorecli/cli/utils.py b/anchorecli/cli/utils.py index 26e2598..feeb5c9 100644 --- a/anchorecli/cli/utils.py +++ b/anchorecli/cli/utils.py @@ -1749,4 +1749,4 @@ def handle_parent_callback(ctx): # This error should never be raised, and would likely be indicative of a regression. raise RuntimeError( "Unable to execute parent callback. This is an unexpected logical error." - ) \ No newline at end of file + ) diff --git a/tests/unit/cli/test_image.py b/tests/unit/cli/test_image.py index bd06530..95ce23c 100644 --- a/tests/unit/cli/test_image.py +++ b/tests/unit/cli/test_image.py @@ -287,4 +287,4 @@ def test_image_subcommand_help(self, subcommand, output_start): runner = CliRunner() result = runner.invoke(subcommand, ["--help"]) assert result.exit_code == 0 - assert result.output.startswith(output_start) \ No newline at end of file + assert result.output.startswith(output_start) From 8708d6cb57e3b0f984385f07463e6aee84df9ae6 Mon Sep 17 00:00:00 2001 From: Daniel Palmer Date: Mon, 21 Dec 2020 09:09:49 -0500 Subject: [PATCH 10/19] Fix linting bug. Signed-off-by: Daniel Palmer --- anchorecli/cli/repo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/anchorecli/cli/repo.py b/anchorecli/cli/repo.py index c4aa59f..b9b32fa 100644 --- a/anchorecli/cli/repo.py +++ b/anchorecli/cli/repo.py @@ -58,7 +58,7 @@ def add(ctx, input_repo, noautosubscribe, lookuptag, dryrun): ret = anchorecli.clients.apiexternal.add_repo( config, input_repo, - auto_subscribe=auto_subscribe, + auto_subscribe=autosubscribe, lookup_tag=lookuptag, dry_run=dryrun, ) From 0274d75b17533a0b116d309b1db44c3fe336d008 Mon Sep 17 00:00:00 2001 From: Daniel Palmer Date: Mon, 21 Dec 2020 09:31:13 -0500 Subject: [PATCH 11/19] Update functional test. Signed-off-by: Daniel Palmer --- tests/functional/test_account.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/functional/test_account.py b/tests/functional/test_account.py index 2bbf6e3..ac90981 100644 --- a/tests/functional/test_account.py +++ b/tests/functional/test_account.py @@ -2,14 +2,14 @@ import json import pytest - +1 @pytest.mark.parametrize( "sub_command", ["add", "del", "disable", "enable", "get", "list", "user", "whoami"] ) def test_unauthorized(sub_command): out, err, code = call(["anchore-cli", "account", sub_command]) - assert code == ExitCode(2) - assert out == '"Unauthorized"\n' + assert code == ExitCode(0) + assert out.startswith("Usage: anchore-cli account {}".format(sub_command)) class TesttList: From 1776c4fc12532d93cf66ce1a1746a61872f98727 Mon Sep 17 00:00:00 2001 From: Daniel Palmer Date: Mon, 21 Dec 2020 09:42:33 -0500 Subject: [PATCH 12/19] Fix typo on last commit. Signed-off-by: Daniel Palmer --- tests/functional/test_account.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/functional/test_account.py b/tests/functional/test_account.py index ac90981..10c4c86 100644 --- a/tests/functional/test_account.py +++ b/tests/functional/test_account.py @@ -2,7 +2,6 @@ import json import pytest -1 @pytest.mark.parametrize( "sub_command", ["add", "del", "disable", "enable", "get", "list", "user", "whoami"] ) From aa7bf2bbce6c4391fecf7667c2e0f2f1a41d9246 Mon Sep 17 00:00:00 2001 From: Daniel Palmer Date: Mon, 21 Dec 2020 14:33:14 -0500 Subject: [PATCH 13/19] Fix functional test. Signed-off-by: Daniel Palmer --- tests/functional/test_account.py | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/tests/functional/test_account.py b/tests/functional/test_account.py index 10c4c86..3f5fa41 100644 --- a/tests/functional/test_account.py +++ b/tests/functional/test_account.py @@ -3,13 +3,30 @@ import pytest @pytest.mark.parametrize( - "sub_command", ["add", "del", "disable", "enable", "get", "list", "user", "whoami"] + "sub_command, expected_code", + [ + ("add", 2), + ("del", 2), + ("disable", 2), + ("enable", 2), + ("get", 2), + ("list", 0), + ("user", 0), + ("whoami", 0), + ], ) -def test_unauthorized(sub_command): +def test_unauthorized(sub_command, expected_code): out, err, code = call(["anchore-cli", "account", sub_command]) - assert code == ExitCode(0) - assert out.startswith("Usage: anchore-cli account {}".format(sub_command)) - + assert code == ExitCode(expected_code) + if expected_code == 2: + assert err.startswith("Usage: anchore-cli account {}".format(sub_command)) + else: + if sub_command == "list": + assert out.startswith("Name") + elif sub_command == "whoami": + assert out.startswith("Username: admin") + else: + assert out.startswith("Usage: anchore-cli account {}".format(sub_command)) class TesttList: def test_is_authorized(self, admin_call): From 424345278c616925e0fc0c88a6f5cef5be6cb5eb Mon Sep 17 00:00:00 2001 From: Daniel Palmer Date: Mon, 21 Dec 2020 14:47:24 -0500 Subject: [PATCH 14/19] More linting. Signed-off-by: Daniel Palmer --- tests/functional/test_account.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/functional/test_account.py b/tests/functional/test_account.py index 3f5fa41..92f5a6b 100644 --- a/tests/functional/test_account.py +++ b/tests/functional/test_account.py @@ -2,6 +2,7 @@ import json import pytest + @pytest.mark.parametrize( "sub_command, expected_code", [ @@ -28,6 +29,7 @@ def test_unauthorized(sub_command, expected_code): else: assert out.startswith("Usage: anchore-cli account {}".format(sub_command)) + class TesttList: def test_is_authorized(self, admin_call): out, err, code = admin_call(["account", "whoami"]) From af7fa5ac5413e28735854c804b0bfd46250d1dc8 Mon Sep 17 00:00:00 2001 From: Daniel Palmer Date: Mon, 21 Dec 2020 14:48:41 -0500 Subject: [PATCH 15/19] Test fix. Signed-off-by: Daniel Palmer --- tests/functional/test_account.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/functional/test_account.py b/tests/functional/test_account.py index 92f5a6b..48294f2 100644 --- a/tests/functional/test_account.py +++ b/tests/functional/test_account.py @@ -13,7 +13,7 @@ ("get", 2), ("list", 0), ("user", 0), - ("whoami", 0), + ("whoami", 2), ], ) def test_unauthorized(sub_command, expected_code): @@ -25,7 +25,7 @@ def test_unauthorized(sub_command, expected_code): if sub_command == "list": assert out.startswith("Name") elif sub_command == "whoami": - assert out.startswith("Username: admin") + assert out.startswith("Unauthorized") else: assert out.startswith("Usage: anchore-cli account {}".format(sub_command)) From 80cacf8b55a13f218add1591f0119a05a3e6a1e5 Mon Sep 17 00:00:00 2001 From: Daniel Palmer Date: Mon, 21 Dec 2020 15:02:45 -0500 Subject: [PATCH 16/19] Temporarily debug test values in ci. Signed-off-by: Daniel Palmer --- tests/functional/test_account.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/tests/functional/test_account.py b/tests/functional/test_account.py index 48294f2..9f34091 100644 --- a/tests/functional/test_account.py +++ b/tests/functional/test_account.py @@ -1,7 +1,10 @@ from conftest import call, ExitCode import json +import logging import pytest +def get_logger(name): + return logging.getLogger("conftest.%s" % name) @pytest.mark.parametrize( "sub_command, expected_code", @@ -13,11 +16,22 @@ ("get", 2), ("list", 0), ("user", 0), - ("whoami", 2), + ("whoami", 0), ], ) def test_unauthorized(sub_command, expected_code): + logger = get_logger("test_unauthorized") + out, err, code = call(["anchore-cli", "account", sub_command]) + + if sub_command in ["list", "use", "whoami"]: + logger.warning("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!") + logger.warning("! sub_command: " + str(sub_command)) + logger.warning("! out: " + str(out)) + logger.warning("! err: " + str(err)) + logger.warning("! code: " + str(code)) + logger.warning("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!") + assert code == ExitCode(expected_code) if expected_code == 2: assert err.startswith("Usage: anchore-cli account {}".format(sub_command)) @@ -25,11 +39,10 @@ def test_unauthorized(sub_command, expected_code): if sub_command == "list": assert out.startswith("Name") elif sub_command == "whoami": - assert out.startswith("Unauthorized") + assert out.startswith("Username: admin") else: assert out.startswith("Usage: anchore-cli account {}".format(sub_command)) - class TesttList: def test_is_authorized(self, admin_call): out, err, code = admin_call(["account", "whoami"]) From 34b481068a0b7a3067a8a490b846f05d4496af1d Mon Sep 17 00:00:00 2001 From: Daniel Palmer Date: Mon, 21 Dec 2020 15:16:10 -0500 Subject: [PATCH 17/19] More test debugging. Signed-off-by: Daniel Palmer --- tests/functional/test_account.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/tests/functional/test_account.py b/tests/functional/test_account.py index 9f34091..52f6832 100644 --- a/tests/functional/test_account.py +++ b/tests/functional/test_account.py @@ -3,9 +3,11 @@ import logging import pytest + def get_logger(name): return logging.getLogger("conftest.%s" % name) + @pytest.mark.parametrize( "sub_command, expected_code", [ @@ -24,7 +26,7 @@ def test_unauthorized(sub_command, expected_code): out, err, code = call(["anchore-cli", "account", sub_command]) - if sub_command in ["list", "use", "whoami"]: + if sub_command in ["list", "user", "whoami"]: logger.warning("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!") logger.warning("! sub_command: " + str(sub_command)) logger.warning("! out: " + str(out)) @@ -37,12 +39,13 @@ def test_unauthorized(sub_command, expected_code): assert err.startswith("Usage: anchore-cli account {}".format(sub_command)) else: if sub_command == "list": - assert out.startswith("Name") + assert out.startswith("Unauthorized") elif sub_command == "whoami": - assert out.startswith("Username: admin") + assert out.startswith("Unauthorized") else: assert out.startswith("Usage: anchore-cli account {}".format(sub_command)) + class TesttList: def test_is_authorized(self, admin_call): out, err, code = admin_call(["account", "whoami"]) From 19da0bcb8d3b458f97f6bf96bd47a9084baa90e6 Mon Sep 17 00:00:00 2001 From: Daniel Palmer Date: Mon, 21 Dec 2020 15:45:33 -0500 Subject: [PATCH 18/19] Remove debug logging and fix test. Signed-off-by: Daniel Palmer --- tests/functional/test_account.py | 25 +------------------------ 1 file changed, 1 insertion(+), 24 deletions(-) diff --git a/tests/functional/test_account.py b/tests/functional/test_account.py index 52f6832..5474489 100644 --- a/tests/functional/test_account.py +++ b/tests/functional/test_account.py @@ -1,13 +1,8 @@ from conftest import call, ExitCode import json -import logging import pytest -def get_logger(name): - return logging.getLogger("conftest.%s" % name) - - @pytest.mark.parametrize( "sub_command, expected_code", [ @@ -22,28 +17,10 @@ def get_logger(name): ], ) def test_unauthorized(sub_command, expected_code): - logger = get_logger("test_unauthorized") - out, err, code = call(["anchore-cli", "account", sub_command]) - - if sub_command in ["list", "user", "whoami"]: - logger.warning("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!") - logger.warning("! sub_command: " + str(sub_command)) - logger.warning("! out: " + str(out)) - logger.warning("! err: " + str(err)) - logger.warning("! code: " + str(code)) - logger.warning("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!") - assert code == ExitCode(expected_code) - if expected_code == 2: + if sub_command in ["add", "del", "disable", "enable", "get"]: assert err.startswith("Usage: anchore-cli account {}".format(sub_command)) - else: - if sub_command == "list": - assert out.startswith("Unauthorized") - elif sub_command == "whoami": - assert out.startswith("Unauthorized") - else: - assert out.startswith("Usage: anchore-cli account {}".format(sub_command)) class TesttList: From eeb58258c623c2c38fe497b9b6bc756418e5c75a Mon Sep 17 00:00:00 2001 From: Daniel Palmer Date: Mon, 21 Dec 2020 15:55:00 -0500 Subject: [PATCH 19/19] Fix expected exit codes. Signed-off-by: Daniel Palmer --- tests/functional/test_account.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/functional/test_account.py b/tests/functional/test_account.py index 5474489..2f93987 100644 --- a/tests/functional/test_account.py +++ b/tests/functional/test_account.py @@ -11,9 +11,9 @@ ("disable", 2), ("enable", 2), ("get", 2), - ("list", 0), + ("list", 2), ("user", 0), - ("whoami", 0), + ("whoami", 2), ], ) def test_unauthorized(sub_command, expected_code):