-
Notifications
You must be signed in to change notification settings - Fork 28
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
Update cluster_role and cluster_role_info modules #88
Conversation
e7ea2c6
to
831ff66
Compare
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 the great implementation, @aljazkosir. But I need to mark this PR with needs more work until we make sure our idempotency checks are working as expected. Can you please check this and report back with the results? Thanks.
And if we indeed find some troubles when digging deeper, we might need to go back and fix the role module. I am mentioning @mancabizjak here just to keep her in the loop about the latest developments in the Tadej failed to properly review the role module area ;)
plugins/modules/cluster_role.py
Outdated
description: | ||
- Rules that the cluster role applies. | ||
type: list | ||
required: yes |
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.
Is the rules
parameter really required? It feels a bit strange that we are demanding its presence while at the same time specifying it in a required_if
condition and not making it mandatory in the argument spec. And while we are here, we should probably also check the role
module and resolve any issues there also. (I am assuming here that this module started as a role
clone and inherited some warts.)
And again, we really need to give sanity
test some much-needed love and polish. Letting things like this slip is really not OK.
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.
Indeed, it's confusing that the sanity checker is happy with or without this line. We should remove this here and in the role
module as well, as rules
is only conditionally required.
plugins/modules/cluster_role.py
Outdated
resource_names=dict( | ||
type="list", | ||
), | ||
|
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.
Nitpick: this empty line is not really needed.
plugins/modules/cluster_role.py
Outdated
payload = arguments.get_mutation_payload( | ||
module.params, "rules" | ||
) | ||
payload['metadata'].pop('namespace') # Remove it, since it's not available to cluster roles |
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.
Maybe we should teac our arguments.get_mutation_payload
method to ignore namespace
metadata field if the auth["namespace"]
is None
? We already filter the other payload arguments this way, so this would not be too much of an exception.
- get | ||
- list |
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.
Does this test fail if we swap the verbs? If the answer is yes, we need to create a custom comparator function that will ignore the order of verbs/resources/resource names.
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.
nice catch :D, tests are failing if verbs are swapped. Custom comparator it is.
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 catch indeed, this one slipped us through for role
.
plugins/modules/cluster_role.py
Outdated
description: | ||
- Rules that the cluster role applies. | ||
type: list | ||
required: yes |
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.
Indeed, it's confusing that the sanity checker is happy with or without this line. We should remove this here and in the role
module as well, as rules
is only conditionally required.
- get | ||
- list |
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 catch indeed, this one slipped us through for role
.
|
||
|
||
class TestClusterRoleInfo(ModuleTestCase): | ||
def test_get_all_roles(self, mocker): |
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.
I would rename the method to test_get_all_cluster_roles
, just to be consistent with naming in test_cluster_role.py
.
assert path == "/clusterroles" | ||
assert context.value.args[0]["objects"] == [1, 2, 3] | ||
|
||
def test_get_single_role(self, mocker): |
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.
Same as above, I suggest renaming to test_get_single_cluster_role
for consistency.
2b3faea
to
67d08b6
Compare
67d08b6
to
9182afe
Compare
I fixed the issues, I did some changes regarding the custom comparator for rules. I created a new file `rule_utils˙ as we talked about in other PR and also backed it with some tests. I would love a feedback on the suggested changes :). |
9182afe
to
c86c9ef
Compare
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.
I reviewed the changes, looks great 👍 Very happy to see such a comprehensive test suite for the role_utils
! Just a couple of minor comments/clarifications from my side.
plugins/module_utils/role_utils.py
Outdated
sorted_current = _sort_rules(current_rules) | ||
sorted_desired = _sort_rules(desired_rules) | ||
|
||
if len(sorted_current) != len(sorted_desired): |
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.
You can move this check to the beginning of the function (if len(current_rules) != len(desired_rules)
), to avoid unnecessary sorting in case the lengths are different.
plugins/module_utils/utils.py
Outdated
return False | ||
|
||
|
||
def sync(state, client, path, payload, check_mode, comparator=do_differ): |
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.
I would really like if we called this argument compare
instead of comparator
🙂 To me, it makes more sense when the code reads like if compare(a, b)
instead of if comparator(a,b)
later on. Plus, comparator sounds kind of classy (no pun intended), meaning that I would expect it to point to (an instance of a) class rather than a simple function.
This is my personal preference ofc, interested to hear what @tadeboro thinks :D
@@ -0,0 +1,122 @@ | |||
from __future__ import absolute_import, division, print_function |
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.
test_cluter_role.py
-> test_cluster_role.py
😱 oops 😀
[{'a': []}] | ||
) is False | ||
|
||
def test_rules_when_value_is_none(self): |
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.
I find this test a bit confusing, what exactly did you want to test here? Maybe it's just the actual test data that confuses me - I would at least make the value of a
the same in current and desired rule set (or remove a
entirely, if we want to test the behavior for None
case in isolation)?
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.
Yeah, I wanted to test the None cases, so a
should not really matter in that case.
Maybe it would've be better if I used the actuall keys and values used in real case data instead of a
and b
and numbers.
plugins/module_utils/role_utils.py
Outdated
if key == 'rules': | ||
if _do_rules_differ(current['rules'], desired['rules']): | ||
return True | ||
elif value != current.get(key): |
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.
Although I really like how nice and readable L38 is, we already have desired['rules']
stored in value
and we could also potentially store current['rules']
(which is exactly current.get(key)
from L40) to current_value
.
Something like:
for key, value in desired.items():
current_value = current.get(key)
if key == 'rules':
if _do_rules_differ(current_value, value):
return True
elif value != current_value:
return True
I don't mind if you leave it as is though, I will have to slightly modify this part myself in #83 🙂.
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.
Heh, that condition was not in the loop at first, that's why it looks like this 😅. I moved it there few seconds before the push. I'll change it, it looks better anyway 👍
} | ||
assert role_utils.do_differ(current, desired) is True | ||
|
||
def test_rule_exists_but_with_additional_rules(self): |
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.
I think you meant test_role_exists_but_with_additional_rules
f5da62a
to
e9e647b
Compare
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.
One more solid piece of code. It is almost a pleasure to read PRs like this one ;)
I added two comments regarding the code itself, but what I would also like to see is changes being split into two or three commits: one for each changeset. One the change of the sync
method definition, one for module implementation (and an optional commit for modification of get_mutation_payload
function).
plugins/module_utils/role_utils.py
Outdated
def _sort_rules(rules): | ||
for rule in rules: | ||
for key in rule: | ||
if rule[key]: | ||
rule[key].sort() | ||
return rules |
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.
This function mutates the input data, which is something I would rather see we avoid as much as possible. Creating a copy of the data might seem expensive, but not as expensive as tracking down the exact mutation place once something will go wrong ;)
But to be completely honest, I would try to avoid sorting the data altogether and instead convert the list of rules into a set of rules. This would mimic the semantics of the API far better. Because, let us face it, sorting two lists and then comparing them is just a low-level implementation of set comparison.
A rule in a mathematical sense is a triplet of sets (verbs, resources, and resource names). So set of rules would be represented In python as
return {
(set(r["verbs"]), set(r["resources"]), set(r["resource_names"]) for r in rules
}
I left out is None
checks, but the general idea should be clear from the code above. We can compare those sets using regular ==
.
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.
@tadeboro I tried with the suggested approach but I had some problems. If I used "one line loop" I was getting generators inside set, which made it impossible to compare. I solved it by implementing a normal for loop that adds a rule one at a time.
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.
Generators inside sets? That sounds like a misplaced )
. I would expect troubles with sets inside a tuple that is then placed in a set itself because the whole tuple becomes unhashable.
This is a bit more polished version of the code that I had in mind:
{
(
frozenset(r["verbs"] or []),
frozenset(r["resources"] or []),
frozenset(r["resource_names"] or [])
) for r in rules
}
Can you please check again what went wrong? Thanks.
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.
Hmm I see, one too many ()
😅. Wouldn't be better to use r.get('verbs', []) or []
for cases that key is completely missing?
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.
Ugh, yes, forgot that arguments are not always ansible-supplied and thus some keys can be missing. Great catch. And now you can ignore my comment in review about this ;)
e9e647b
to
d1529c0
Compare
d1529c0
to
cfb48d8
Compare
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.
There is only one serious thing to resolve before we can merge: making sure we know what happens with empty lists in arguments. Other than that, I have no major complaints. Great work.
plugins/module_utils/role_utils.py
Outdated
@@ -42,3 +42,40 @@ def _do_subjects_differ(a, b): | |||
sorted_a = sorted(a, key=lambda x: (x['type'], x['name'])) | |||
sorted_b = sorted(b, key=lambda x: (x['type'], x['name'])) | |||
return sorted_a != sorted_b | |||
|
|||
|
|||
def _rule_sets(rules): |
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.
Maybe we should keep this name singular since we always return a single set?
) | ||
) | ||
|
||
module.params['auth']['namespace'] = None # Making sure we are not fallbacking to default |
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.
That comment is the best!
cfb48d8
to
14180ef
Compare
14180ef
to
8d6b9f6
Compare
def validate_module_params(module): | ||
params = module.params | ||
if params['state'] == 'present': | ||
if not params['rules']: | ||
module.fail_json( | ||
msg='state is present but all of the following are missing: rules' | ||
) |
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.
I there a reason why this is not implemented using required_if
?
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.
required_if
only checks if value is None
, here we also check if rules
is empty array.
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.
This is ready to go. I will wait for @aljazkosir to report back with the answer about the custom validation of rules parameter and then we can smash the merge button. Thanks!
|
||
- assert: | ||
that: | ||
- result.objects | length == 8 # There are 6 default cluster roles |
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.
This will save us a ton of debugging once the Sensu changes defaul settings.
@mancabizjak Once you clear this PR, can you please merge it? Thank you. |
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.
Looks great 👍 I will create a follow-up PR for the role
module in order to mirror custom comparator and additional validation check added here.
This PR is a follow-up of #70. We add cluster role and cluster role info support as a new modules. Cluster roles adds support for
Cluster-wide resource types
in addition to namespaced resource types.