-
Notifications
You must be signed in to change notification settings - Fork 40.7k
Enable the RequiredFields Kube-API-Linter rule #131884
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
base: master
Are you sure you want to change the base?
Enable the RequiredFields Kube-API-Linter rule #131884
Conversation
This PR may require API review. If so, when the changes are ready, complete the pre-review checklist and request an API review. Status of requested reviews is tracked in the API Review project. |
# All four of these files contain a variant of {Ingress}PortStatus, which has an Error field marked as optional and kubebuilder:validation:Required. | ||
# This means the behaviour of the field is inconsistent for core types vs CRDs, and should not be fixed. | ||
# It also means the requiredfield linter complains about the fields being pointers, which they must be for the core types. | ||
- path: "staging/src/k8s.io/api/core/v1/types.go|staging/src/k8s.io/api/extensions/v1beta1/types.go|staging/src/k8s.io/api/networking/v1beta1/types.go|staging/src/k8s.io/api/networking/v1/types.go" |
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 don't understand what this is saying. The field appears to be annotated incorrectly; it claims kubebuilder:validation:Required
, but the actual API validation does not require it. Why shouldn't we just remove the annotation?
(Also, why do you mention CRDs here?)
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.
Anyone who is creating a CRD that imports these types somewhere into their API, would fall into this switch case in the CRD schema generator and would hit kubebuilder:validation:Required
rather than optional
.
This means that the CRD schema that is generated would say that the Error
field for their API is required.
If we removed the kubebuilder:validation:Required
marker, than the next time they generate their CRD, that field would change from required to optional, which is technically a breaking change, which we probably don't want to just blanketly force upon people.
Yes, I appreciate that this is a core type, but we do know that folks import core types and generate schemas based on the markers on those core types in the wild
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.
creating a CRD that imports these types somewhere into their API
ah, can you clarify that a little better in the comment?
and maybe the API docs for that type should warn about this too? (Actually, why does the linter exception end up in an unrelated file somewhere else rather than being a //nolint
-style comment at the point where it's needed?)
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.
ah, can you clarify that a little better in the comment?
I can certainly try!
and maybe the API docs for that type should warn about this too?
I can also give that a go
//nolint-style comment at the point where it's needed?
Using an exceptions file is not new to K/K.
At present, the only way to ignore one of the linter rules is //nolint:kubeapilinter
which would ignore every rule on that line/type, which is undesirable.
It would probably be beneficial to have a no lint for the individual rules but we don't have that yet. If the exceptions are not natural like this, and folks think it's important, I can look at prioritising a way to ignore the individual 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.
@danwinship If you can check the latest updates, is the documentation clearer now?
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.
what's the history behind // +kubebuilder:validation:Required
getting added to an optional field ... that seems like a bug at first glance
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.
Seems it was added in c970a46#diff-5ffbebb40a29f8e491f4ff1e07c618eb3edcc0d5f00a1a71445272d6767d7883, which added the field as +optional
, but also +kubebuilder:validation:Required
Scanning through the comments on the PR that added it, I cannot see anything that suggests it was talked about
4847c6a
to
d4e7ed6
Compare
[APPROVALNOTIFIER] This PR is NOT APPROVED This pull-request has been approved by: JoelSpeed The full list of commands accepted by this bot can be found here.
Needs approval from an approver in each of these files:
Approvers can indicate their approval by writing |
d4e7ed6
to
b0c4efd
Compare
@JoelSpeed: The following test failed, say
Full PR test history. Your PR dashboard. Please help us cut down on flakes by linking to an open issue when you hit one in your PR. Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository. I understand the commands that are listed here. |
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.
lgtm
/triage accepted |
@@ -229,7 +244,7 @@ linters: | |||
# - "nomaps" # Ensure maps are not used, unless they are `map[string]string` (for labels/annotations/etc). | |||
# - "nophase" # Ensure field names do not have the word "phase" in them. | |||
# - "optionalorrequired" # Every field should be marked as `+optional` xor `+required`. | |||
# - "requiredfields" # Required fields should not be pointers, and should not have `omitempty`. | |||
- "requiredfields" # Required fields should not be pointers, and should not have `omitempty`. |
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.
remind me why a required field should not be a pointer? that seems orthogonal to omitempty
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.
If you had a field that was a pointer, but not omitempty, assuming a structured client hasn't set it, so, something like
type A struct {
Foo *string `json:"foo"`
Bar *string `json:"bar,omitempty"`
}
a := A{}
aData, err := json.Marshal(a)
if err != nil {
panic(err)
}
fmt.Printf("A: %s", string(aData))
The output would be A: {"foo":null}
.
Now, I know fields can be +nullable
, but my understanding was that this is the rarer case, and generally fields would not be nullable. I've not personally come across a use case for this before.
In the case that the structured client hasn't expressed an opinion on this required, the likelihood is that null
would be rejected by the API server as an invalid value, but not for the right reason?
For example, a required string field, I would expect to have some sort of validation that asserts that when specified, the length is greater than zero. This certainly seems to be the case for all the field validations I did check as part of working up this PR.
I also believe that the more pointers we have in our APIs, the more likely folks are to create accidental dereferencing bugs, so we should encourage not to use pointers where it makes sense.
Required fields have the opposite properties, namely:
* They do not have an +optional comment tag.
* They do not have an omitempty struct tag.
* They are not a pointer type in the Go definition (e.g. AnotherFlag SomeFlag).
* The API server should not allow POSTing or PUTing a resource with this field unset.
So far I've only discovered 1 example where the field is a pointer and also required, and when I reviewed the validation for that field I couldn't see any reason why changing that field to remove the pointer would have made a difference to the validation from either structured or unstructured client perspectives.
Since not every field is marked up as either +optional
or +required
yet, there may be more exceptions out there to discover still.
Are there examples you're aware of where the fields are required and should absolutely be pointers?
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.
Any required scalar field where the zero-value is a valid value (e.g. 0 for int fields, false for bool fields) must be a pointer to avoid implicitly defaulting to the zero-value client-side via a typed client.
Once it's a pointer, unless you want to make it nullable as well (which you probably don't), it would also need to be omitempty
to keep spurious nulls out of the serialization.
So something like this seems fairly legitimate / necessary if you want a required scalar field where the zero-value of the scalar is valid:
// +required
// +k8s:minimum=0
Foo *int32 `json:"foo,omitempty"`
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.
So that example breaks two of the four points from the API conventions. There are always exceptions I guess.
Does this example also mean that we shouldn't be enforcing no omitempty on required fields?
For the reverse of this linter, the optionalfields
, we implemented bounds checking, and the linter recommends the best approach it can based on the bounds it has available. I'm wondering if something similar could be implemented for cases like this integer one you mention.
Until we have declarative validation on fields, being smart in the linter is harder for built in types though. I could certainly go ahead and implement it, but we would have to find exceptions and mark them with the declarative validation syntax as we find them I think? As mentioned, so far there are no fields marked explicitly as required that have had this issue so far
Would you agree with:
- Required fields should not have omitempty, and should not be pointers UNLESS their zero value is a valid and accepted value, and the distinction between unset and the zero value matters
This would mean then that a string with a +k8s:minLength
of 0
would also need to be pointer and omitempty
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.
So that example breaks two of the four points from the API conventions. There are always exceptions I guess.
Hmm... I didn't remember the conventions talking about required fields not being pointers and not using omitempty. That's worth an update there... those two conventions in that direction are less hard and fast.
Something like:
Required fields have the following properties:
- They do not have an `+optional` comment tag.
- They mark themselves as required explicitly with a `+required` comment tag, or implicitly by not setting an `omitempty` struct tag.
- The API server should not allow POSTing or PUTing a resource with this field unset.
- They _typically_ do not use pointer types in the Go definition (e.g. `AnotherFlag SomeFlag`), though required fields where the zero value is a valid value may use pointer types, likely paired with an `omitempty` struct tag to avoid spurious null serializations.
Using the `+optional` comment tag, or the `omitempty` struct tag without a `+required` comment tag, causes OpenAPI documentation to reflect that the field is optional.
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 that makes sense to me. I can raise a PR.
So I think in terms of moving this forward, I can look at adding zero value detection based on declarative markers to this linter, and then we would have the following:
- If a field is marked required, and has incomplete
+k8s
markers, it should not be a pointer, and should not have omitempty - If a field is marked required, and has
+k8s
markers that suggest the zero value is valid, then it should be a pointer, and should have omitempty or+nullable
This would mean that as we start marking more fields as +required
during the validation gen project, we would have a way to express to this linter "it's ok for this field to be a pointer" without adding exceptions everywhere, and would help towards the generated validations in the long term
Does that sound like a reasonable plan forward to make this a useful linter rule?
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.
- If a field is marked required, and has incomplete
+k8s
markers, it should not be a pointer, and should not have omitempty- If a field is marked required, and has
+k8s
markers that suggest the zero value is valid, then it should be a pointer, and should have omitempty or+nullable
I'm not quite sure what "incomplete +k8s markers" means... with 0 +k8s markers, the zero value would be considered valid, right?
I think I'd phrase it something like the following:
If a field is marked required, and the zero value appears to be valid (either because of missing +k8s validation markers or validation that allows the zero value), then it should (or maybe could?) be a pointer, and if so, should either have omitempty or +nullable
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'm not quite sure what "incomplete +k8s markers" means... with 0 +k8s markers, the zero value would be considered valid, right?
Yes, sorry, I meant "doesn't have enough markers to know if the zero value is valid or not", so yes, if the markers are incomplete, the 0 value could be valid. In which case yes, the safest thing would be to pointer+omitempty it.
I think I'm coming to the realisation that while I thought this rule was low hanging fruit, it actually will rely on having complete validation on every required field across all of our types before we could really leverage it safely if we implement it correctly.
I'll work on getting the changes on the linter side and see how many exceptions it pulls up here.
And will need to catch up with the validation-gen folks to see what progress there is on markers that would allow this to be useful (minLength, minimum, maximum etc)
If a field is marked required, and the zero value appears to be valid (either because of missing +k8s validation markers or validation that allows the zero value), then it should (or maybe could?) be a pointer, and if so, should either have omitempty or +nullable
I think "should be a pointer is correct" based on the earlier comments in this thread. If the zero value is a valid choice, and the field is required, then without it being a pointer, it cannot have omitempty, and an empty marshalled object would implicitly marshal a value, removing the value of the field being required
PR needs rebase. Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository. |
What type of PR is this?
/kind api-change
What this PR does / why we need it:
This PR enables the
requiredfield
linter rule of KAL.In short:
Enabling this check on K/K flagged 19 existing issues where we have examples of fields marked
+required
but that also had omitempty, or are pointers.4 of these cases refer to approximately the same type,
{Ingress}PortStatus
which has fields marked as+optional
, but also+kubebuilder:validation:Required
. This makes them flag as required for the purpose of this linter, but, they are considered optional within Kube core types. In this case I've added an exception.For the remaining cases:
For each of the above errors, the field validation that prevents the field length from being 0. So whether the JSON serialization omits the value, or marshals the zero value (
"counters": {}
,"strategy": ""
) doesn't appear to matter, both would be invalid. These are also all in alpha and beta API types. I believe the omitempty can be removed in these cases.The
ParentRef
,Resource
andName
are all validated in this validation which checks theParentRef
itself is not nil, and that the Resource and Name are not empty.Fixing Name and Resource is like the above, the difference between omitted and empty wouldn't make a difference to the validation.
For the
ParentRef
, if we removed omitempty, the JSON representation would then benull
, since the field is a pointer. I think this one should not be fixed.The Taint field uses a type
DeviceTaint
, which is a struct. The Taint field does not use a pointer, and so, since the Go JSON library cannot omit a struct reference, the omitempty on this field is pointless and can definitely be removed, as it doesn't change the serialization at all.Which issue(s) this PR fixes:
Fixes #
Special notes for your reviewer:
@thockin This will be the first actual rule enabling checks from KAL. Starting with this one as it seems fairly straightforward and it hasn't got too many existing issues to look at fixing. Would appreciate if you could validate my logic on the acceptable
omitempty
removals above.Does this PR introduce a user-facing change?
Additional documentation e.g., KEPs (Kubernetes Enhancement Proposals), usage docs, etc.: