-
Notifications
You must be signed in to change notification settings - Fork 1.5k
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
Initial KEP for immutable fields #1099
Conversation
[APPROVALNOTIFIER] This PR is NOT APPROVED This pull-request has been approved by: apelisse 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 |
object to new object. | ||
- Lists fields are compared using the same logic currently used for native | ||
kubernetes types: Empty and null lists are equal. A missing list is NOT equal | ||
to a null or empty 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.
I would really like to have a way in CRD OpenAPI to specify a normalization behaviour for lists and maps:
x-kubernetes-normalize-null: empty
x-kubernetes-normalize-null: undefined
x-kubernetes-normalize-empty: null
x-kubernetes-normalize-empty: undefined
And then we should be strict here in the mutation test.
Also note that in Proto we have neither null nor empty by default. I still think we need a more broad discussion of null, undefined, empty values.
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 disagree, I don't know if this should be blocking?
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.
At least we have to leave it open here and have a plan before marking it implementable. But I would not like to move forward with implementation before being very clear about this topic and where it is heading.
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.
Or in other words: whatever we decide here will drive the API design of the whole CRD ecosystem. We should take the time it needs to understand the topic.
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 we should make a decision and enforce it rather than offer too many complicated options. We currently consider empty and null to be equal for existing type, and I think that we should just tell people that's how things are going to work.
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.
My main problem with any non-strict behaviour for CRDs is that it feels very odd to have different objects in etcd, but consider them equal.
For native resource with proto encoding this is not the because proto unifies undefined, null and empty all to one value und unmarshal those to null for comparison.
I wonder whether we should talk about normalization for CRDs before talking about equality:
- we have one normalization already: undefined => default value
- we don't have normalization for empty
- we don't have normalization for null
With normalization happening as part of our deserialization stack, we can separate both concerns and use strict equality in immutability checks.
Today we use semantic equality mostly because JSON and Proto have different normalization behaviours (partly driven by omitempty as well).
normalization JSON omitempty | |
---|---|
undefined | undefined |
null | undefined |
empty | undefined |
normalization JSON | |
---|---|
undefined | null |
null | null |
empty | empty |
normalization Proto omitempty/non-omitempty | |
---|---|
undefined | undefined |
normalization CRD omitempty/non-omitempty | |
---|---|
undefined | undefined |
null | null |
empty | empty |
normalization CRD default=null | |
---|---|
undefined | null |
null | null |
empty | empty |
normalization CRD default=empty | |
---|---|
undefined | empty |
null | null |
empty | empty |
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 I'm understanding all the normalization rules right, then for native types we have something like this?
normalization native (Proto in etcd), client speaking JSON | non-omitempty | omitempty |
---|---|---|
undefined | empty | undefined |
null | empty | undefined |
empty | empty | undefined |
normalization native omitempty/non-omitempty, client speaking Proto | |
---|---|
undefined | undefined |
null | undefined |
empty | undefined |
And we don't have the concept of omitempty empty for CRDs?
Would it make sense to normalize the same way for CRDs as we do for native types (using proto)? I.e. normalize null and empty to undefined. We'd need to normalize all data received from clients and all data read from etcd (since we have non-normalized data at rest). If we did that, we should be able to do strict equality in the api-server code.
I'm guesses that to be consistent we should also normalize native types stored as json like we do for CRDs?
(edit: Adding the normalization would be breaking to any CRD users that expect normalization to not occur.. )
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.
not sure about the first table, what are the values?
And we don't have the concept of omitempty empty for CRDs?
There is no Golang type that could have omitempty in the request handler logic. JSON unmarshalling for CRDs is faithful, i.e. does not swallow or normalize anything.
I am not convinced we want to start putting up fuzzy glasses for CRDs by adding normalization by default. Moreover, we cannot even do that without breaking compatibility, at least not in general (we could do it for equality only).
I don't like the idea of having the normal REST logic with strict empty-null-undefined logic, but immutability to act differently. That's pretty counter-intuitive.
I tend to think about normalization as a property of the fields, i.e. the schema properties. For example we can express nullable fields (which preserve null in all cases, and empty and undefined) using special Golang types (like extra pointers) and at worst custom marshallers. What we don't do today is publishing of this as part of OpenAPI, and in general we never formalized that. But my point is that there are native types which do not normalize prior to persisting values and before comparing, it is just not what we get by default with naive or lazy Golang type definitions and our serializer stack.
In other words, we might want to formalize the normalization of any slice and map in the schemas, for native types and for CRDs. CRD schemas could get a x-kubernetes-normalize-empty: undefined
vendor extensions (not necessarily for 1.17, could be added later). Equality should be strict, relative to the defined normalization (which is not the indenty normalization for native types then).
Also for later introduction of protobuf support for a CRDs, we can require a certain normalization that is compatible with proto. I.e. the CRD validation of a CRD instance with proto enabled, would validate the proto numerals are all specified, and it would validate that normalization is compatible.
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.
properties:
foo:
x-kubernetes-proto: 6
x-kubernetes-normalize-empty: empty
x-kubernetes-normalize-null: empty # if nullable
x-kubernetes-normalize-undefined: undefined
nullable: true
x-kubernetes-immutable: true
validation error: properties.foo.x-kubernetes-normalize-undefined must be empty if x-kubernetes-proto is set.
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 explaining. I don't think I appreciated the extend our existing golang type definitions play into the situation. That's good to know.
From a usability perspective, it wish we could provide a sane default normalization rule that users could get by default and not have to specify any normalization settings explicitly, but I agree we can't just start doing that for CRDs since it breaks backward compatibility.
I really like the idea of specifying the normalization we expect then validating our encodings and go types are all aligned with that.
existing items can be modified. | ||
- Recursive: means that none of their field can change, and new fields are not | ||
accepted. | ||
- Recursive with addition and/or deletion: |
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.
Actually, this can be done by making the list/map mutable, but the values of the list/map immutable.
Is it a goal that this could, in theory, be used to annotate the Pod type? I think we'd want to do:
|
We'll define immutable as "writeOnce", which means that the fields can only be | ||
set at creation time, and can never be updated after. Attempts to update an | ||
immutable field will result in an error (as opposed to being ignored), though we | ||
could potentially add that semantics later. |
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.
Perhaps add the following, to make it explicit:
Clients implement idempotent creation by retrying creation, and they currently do not get errors if the same creation is tried twice. This KEP will not change that. Specifically, two consecutive POSTs of the same object, which are mutated the same way by mutating admission controller (which is the typical case), will both succeed. If an error were returned the second time, and the client did not see the first success response, the client would be confused.
A few possible semantics are described here: | ||
|
||
- Non-recursive: For a list or a map, one can not remove or add new items, but | ||
existing items can be modified. |
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 needed to implement an object with the same semantics as Pod.
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.
xref #1099 (comment) and #1099 (comment) for more detail on how Pod immutability can be supported
I think we can get by with just one "selection" behavior, if we want to keep it simple. That behavior is:
|
Agreed, that makes a lot of sense to me. |
The above semantics supports the Pod/Containers use case, where you want to allow some fields to be updated on list items, but don't allow adding list items. The Deployment use case is this: you want to have a PodTemplate where everything is mutable. Today this is handled by having a separate copy of the type definition, which has lacks |
|
||
### Semantics | ||
|
||
We'll define immutable as "writeOnce", which means that the fields can only be |
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.
Prior art, same idea but they opted to name the concept "CreateOnly" which I personally found non-confusing.
I'm interested in immutable fields in CRDs as well. Would it be simpler to do something like:
instead? That seems simpler to implement to me, less to document, anything that's immutable, put in there. Seems like this could be implemented rather uniformly. |
@apelisse Would it be okay to add the two main open issues (equality, recursive) to this PR and merge it as provisional? We can open followups to resolve those, hopefully this week. |
Sure, I'll update the KEP |
1c38e42
to
fa497d8
Compare
Replaced by #1265 |
No description provided.