-
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
api-machinery: KEP for immutable fields #1265
Conversation
rejected. No mutation is performed at all. | ||
- Ignored: mutation of the field (and/or sub-fields) are completely ignored, and | ||
not persisted. They do not trigger an error. | ||
means that the whole `.foo` object can be removed if set, or set if it is unset, but |
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.
allowing a delete and a recreate with different values defies my expectations. This is consistent with how it's treated for objects in some way? Seems weird to allow removing entire stanzas to remove values. Is this to make it work nicely with unions?
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 this is what @deads2k is already alluding to, but how is a delete and insert identified differently from an update of an entry in-place? It seems like this sort of thing would only be possible if using a list that is keyed on, e.g. a name
field. Even then, I agree it seems like an odd thing to permit as from a users perspective, they now just have to perform two API operations instead of one in order to update certain fields.
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 get your point. tl/dr: you question the sense of pure removal/addition without identifier. I don't object.
What about string slices? Is the identifier implicitly the value and we would allow a deletion and an insertion in a special mode, but no order changes?
Note, we are very flexible what to allow. The big question is what we really want to offer in Kubernetes-like APIs defined through CRDs.
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 your two question are actually the same. I fixed what I think @deads2k pointed out. My example was wrong. Specifying x-kubernetes-immutable
applies to the field itself, not only the sub-fields in case of an object.
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 really good. Added a couple suggestions and questions.
is rejected because of immutability conflict. | ||
- the mechanism must extend to | ||
- the addition of protobuf or other encodings which unify values like empty, null and undefined. | ||
- the use for existing native types in order to replace complex validation code with a simple declarative marker on the types. |
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.
@erictune mentioned a requirement that this proposal supports with but might be worth calling in the goals here: create requests that set immutable fields must be idempotent. It highlights the importance of handling equality correctly.
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.
do you have an example?
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.
Here's the original comment: #1099 (comment)
It's got an example in it.
properties: | ||
name: | ||
type: string | ||
x-kubernetes-immutable: true |
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 sure I understand what this means
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.
We can move these up to the items
level. Both are equivalent.
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, there is a slight difference between both:
type: object
nullable: true
properties:
x:
type: string
x-kubernetes-immutable: true
allows a change from {x: something}
to null
or undefined (when used as a field value), but
type: object
x-kubernetes-immutable: true
nullable: true
properties:
x:
type: string
does not allow that.
Without nullable
and as array items, both coincide.
7a2f63c
to
b28bfe2
Compare
// no new containers can be added, no existing container can be removed. | ||
// +immutable | ||
Containers []Containers | ||
``` |
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.
Suggestion: Recommend a marker syntax for maps and map-slices in Go.
Possible syntax:
// Okay to change any way
A map[string]string
// Can't touch this.
// +immutable
B map[string]string
// Allowed addition and deletion.
// +immutable-map-values <--- not sure how to express this in Go.
C map[string]string
// Allowed addition and deletion.
// +listType=map
// +listMapKey=name
// +immutable-map-values
D []SomeType
// Allowed existing value modification.
// +immutable-keys
E map[string]string
// Allowed existing value modification.
// +listType=map
// +listMapKey=name
// +immutable-keys
F []SomeType
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 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.
@erictune there is a discussion (for quite some time) about how to express properties of items and map values. Am not sure this is so different here, at least it is overlapping.
I'd like to have general +kubebuilder:validation:items:<some-marker>
and +kubebuilder:validation:values:<some-marker>
syntax.
traditional JSON format used on-the-wire and for storage. | ||
|
||
When adding protobuf encoding to CRDs in the future, we have to (without major, non-standard | ||
efforts) identify unset, empty and null for CRs as well. This leads to the idea to use a less |
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 an alternative.
We could generate proto files and proto descriptors from the types.go
. A map[string]string
could turn into a map<StringOrEmptyOrNull, StringOrEmptyOrNull>
.
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.
How unusual would such an encoding be (outside of kube-proto) ?
We could derive the right proto schema from the specified normalization behaviour. But then we come to the question of compatibility if you change normalization at a later point.
We should start early to flesh out proto targetting 1.18. There seem to be so many details to get right.
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.
Proto's standard libraries define something like this which is used for proto's JSON support:
https://github.com/protocolbuffers/protobuf/blob/master/src/google/protobuf/struct.proto
We'd basically be defining something similar.
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.
Ic, just another sum type (oneof). The question will be whether we then model something in Proto that was just an accident by our use of JSON initially. With JSON we have no choice to get null and undefined as separate values, with proto we could fix our accident.
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.
With JSON we have no choice to get null and undefined as separate values, with proto we could fix our accident.
We need to find a good balance between the "API semantics make sense", "APIs have too many knobs" and "consistency between CRD and built-in types" trade-off.
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.
Some further thoughts for proto and normalization:
- We already have
nullable
to explicitly allow nulls. - With native types we have
omitempty
which maps null and empty to undefined.
-
In the case of non-nullable, we only have two values. Without normalization we get:
undefined -> undefined empty -> empty
By using a default, we can get:
undefined -> empty empty -> empty
Omitempty would give us:
undefined -> undefined empty -> undefined
Hence, if we had
x-kubernetes-omitempty: true
we would have covered all variants we know
today with native types. -
With nullable and no normalization we get
undefined -> undefined null -> null empty -> empty
With defaults, we are able to express:
undefined -> null null -> null empty -> empty
With omitempty, we would get:
undefined -> undefined null -> undefined empty -> undefined
What we miss is:
undefined -> empty null -> empty empty -> empty
I question that we really need this at all. If we identity empty in proto with undefined in JSON,
x-kubernetes-omitempty: true
would be enough.
In other words, we maybe only need x-kubernetes-omitempty: true
and cover all interesting cases.
/lgtm |
I got my comments from slack and in the PR addressed. /approve holding until monday PM for @lavalamp to have a pass if he wishes. |
3206c49
to
cb44caa
Compare
/lgtm |
[APPROVALNOTIFIER] This PR is APPROVED This pull-request has been approved by: deads2k, erictune, sttts The full list of commands accepted by this bot can be found here. The pull request process is described here
Needs approval from an approver in each of these files:
Approvers can indicate their approval by writing |
pod, it must be deleted, recreated and rescheduled. Users want to implement the | ||
same kind of read-only semantics for CustomResources, for example: | ||
https://github.com/kubernetes/kubernetes/issues/65973. Today this is only possible | ||
with the unreasonable development overhead of a webhook. |
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 have a vague memory of an immutable-once-set semantic we wanted at one point (would enable an async allocator to set a field once via the API, after which it would be locked). Was that discarded or deferred? Not having that means any immutable field must be set synchronously on create by the object originator or a webhook.
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.
Am not aware of that old discussion, but it definitly makes sense to flesh that out.
It sounds like you want a way to declare a struct not completely immutable, but stop it from being deleted, and certain fields to be set-once-only:
- This is more than marking fields immutable, because you could still set them to undefined.
- It is less than marking the whole struct's keys immutable because not all fields might be set only.
In other words, this is some weaker form ofx-kubernetes-immutable-keys
on the hosting struct, let's call itx-kubernetes-set-once-keys
:
Example:
apiVersion: setonce/v1
kind: Once
spec:
once: true
name: foo
with
type: object
x-kubernetes-immutable-keys: true
properties:
spec:
type: object
x-kubernetes-set-once-keys: ["once","name"]
properties:
once:
type: boolean
name:
type: string
x-kubernetes-immutable: true
Here, name
can only be set to a fixed value once, and never changed anymore. once
can be set once (i.e. from undefined to some value), but the actual value can be changed any 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.
@liggitt Did you have an actual use-case in mind? I can't remember talking about that either, and I'd really like to keep the number of knobs low if we can.
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.
Did you have an actual use-case in mind?
We use this capability in built-in resources today:
- scheduler setting spec.nodeName (and it being immutable after that point)
- populating clusterIP in services
- append-only lists (like ephemeralContainers)
- remove-only lists (like finalizers that may only shrink after deletion)
Talked through a lot of scenarios with @sttts, and I think the API guideline to think twice about boolean use probably applies here. Modeling restrictions on key/value immutability as an enum rather than a boolean allows more use cases to avoid requiring a validating webhook. Something like:
x-kubernetes-mutability: Immutable | AddOnly | RemoveOnly
x-kubernetes-key-mutability: Immutable | AddOnly | RemoveOnly
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.
scheduler setting spec.nodeName (and it being immutable after that point)
This is write-once, I agree (but not sure we need a general mechanism for this).
populating clusterIP in services
Is this actually write-once?
append-only lists (like ephemeralContainers)
append-only isn't quite the same as write-once.
remove-only lists (like finalizers that may only shrink after deletion)
IMO the "after deletion" qualifier definitely means that there's not going to be a general mechanism for this.
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.
scheduler setting spec.nodeName (and it being immutable after that point)
This is write-once, I agree (but not sure we need a general mechanism for this).
populating clusterIP in services
Is this actually write-once?
We set clusterIPs in-process in the service registry implementation, and do not allow it to change on update. To replicate these interactions without write-once capabilities, a custom resource would either have to:
- use a mutating webhook on create (which would require allocation to be really fast) and built-in immutability on the CR schema
- use a controller to allocate asynchronously and a validating webhook to prevent changing/unsetting a value
If we can allow custom resources to express invariants without hooking up dynamic logic, that seems valuable.
append-only isn't quite the same as write-once
You can accomplish append-only with a combination of key-mutability: AddOnly
on the container, and mutability: Immutable
on the individual items.
IMO the "after deletion" qualifier definitely means that there's not going to be a general mechanism for this.
I agree the AddOnly case is stronger than RemoveOnly.
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 we can allow custom resources to express invariants without hooking up dynamic logic, that seems valuable.
Yes, the question isn't if it is valuable, the question is how valuable, to how many users, and at what cost, where cost is initial implementation cost (amortized) + ongoing maintenance cost + ongoing API complexity cost to users and us
One question about immutable-once-set, and one comment about the direct-to-beta proposal. LGTM otherwise. |
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.
Sorry these comments are late.
- I recommend removing the future-looking sections (proto normalization, add/delete).
- I would like to see more justification for the immutable keys rather than not supporting this feature or supporting it by permitting nested types to turn off immutablity.
- extend the CRD API to be able to specify immutability for fields. | ||
- publish the immutability field of CRDs via OpenAPI as vendor extension. | ||
- verify immutability on CR update and patch requests. | ||
- propose a source code marker to be consumed by kubebuilder and openapi-gen. |
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.
and our built in tooling? Does openapi-gen cover that?
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.
yes, openapi-gen will produce the immutable tags as vendor extensions in the go-openapi definition we compile in. SSA will get them through the schema passed into the handlers.
- the mechanism must extend to | ||
- the addition of protobuf or other encodings which unify values like empty, null and undefined. | ||
- the use for existing native types in order to replace complex validation code with a simple declarative marker on the types. | ||
- the restriction of the equality to only map keys, but not their 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.
why?
We propose | ||
|
||
1. adding boolean vendor extensions to CRD OpenAPI schemas named `x-kubernetes-immutable` and `x-kubernetes-immutable-keys` with `true` as the only valid value. | ||
2. do **strict deep-equal** comparison of those fields marked as immutable during |
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.
Now that I have read the bottom, I suggest you revise this to consider unset, empty, and 0-length items to be the same.
`never-change-after-creation` semantics. | ||
- Allowing `false` as value for `x-kubernetes-immutable: false` was considered to | ||
disable immutability imposed by a parent node. This complicates the semantics | ||
considerably and can be expressed with a combination of `x-kubernetes-immutable-keys` |
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 sentence contradicts itself. If you can already express it, the system can mechanically convert one expression to the other, the semantics must already be the same.
- OpenAPI has a notion of `readOnly`. This is meant to restrict fields to be set | ||
only in responses, not in a request payload. This does not match our | ||
`never-change-after-creation` semantics. | ||
- Allowing `false` as value for `x-kubernetes-immutable: false` was considered to |
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 this would be much less surprising for humans making specs.
Continuous #1099