Skip to content

Commit 50ff268

Browse files
feat: add new pre_hook_with_context method to Command (#35)
This feature adds a new lifecycle method to the Command base class: def pre_hook_with_context(self, context): pass The value the context parameter comes from the new context parameter on AccountRunner.run: def run(self, cmd, accounts, key=lambda x: x, context=None): pass This allows users of the awsrun library to pass a runtime context into commands. As the primary user of the library, the awsrun CLI command uses this new feature to pass in references to the account loader and session provider plug-ins to a command. It allows command authors to lookup metadata associated with other accounts and obtaining sessions for other accounts while processing accounts. Here is an example: """Identify shared VPCs from our main account 999999999999 that have been shared with other accounts owned by a different Business Unit.""" import io from awsrun.cli import Context from awsrun.runner import RegionalCommand class CLICommand(RegionalCommand): """Display VPCs configured in accounts.""" def pre_hook_with_context(self, context: Context): acct_id = "999999999999" # Let's get the account object for our main account. Assume the # account loader plug-in used annotates that object with one # metadata value called "BU" representing the business unit. acct = context.account_loader.accounts(acct_ids=[acct_id])[0] # Let's get a session object for our main account so we can get # a list of VPCs from it. session = context.session_provider.session(acct_id) vpc_ids = [] for region in self.regions: ec2 = session.resource("ec2", region_name=region) vpc_ids.extend(vpc.id for vpc in ec2.vpcs.all()) # Save these for use later in regional_execute() self.important_acct = acct self.important_vpc_ids = vpc_ids def regional_execute(self, session, acct, region): out = io.StringIO() ec2 = session.resource("ec2", region_name=region) # Check each VPC in this current account to see if it is in the # list of VPCs from our main account. And, then check if the BU # for this current account is the same as the BU for our main # account. Report when they differ. for vpc in ec2.vpcs.all(): if vpc.id in self.important_vpc_ids and acct.BU != self.important_acct.BU: print( "{acct}/{region}: Shared VPC {vpc.id} owned by other Business Unit!", file=out, ) return out.getvalue() The above example demonstrates how you can use these plug-ins in your commands to inspect metadata of other accounts and to obtain sessions for any account. This can be useful if you need to access another account while processing the current account. For example, perhaps you want to accept a VPC peering request after initiating it. --------- Co-authored-by: Kazmier, Peter <peter.kazmier@fmr.com>
1 parent 415a351 commit 50ff268

File tree

3 files changed

+157
-13
lines changed

3 files changed

+157
-13
lines changed

src/awsrun/cli.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1078,7 +1078,12 @@ def _cli(csp):
10781078
# awsrun can be used without the need of the CLI. One only needs a list of
10791079
# accounts, an awsrun.runner.Command, and an awsrun.session.SessionProvider.
10801080
runner = AccountRunner(session_provider, args.threads)
1081-
elapsed = runner.run(command, accounts, key=account_loader.acct_id)
1081+
elapsed = runner.run(
1082+
command,
1083+
accounts,
1084+
key=account_loader.acct_id,
1085+
context=Context(session_provider, account_loader, accounts),
1086+
)
10821087

10831088
# Show a quick summary on how long the command took to run.
10841089
pluralize = "s" if len(accounts) != 1 else ""
@@ -1179,5 +1184,19 @@ def default_session_provider(self):
11791184
return "awsrun.plugins.creds." + self.name.lower() + ".Default"
11801185

11811186

1187+
class Context:
1188+
"""Used when `Command.pre_hook_with_context` is invoked.
1189+
1190+
The `Context` provides a `Command` access to awsrun information/context prior to any
1191+
accounts being processed by `Runner`.
1192+
1193+
"""
1194+
1195+
def __init__(self, session_provider, account_loader, accounts):
1196+
self.session_provider = session_provider
1197+
self.account_loader = account_loader
1198+
self.accounts = accounts
1199+
1200+
11821201
if __name__ == "__main__":
11831202
main()

src/awsrun/commands/__init__.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -487,6 +487,72 @@ def post_hook(self):
487487
post-hook to print a summary of the data we collected during the processing of
488488
accounts.
489489
490+
### Pre-Hook With Context
491+
492+
We can use `awsrun.runner.Command.pre_hook_with_context` to obtain access to
493+
the CLI's `awsrun.cli.Context` object, which contains a reference to the
494+
`awsrun.acctload.AccountLoader`, the `awsrun.session.SessionProvider`, and
495+
the list of accounts being processed by the CLI. This allows command authors
496+
to use those plug-ins as part of their command. Let's use an example to
497+
illustrate. Assume that the account loader plug-in annotates the accounts
498+
with one metadata field called "BU" that represents the business unit that
499+
owns the account.
500+
501+
\"\"\"Identify shared VPCs from our main account 999999999999 that have
502+
been shared with other accounts owned by a different Business Unit.\"\"\"
503+
504+
import io
505+
506+
from awsrun.cli import Context
507+
from awsrun.runner import RegionalCommand
508+
509+
510+
class CLICommand(RegionalCommand):
511+
\"\"\"Display VPCs configured in accounts.\"\"\"
512+
513+
def pre_hook_with_context(self, context: Context):
514+
acct_id = "999999999999"
515+
516+
# Let's get the account object for our main account. Assume the
517+
# account loader plug-in used annotates that object with one
518+
# metadata value called "BU" representing the business unit.
519+
acct = context.account_loader.accounts(acct_ids=[acct_id])[0]
520+
521+
# Let's get a session object for our main account so we can get
522+
# a list of VPCs from it.
523+
session = context.session_provider.session(acct_id)
524+
vpc_ids = []
525+
for region in self.regions:
526+
ec2 = session.resource("ec2", region_name=region)
527+
vpc_ids.extend(vpc.id for vpc in ec2.vpcs.all())
528+
529+
# Save these for use later in regional_execute()
530+
self.important_acct = acct
531+
self.important_vpc_ids = vpc_ids
532+
533+
def regional_execute(self, session, acct, region):
534+
out = io.StringIO()
535+
ec2 = session.resource("ec2", region_name=region)
536+
537+
# Check each VPC in this current account to see if it is in the
538+
# list of VPCs from our main account. And, then check if the BU
539+
# for this current account is the same as the BU for our main
540+
# account. Report when they differ.
541+
for vpc in ec2.vpcs.all():
542+
if vpc.id in self.important_vpc_ids and acct.BU != self.important_acct.BU:
543+
print(
544+
"{acct}/{region}: Shared VPC {vpc.id} owned by other Business Unit!",
545+
file=out,
546+
)
547+
548+
return out.getvalue()
549+
550+
The above example demonstrates how you can use these plug-ins in your
551+
commands to inspect metadata of other accounts and to obtain sessions for
552+
any account. This can be useful if you need to access another account while
553+
processing the current account. For example, perhaps you want to accept a
554+
VPC peering request after initiating it.
555+
490556
You should now have a good understanding of the do's and don'ts to keep in mind
491557
when authoring your own commands. In the next section, we will discuss how to
492558
install your commands.

src/awsrun/runner.py

Lines changed: 71 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,36 @@ def regional_execute(self, session, acct, region):
9797
account_runner = AccountRunner(session_provider)
9898
account_runner.run(cmd, ['111222333444', '222333444111'])
9999
100+
## Context With Pre-Hook
101+
102+
In some cases users may wish to pass additional context to the `Command` that
103+
can be used during execution. This can be done via the `context` parameter of
104+
`AccountRunner.run`. That value is passed to `Command.pre_hook_with_context`,
105+
which command authors can then use. For example, to pass a trivial prefix to be
106+
used as part of the output:
107+
108+
from awsrun.runner import AccountRunner, RegionalCommand
109+
from awsrun.session import CredsViaProfile
110+
111+
class VpcInfoCommand(RegionalCommand):
112+
# Save our context, a simple string in this case, to self.prefix.
113+
def pre_hook_with_context(self, context):
114+
self.prefix = context
115+
116+
def regional_execute(self, session, acct, region):
117+
ec2 = session.resource('ec2', region_name=region)
118+
vpc_ids = ', '.join(vpc.id for vpc in ec2.vpcs.all())
119+
# Prefix the output with the prefix passed via context
120+
return f'{self.prefix}/{acct}/{region}: {vpc_ids}\\n'
121+
122+
cmd = VpcInfoCommand(['us-east-1', 'us-west-2'])
123+
session_provider = CredsViaProfile()
124+
account_runner = AccountRunner(session_provider)
125+
# We pass a simple string prefix as the context in this trivial example
126+
account_runner.run(cmd, ['111222333444', '222333444111'], context="Group-A")
127+
account_runner.run(cmd, ['333444555666'], context="Group-B")
128+
account_runner.run(cmd, ['444555666777', '555666777888'], context="Group-C")
129+
100130
## Collecting Results
101131
102132
The final example demonstrates how to collect the results from all of the
@@ -173,6 +203,7 @@ def regional_collect_results(self, acct, region, get_result):
173203
account_runner = AccountRunner(session_provider)
174204
account_runner.run(cmd, ['111222333444', '222333444111'])
175205
"""
206+
176207
import functools
177208
import logging
178209
import sys
@@ -307,12 +338,29 @@ def from_cli(cls, parser, argv, cfg):
307338
parser.parse_args(argv)
308339
return cls()
309340

310-
def pre_hook(self):
311-
"""Invoked by `AccountRunner.run` before any processing starts.
341+
def pre_hook_with_context(self, context):
342+
"""Invoked by `AccountRunner.run` before any account processing starts.
312343
313344
This method is invoked only once per invocation of `AccountRunner.run`.
314-
It is not executed before each account is processed, but rather once
315-
before any accounts are processed.
345+
The `context` parameter is the opaque object passed as the context
346+
parameter to `AccountRunner.run`. It is intended to provide access to
347+
additional runtime context information for the command to leverage.
348+
The method is not executed before each account is processed, but
349+
rather once before any accounts are processed.
350+
351+
To provide backwards compatibility, the default implementation invokes
352+
`Command.pre_hook`.
353+
"""
354+
self.pre_hook()
355+
356+
def pre_hook(self):
357+
"""Invoked in the default implementation of `pre_hook_with_context`
358+
before any account processing starts.
359+
360+
This method is invoked in the event a command does not override the
361+
default implementation of `pre_hook_with_context`. The method is not
362+
executed before each account is processed, but rather once before
363+
any accounts are processed.
316364
317365
The default implementation does nothing.
318366
"""
@@ -844,18 +892,23 @@ def __init__(self, session_provider, max_workers=10):
844892
self.session_provider = session_provider
845893
self.max_workers = max_workers
846894

847-
def run(self, cmd, accounts, key=lambda x: x):
895+
def run(self, cmd, accounts, key=lambda x: x, context=None):
848896
"""Execute a command concurrently on the specified accounts.
849897
850898
This method will block until all accounts have been processed. The
851899
return value is the number of seconds it took to process the accounts.
852900
853-
The `cmd` must be a subclass of `Command`. The runner will invoke the
854-
`Command.pre_hook` once before it starts processing any accounts, then
855-
accounts are processed concurrently and `Command.execute` is invoked by
856-
a worker for each account. As each execute method returns, the main
857-
thread will invoke `Command.collect_results`, which ensures results are
858-
collected sequentially. Finally, after all accounts have been processed,
901+
The `cmd` must be a subclass of `Command`. The runner will invoke
902+
`Command.pre_hook_with_context` once before it starts processing any
903+
accounts passing it the `context` argument, which is simply passed
904+
through as an opaque object. This can be used to pass a runtime context
905+
to a `Command`.
906+
907+
After the pre-hook has been invoked, accounts are then processed
908+
concurrently and `Command.execute` is invoked by a worker for each
909+
account. As each execute method returns, the main thread will invoke
910+
`Command.collect_results`, which ensures results are collected
911+
sequentially. Finally, after all accounts have been processed,
859912
`Command.post_hook` is called.
860913
861914
The specified list of `accounts` can be of any type as long as the
@@ -887,6 +940,12 @@ def run(self, cmd, accounts, key=lambda x: x):
887940
exception, then an `InvalidAccountIDError` is raised in the worker
888941
thread processing the account, which will then propagate to the
889942
`Command.collect_results`.
943+
944+
The optional `context` parameter can be any value. It is passed
945+
as-is to `Command.pre_hook_with_context`. It provides a mechanism
946+
for the caller to pass a runtime context to the command being
947+
executed. See the [Context With Pre-Hook](#context-with-pre-hook)
948+
section of the use guide for an example.
890949
"""
891950
# This will ensure v1 users of awsrun aren't mixing v1 Command's with
892951
# the v2 framework.
@@ -899,7 +958,7 @@ def run(self, cmd, accounts, key=lambda x: x):
899958
key = _valid_key_fn(key)
900959

901960
start = time.time()
902-
cmd.pre_hook()
961+
cmd.pre_hook_with_context(context)
903962

904963
with ThreadPoolExecutor(max_workers=self.max_workers) as pool:
905964
# The worker task processes a single account. The worker task takes

0 commit comments

Comments
 (0)