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..c9099c1 100644 --- a/anchorecli/cli/account.py +++ b/anchorecli/cli/account.py @@ -10,42 +10,58 @@ @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 ecode = 0 + + 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) + print(anchorecli.cli.utils.format_output(config, "account_whoami", {}, whoami)) anchorecli.cli.utils.doexit(ecode) @@ -55,7 +71,8 @@ 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 @@ -65,6 +82,8 @@ def add(account_name, email): ecode = 0 try: + anchorecli.cli.utils.handle_parent_callback(ctx) + ret = anchorecli.clients.apiexternal.add_account( config, account_name=account_name, email=email ) @@ -88,7 +107,8 @@ 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 @@ -96,6 +116,8 @@ def get(account_name): ecode = 0 try: + anchorecli.cli.utils.handle_parent_callback(ctx) + ret = anchorecli.clients.apiexternal.get_account( config, account_name=account_name ) @@ -120,10 +142,14 @@ 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): + """""" 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"]: @@ -148,7 +174,8 @@ 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) @@ -156,6 +183,15 @@ def delete(account_name, dontask): """ 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" @@ -202,7 +238,8 @@ 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 @@ -210,6 +247,8 @@ def enable(account_name): ecode = 0 try: + anchorecli.cli.utils.handle_parent_callback(ctx) + ret = anchorecli.clients.apiexternal.enable_account( config, account_name=account_name ) @@ -235,7 +274,8 @@ 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 @@ -243,6 +283,8 @@ def disable(account_name): ecode = 0 try: + anchorecli.cli.utils.handle_parent_callback(ctx) + ret = anchorecli.clients.apiexternal.disable_account( config, account_name=account_name ) @@ -270,27 +312,35 @@ 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 """ - - 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( @@ -324,19 +374,21 @@ 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 """ - - 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 ) @@ -361,19 +413,21 @@ 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 """ - - 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 ) @@ -397,19 +451,21 @@ 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 """ - - 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"]: @@ -433,21 +489,23 @@ 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 """ - - 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 88a56f8..5dc9046 100644 --- a/anchorecli/cli/archives.py +++ b/anchorecli/cli/archives.py @@ -12,35 +12,44 @@ @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 """ 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( @@ -74,13 +83,16 @@ 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 """ ecode = 0 try: + anchorecli.cli.utils.handle_parent_callback(ctx) + for digest in image_digests: if not re.match(digest_regex, digest): raise Exception( @@ -114,13 +126,16 @@ 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) """ ecode = 0 try: + anchorecli.cli.utils.handle_parent_callback(ctx) + ret = anchorecli.clients.apiexternal.get_archived_analysis(config, digest) if ret: @@ -153,10 +168,13 @@ 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): 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"]: @@ -183,13 +201,16 @@ 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) """ ecode = 0 try: + anchorecli.cli.utils.handle_parent_callback(ctx) + ret = anchorecli.clients.apiexternal.delete_archived_analysis(config, digest) if ret: @@ -216,8 +237,12 @@ 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 +284,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, @@ -321,6 +348,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, @@ -368,10 +407,13 @@ 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): ecode = 0 try: + anchorecli.cli.utils.handle_parent_callback(ctx) + ret = anchorecli.clients.apiexternal.get_transition_rule(config, rule_id) if ret: @@ -400,10 +442,13 @@ 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): 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"]: @@ -429,10 +474,13 @@ 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): 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 a20bb2f..8c62599 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,13 +39,16 @@ 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 """ 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 c7f4aa4..617bf0d 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, @@ -97,6 +102,8 @@ def list( ecode = 0 try: + anchorecli.cli.utils.handle_parent_callback(ctx) + if level: if level.lower() not in ["info", "error"]: raise Exception( @@ -145,13 +152,16 @@ 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 """ 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"]: @@ -191,7 +201,8 @@ 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 @@ -201,6 +212,8 @@ def delete(since=None, before=None, dontask=False, event_id=None, all=False): 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 7842d0f..0482703 100644 --- a/anchorecli/cli/image.py +++ b/anchorecli/cli/image.py @@ -13,16 +13,19 @@ @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): + 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) @image.command(short_help="Wait for an image to analyze") @@ -39,7 +42,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 @@ -50,6 +54,8 @@ def wait(input_image, timeout, interval): 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) @@ -135,13 +141,16 @@ 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 """ ecode = 0 try: + anchorecli.cli.utils.handle_parent_callback(ctx) + itype = anchorecli.cli.utils.discover_inputimage_format(config, input_image) dockerfile_contents = None @@ -202,10 +211,13 @@ 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): ecode = 0 try: + anchorecli.cli.utils.handle_parent_callback(ctx) + with open(infile, "r") as FH: anchore_data = json.loads(FH.read()) @@ -237,13 +249,16 @@ 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 """ ecode = 0 try: + anchorecli.cli.utils.handle_parent_callback(ctx) + itype = anchorecli.cli.utils.discover_inputimage_format(config, input_image) image = input_image @@ -292,10 +307,13 @@ 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): 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"]: @@ -321,7 +339,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 @@ -332,6 +351,8 @@ def query_content(input_image, content_type): ecode = 0 try: + anchorecli.cli.utils.handle_parent_callback(ctx) + itype, image, imageDigest = anchorecli.cli.utils.discover_inputimage( config, input_image ) @@ -385,7 +406,8 @@ 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 @@ -395,6 +417,8 @@ def query_metadata(input_image, metadata_type): ecode = 0 try: + anchorecli.cli.utils.handle_parent_callback(ctx) + itype, image, imageDigest = anchorecli.cli.utils.discover_inputimage( config, input_image ) @@ -450,7 +474,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 @@ -460,6 +485,8 @@ def query_vuln(input_image, vuln_type, vendor_only): """ ecode = 0 try: + anchorecli.cli.utils.handle_parent_callback(ctx) + itype, image, imageDigest = anchorecli.cli.utils.discover_inputimage( config, input_image ) @@ -510,7 +537,8 @@ 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 """ @@ -518,6 +546,8 @@ def delete(input_image, force, all): 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"]: @@ -558,6 +588,8 @@ def delete(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 c3fc078..79b2c66 100644 --- a/anchorecli/cli/policy.py +++ b/anchorecli/cli/policy.py @@ -10,17 +10,21 @@ @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,10 +34,13 @@ def policy(ctx_config, ctx): type=click.Path(exists=True), metavar="", ) -def add(input_policy): +@click.pass_context +def add(ctx, input_policy): ecode = 0 try: + anchorecli.cli.utils.handle_parent_callback(ctx) + with open(input_policy, "r") as FH: policybundle = json.loads(FH.read()) @@ -61,13 +68,16 @@ 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 """ ecode = 0 try: + anchorecli.cli.utils.handle_parent_callback(ctx) + ret = anchorecli.clients.apiexternal.get_policy( config, policyId=policyid, detail=detail ) @@ -90,10 +100,13 @@ def get(policyid, detail): @policy.command(name="list", short_help="List all policies") -def policylist(): +@click.pass_context +def policylist(ctx): 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"]: @@ -115,13 +128,16 @@ 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 """ ecode = 0 try: + anchorecli.cli.utils.handle_parent_callback(ctx) + ret = anchorecli.clients.apiexternal.get_policy( config, policyId=policyid, detail=True ) @@ -164,13 +180,16 @@ 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 """ 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"]: @@ -206,9 +225,12 @@ 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): ecode = 0 try: + anchorecli.cli.utils.handle_parent_callback(ctx) + ret = anchorecli.clients.apiexternal.describe_policy_spec(config) if ret["success"]: @@ -256,19 +278,36 @@ 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"]: + def execute(): try: - anchorecli.cli.utils.check_access(config) - except Exception as err: - print(anchorecli.cli.utils.format_error_output(config, "policy", {}, err)) - sys.exit(2) + 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: + 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): ecode = 0 try: + anchorecli.cli.utils.handle_parent_callback(ctx) + ret = anchorecli.clients.hub.get_policies(config) if ret["success"]: print( @@ -291,10 +330,13 @@ def hublist(): @hub.command(name="get") @click.argument("bundlename", nargs=1) -def hubget(bundlename): +@click.pass_context +def hubget(ctx, bundlename): ecode = 0 try: + anchorecli.cli.utils.handle_parent_callback(ctx) + ret = anchorecli.clients.hub.get_policy(config, bundlename) if ret["success"]: print( @@ -323,10 +365,13 @@ 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): 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 9020b78..2a9dcc9 100644 --- a/anchorecli/cli/query.py +++ b/anchorecli/cli/query.py @@ -9,16 +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( @@ -47,13 +50,18 @@ 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: + anchorecli.cli.utils.handle_parent_callback(ctx) + ret = anchorecli.clients.apiexternal.query_images_by_vulnerability( config, vulnerability_id, @@ -100,11 +108,15 @@ 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: + 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 706d38c..13ffeda 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, @@ -59,9 +64,10 @@ def add( """ 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: " @@ -133,7 +139,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, @@ -152,6 +160,8 @@ def upd( ecode = 0 try: + anchorecli.cli.utils.handle_parent_callback(ctx) + if not registry_name: registry_name = registry @@ -186,13 +196,16 @@ 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 """ 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"]: @@ -214,10 +227,13 @@ def delete(registry): @registry.command(name="list", short_help="List all current registries") -def registrylist(): +@click.pass_context +def registrylist(ctx): 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"]: @@ -240,13 +256,16 @@ 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 """ 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 0b2fe96..b9b32fa 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,21 +41,24 @@ 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 """ 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, - auto_subscribe=auto_subscribe, + auto_subscribe=autosubscribe, lookup_tag=lookuptag, dry_run=dryrun, ) @@ -75,10 +81,13 @@ def add(input_repo, noautosubscribe, lookuptag, dryrun): @repo.command(name="list", short_help="List added repositories") -def listrepos(): +@click.pass_context +def listrepos(ctx): 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"]: @@ -100,16 +109,19 @@ 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 """ 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) @@ -137,16 +149,19 @@ 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 """ 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: @@ -174,16 +189,19 @@ 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 """ 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: @@ -211,16 +229,19 @@ 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 """ 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 98530ca..6b2f3a8 100644 --- a/anchorecli/cli/subscription.py +++ b/anchorecli/cli/subscription.py @@ -8,22 +8,30 @@ @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: @@ -38,6 +46,8 @@ def activate(subscription_type, subscription_key): ecode = 0 try: + anchorecli.cli.utils.handle_parent_callback(ctx) + ret = anchorecli.clients.apiexternal.activate_subscription( config, subscription_type, subscription_key ) @@ -66,7 +76,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: @@ -81,6 +92,8 @@ def deactivate(subscription_type, subscription_key): ecode = 0 try: + anchorecli.cli.utils.handle_parent_callback(ctx) + ret = anchorecli.clients.apiexternal.deactivate_subscription( config, subscription_type, subscription_key ) @@ -112,9 +125,13 @@ 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): 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 8ba1011..8dcbdf0 100644 --- a/anchorecli/cli/system.py +++ b/anchorecli/cli/system.py @@ -17,24 +17,31 @@ 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): 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"]: @@ -59,10 +66,13 @@ 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): 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"]: @@ -111,7 +121,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 @@ -124,6 +135,8 @@ def wait(timeout, interval, feedsready, servicesready): 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 @@ -290,10 +303,13 @@ 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): ecode = 0 try: + anchorecli.cli.utils.handle_parent_callback(ctx) + ret = anchorecli.clients.apiexternal.delete_system_service( config, host_id, servicename ) @@ -319,15 +335,22 @@ 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): 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"]: @@ -356,11 +379,14 @@ 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): global input ecode = 0 try: + anchorecli.cli.utils.handle_parent_callback(ctx) + answer = "n" try: print( @@ -407,10 +433,13 @@ 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): 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: @@ -463,9 +492,12 @@ 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): 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 a7e0211..feeb5c9 100644 --- a/anchorecli/cli/utils.py +++ b/anchorecli/cli/utils.py @@ -1731,3 +1731,22 @@ 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 + + +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." + ) diff --git a/tests/functional/test_account.py b/tests/functional/test_account.py index 2bbf6e3..2f93987 100644 --- a/tests/functional/test_account.py +++ b/tests/functional/test_account.py @@ -4,12 +4,23 @@ @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", 2), + ("user", 0), + ("whoami", 2), + ], ) -def test_unauthorized(sub_command): +def test_unauthorized(sub_command, expected_code): out, err, code = call(["anchore-cli", "account", sub_command]) - assert code == ExitCode(2) - assert out == '"Unauthorized"\n' + assert code == ExitCode(expected_code) + if sub_command in ["add", "del", "disable", "enable", "get"]: + assert err.startswith("Usage: anchore-cli account {}".format(sub_command)) class TesttList: diff --git a/tests/unit/cli/test_account.py b/tests/unit/cli/test_account.py index bb2f0c1..00448f8 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..9fcfb57 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..959e05b 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..976cd3b 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..95ce23c 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 @@ -82,6 +84,16 @@ def apply(success=True, httpcode=200, has_error=None): ] +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): monkeypatch.setattr( @@ -92,7 +104,12 @@ 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 +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"]) + 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 +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"]) + 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 +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"]) + 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 +186,9 @@ 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 +210,9 @@ 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 +234,9 @@ 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,5 +262,29 @@ 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 + + +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) diff --git a/tests/unit/cli/test_policy.py b/tests/unit/cli/test_policy.py index 561a91f..e57af97 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..6e00996 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..cf105c1 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..d7e83e0 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..adc5318 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.list_subscriptions, "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..5c94b64 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)