-
Notifications
You must be signed in to change notification settings - Fork 8
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
extend target_ids to support AWS Organization Units #30
extend target_ids to support AWS Organization Units #30
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey - thanks for the excellent contribution!
I've added a few comments to the PR that it'd be good to address before merging, but I really like the change.
There are some typing and linting errors - poetry run pre-commit run --all-files
will flag them up.
I'd like to have some tests for the recursive logic but I'm happy to contribute those later - I've been playing with the moto library as mocking out the org paginators manually would be very unplesant.
@aperov9 , thanks for the contribution! It's been on my mental feature backlog for botocove for a while. I agree absolutely that botocove should have a way to filter by organizational unit (OU). I don't agree with making it another responsibility of the CoveHostAccount class. Overloading the meaning of target_account_ids makes it harder to explain what the parameter means. CoveHostAccount is already quite complicated, and I think if anything we should start trying to break it up into tightly-scoped and more testable parts. My request is that this feature be implemented with minimal changes to the existing CoveHostAccount class. I believe it will make for more robust and reusable code. I propose that we encapsulate the logic in a new function called something like @cove(
target_account_ids=org_account_selector(included_ous=["ou-cpg3-k3ijh102", "ou-ckf1-kp2i7tzd"]),
)
def wrapped_function(session):
... By default the function would return the IDs of all the organization's AWS accounts whose status is active, matching botocove's current behavior. It would take an optional parameter called something like It would take another optional parameter called something like If you need to express your account selection as mix of OU memberships and literal account IDs, you can do this by concatenating the result of the function with a list of literal IDs. @cove(
target_account_ids=(
org_account_selector(included_ous=["ou-cpg3-k3ijh102", "ou-ckf1-kp2i7tzd"]) +
["111111111111", "222222222222"]
),
)
def wrapped_function(session):
...
The built-in botocore Stubber class allows us to mock individual API calls in a specific order. That would couple the implementation too tightly to the AWS APIs and have us testing the implementation rather than the interface. If moto supports all the organizations APIs that we need to use, I agree that it is probably the best way to go for testing this. It should allow us to declare different organization structures using a series of create_account calls in the setup functions of the test suite. @connelldave , @aperov9 , what do you think of my proposal? If you agree I would be able to help with the testing and implementation. I have some code in aws-org-tree that could be adapted for the purpose. Its job is to recurse over the whole AWS organization to link each AWS account to its parent OU. Since we don't even need to retain the OU membership information, a simplier verison of it would probably be good enough. aws-org-tree doesn't have any tests yet either, but I was planning on adding tests using moto before this issue appeared. :-) |
I feel this concern, but given the kwarg name is My primary worry with overloading is that there's an implicit coupling between passing OU id's and org_master=True. The presence of an OU id adds a surprise dependency for IAM permission and requirement for use of the Organizations client. We can check for this and fail explicitly though. A more decomposed, decoupled approach that requires helper functions and complexity to resolve function calls in construction is a large burden on users, especially as decorators are most often constructed in global scope. The other challenge is actually making users aware of this behaviour and where to import it from: ideally the barrier for entry for Python users is low. Failing on bad input rather than requiring complexity in my head is closer to the zen of Python :)
Overloading ignore_ids to also take OU ids would also solve this problem, which is an extension that crossed my mind too :) |
@connelldave @iainelder Before writing my code I thought about the same issue if I would go with an extra parameter, an additional helper function or just overload the existing target_ids. I prefered the simplicity of the end solution that account id's and organization unit id's are both just target_ids. However, I do see that this way is harder to test. I would be glad if someone could assist in writing the test for this new functionality. I am also not familiar enough on how the org_master parameter is impacted by this in particular. Also I don't see an issue in overloading the ignore_ids in the same way :) |
No worries! For context - the org_master argument is actually in a bit of a weird place since release 1.5.0 - it's actually not required in CoveHostAccount right now. It only gets used to disable enrichment of session information as things stand. It's purpose is for a user to be able to disable the standard behaviour of botocove relying on running in an organisation's master account - for situations such as security account that has trust relationships with roles in many accounts. This would make calls to the organizations APIs that rely on running from an org master (list_accounts, describe_account and now list_children) unavailable. We're actually checking only if target_ids=None today on line 110 of cove_host_account.py for "don't do org master related tasks" which is a bit smelly - maybe we should raise if org_master is False in this block or similar too. if target_ids is None:
# No target_ids passed
# new:
if self.org_master is False:
raise ValueError("target_ids keyword argument must be provided if org_master is set to False")
target_accounts = self._gather_org_assume_targets() |
@iainelder couldn't tag you on the fork PR but aperov9#1 - fyi This is just a first pass this morning, it can probably be used more cleanly but as a first moto attempt it was nice to work with! No obligation to merge or use it @aperov9 , just thought I'd share if useful. Feel free to alter or use it if helps dev. |
You've persuaded me on the overloading. Overloading the meaning of the existing I still believe that by avoiding modifications to CoveHostAccount to implement this feature we'll have an easier time maintaining and testing it. So I would propose something like the
Botocove already documents the permissions that it requires. Don't we just need to add the extra permissions the feature requires to the documentation?
I was thinking that we should rename the
I also appreciate yours! It's great to see that this feature is also useful for others. Thanks for the PR link. I'll have a look later on. |
Co-authored-by: Dave Connell <39798556+connelldave@users.noreply.github.com>
Co-authored-by: Dave Connell <39798556+connelldave@users.noreply.github.com>
With the latest commit ffc946e I addressed all your previous comments @connelldave and checked that pre-commit runs now without any errors. Next, I am looking into extending the logic to |
Thanks - looks good!
Suggest splitting this across two commits - I'm keen to see what problem that decoupling solves, since "getting accounts" is in the remit of the host account when running from an org master account. |
I didn't get chance to look at the code for this and I won't have any time until at least next week. Don't wait for me to merge it if it looks good to you @connelldave . I trust your judgement here. |
Extended the logic so I also looked into decoupling the logic and how the current CoveHostAccount class is setup. I realised that there is no real benefit decoupling it even more. I think the existing solution is sufficient. Lastly, while testing I encountered the following situation. Botocove will return "There are no eligible account ids to run decorated func against" in case an ou in |
I did a bit of clean-up work on my branch for multi-region support as there's a bit of duplication around how we build these two sets. We also need to guard against an OU capturing the host account itself in it's account list: I don't think botocove should impact the Host account. With that in mind, we can just decompose the
does that make sense? I'll try and get #29 merged this evening which starts this process, you could rebase on that branch to save time if you'd like.
I agree, but for this iteration we can just ship "Did you ignore a parent OU that all target accounts belong to?" or something rather than adding complexity trying to track this edge case. My thinking so far has been only worry about checking for cases that can cause risk - that's why initially only ignore_ids got a validation pass, a malformed ignore has much higher consequence than a malformed target. |
|
||
def _get_all_child_ous(self, parent_ou: str, ou_list: List[str]) -> None: | ||
|
||
child_ous = ( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let's break both paginators out into function calls so we can use a cache (functools.lru_cache would be my suggestion) to avoid re-doing work when traversing now that we're potentially going to be walking the same OU tree twice or more
(for example - in OU A, containing B and C, we want to pass A as a target but ignore C - we'd look up children of C twice, and contents of C twice without caching)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we can patch this in later, let's not block merging with minor optimisation :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great, thanks for your valuable feedback! And lets patch this optimisation later
With the latest commits I merged the changes from master into my branch and adjusted/added some test cases with the moto mock org. The setup is indeed quite easy to use and self explanatory :) I also refactored The only thing missing now would be the caching mechanism and some further test cases. I'll be quite busy the next weeks and I'm not too really familiar with the functools module. So feel free to look into that and create a MR in the meantime. @connelldave |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for making changes from feedback! Excellent addition, feels like a clean implementation and upgrade. I'd like to get it merged :)
If you want to push a commit to pick up on these final comments feel free, or I can pick them up on the release commit on master if you don't have time.
Thanks again!
Dave
|
||
def _get_all_child_ous(self, parent_ou: str, ou_list: List[str]) -> None: | ||
|
||
child_ous = ( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we can patch this in later, let's not block merging with minor optimisation :)
🎉 now live on pypi under version 1.6.0! I snuck in the caching on master: it's very simple botocove/botocove/cove_host_account.py Line 238 in f6d52f8
|
Modified botocove so it can take AWS accounts and/or AWS Organization Units as target_ids.
It will fetch all child ou's from the parent ou recursively.
Exceptions will be raised if target_ids does not match the regex for account or ou.
Possible inputs now: