Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Description of your changes
Disclaimer⚠️
I am not entirely happy with this solution and am wondering if we should merge this. Putting it out for discussion and explaining my thought process below
Intro
In #237 we made all non-init fields optional, so that they
can be set either in
spec.forProvider
orspec.initProvider
. For top level fields, we replaced the "required" validation withCEL rules. Such as:
// +kubebuilder:validation:XValidation:rule="!('*' in self.managementPolicies || 'Create' in self.managementPolicies || 'Update' in self.managementPolicies) || has(self.forProvider.title) || has(self.initProvider.title)",message="title is a required parameter"
But we didn't have a solution for the nested fields. So the purpose of this PR is to add CEL validation for nested fields that should be required.
This proved to be tricky due to 2 factors:
Starting this task I had expected to generate something like:
But I was very wrong 😰 .
Problem explanation
Firstly, we generate almost every nested struct as an array. Even though the resource expects and needs only one element of that array. I think its due to how terraform schemas are translated into our types, as they mark all of the nested structs as lists of nested structs. I didn't go too deep into this as it's highly unlikely that we would make some changes there and break our API.
Example from upbound/provider-gcp Cluster:
Secondly, it is how CEL works:
Scope
For the scope, it basically means that as we want to reach both
forProvider
andinitProvider
, the CEL rule has to be defined onspec
. We cannot define a rule in a nested field as it wont have access toinitProvider
and vice-versa. Otherwise this would be very handy and we wouldn't have a problem with field selection as we could just set the rule on the nested struct in question.Field selection
On the other hand, we have the problem of how to actually get through to the field we want to check if it is set. There are multiple ways.
Lets imagine we have the following resource:
The CEL rule can look like this if we want to check just if the first element exists. For most cases this is ok as we expect only one element. For those that are truly lists we miss
has(self.forProvider.someList[0].reqField) || has(self.initProvider.someList[0].reqField)
CEL also provides an
all
,exists
,exists_one
macros that could be helpful:self.forProvider.someList.all(e, has(e.reqField)) || self.initProvider.someList.all(e, has(e.reqField))
The problem here is that the check will not work well with true lists, as we could have some elements in initProvider, and some in forProvider, which is narrow edge case, but still worth to keep in mind. If we use
exists
we come back to a similar solution as using[0]
.Example where all wont work well.
So lets say that we use the solution with
[0]
:has(self.forProvider.someList[0].reqField) || has(self.initProvider.someList[0].reqField)
What if
someList
is optional? The fieldreqField
is only required ifsomeList
actually exists. With the above rule, we would force users to usesomeList
. Thats why we could add:!has(self.forProvider.someList) || has(self.forProvider.someList[0].reqField) || has(self.initProvider.someList[0].reqField)
And as resources have required fields few levels deep, we can get things like this(example is from GCP Provider AccessLevel):
which becomes unreadable, but works.
As I said in the beginning, not sure if we should include this. Maybe somebody will see a better solution. The current PR implements the above.
Fixes #239
I have:
make reviewable
to ensure this PR is ready for review.backport release-x.y
labels to auto-backport this PR if necessary.How has this code been tested
Tested with https://github.com/upbound/provider-gcp/. Ran
make generate
with myupjet
. Created the/apis/accesscontextmanager/v1beta1/zz_accesslevel_types.go
CRD in a local kind cluster and tested with a few examples:Resulting CEL rules:
Minimal passing validation:
Result: PASSES
Description: contains the 3 required top level fields
Minimal not passing validation
Result: FAILS
Description:
Nested validation - one level
Result: PASSES
Description: sets just
basic[0].conditions[0].devicePolicy[0]
. Does not go further intoosConstraints[]