Skip to content
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

Merged
merged 2 commits into from
Nov 7, 2019

Conversation

aljazkosir
Copy link
Contributor

@aljazkosir aljazkosir commented Oct 30, 2019

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.

@aljazkosir aljazkosir changed the title Update cluster_role and cluster_role_info modules [WIP] Update cluster_role and cluster_role_info modules Oct 30, 2019
Copy link
Contributor

@tadeboro tadeboro left a 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 ;)

description:
- Rules that the cluster role applies.
type: list
required: yes
Copy link
Contributor

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.

Copy link
Contributor

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.

resource_names=dict(
type="list",
),

Copy link
Contributor

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.

payload = arguments.get_mutation_payload(
module.params, "rules"
)
payload['metadata'].pop('namespace') # Remove it, since it's not available to cluster roles
Copy link
Contributor

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.

Comment on lines 68 to 82
- get
- list
Copy link
Contributor

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.

Copy link
Contributor Author

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.

Copy link
Contributor

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.

description:
- Rules that the cluster role applies.
type: list
required: yes
Copy link
Contributor

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.

Comment on lines 68 to 82
- get
- list
Copy link
Contributor

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):
Copy link
Contributor

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):
Copy link
Contributor

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.

@aljazkosir aljazkosir force-pushed the update-cluster-role branch 3 times, most recently from 2b3faea to 67d08b6 Compare November 5, 2019 06:48
@aljazkosir aljazkosir changed the title [WIP] Update cluster_role and cluster_role_info modules Update cluster_role and cluster_role_info modules Nov 5, 2019
@aljazkosir
Copy link
Contributor Author

aljazkosir commented Nov 5, 2019

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 :).

Copy link
Contributor

@mancabizjak mancabizjak left a 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.

sorted_current = _sort_rules(current_rules)
sorted_desired = _sort_rules(desired_rules)

if len(sorted_current) != len(sorted_desired):
Copy link
Contributor

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.

return False


def sync(state, client, path, payload, check_mode, comparator=do_differ):
Copy link
Contributor

@mancabizjak mancabizjak Nov 5, 2019

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
Copy link
Contributor

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 😀

tests/unit/module_utils/test_role_utils.py Show resolved Hide resolved
[{'a': []}]
) is False

def test_rules_when_value_is_none(self):
Copy link
Contributor

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)?

Copy link
Contributor Author

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.

Comment on lines 37 to 74
if key == 'rules':
if _do_rules_differ(current['rules'], desired['rules']):
return True
elif value != current.get(key):
Copy link
Contributor

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 🙂.

Copy link
Contributor Author

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):
Copy link
Contributor

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

@aljazkosir aljazkosir force-pushed the update-cluster-role branch 2 times, most recently from f5da62a to e9e647b Compare November 5, 2019 11:00
Copy link
Contributor

@tadeboro tadeboro left a 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/arguments.py Show resolved Hide resolved
Comment on lines 10 to 15
def _sort_rules(rules):
for rule in rules:
for key in rule:
if rule[key]:
rule[key].sort()
return rules
Copy link
Contributor

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 ==.

Copy link
Contributor Author

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.

Copy link
Contributor

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.

Copy link
Contributor Author

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?

Copy link
Contributor

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 ;)

Copy link
Contributor

@tadeboro tadeboro left a 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 Show resolved Hide resolved
@@ -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):
Copy link
Contributor

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?

plugins/modules/cluster_role.py Show resolved Hide resolved
plugins/modules/cluster_role.py Show resolved Hide resolved
plugins/modules/cluster_role.py Show resolved Hide resolved
plugins/modules/cluster_role.py Show resolved Hide resolved
)
)

module.params['auth']['namespace'] = None # Making sure we are not fallbacking to default
Copy link
Contributor

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!

Comment on lines +83 to +89
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'
)
Copy link
Contributor

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?

Copy link
Contributor Author

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.

Copy link
Contributor

@tadeboro tadeboro left a 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
Copy link
Contributor

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.

@tadeboro
Copy link
Contributor

tadeboro commented Nov 6, 2019

@mancabizjak Once you clear this PR, can you please merge it? Thank you.

Copy link
Contributor

@mancabizjak mancabizjak left a 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants