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

Initial KEP for immutable fields #1099

Closed
wants to merge 10 commits into from

Conversation

apelisse
Copy link
Member

No description provided.

@k8s-ci-robot k8s-ci-robot added the cncf-cla: yes Indicates the PR's author has signed the CNCF CLA. label Jun 12, 2019
@k8s-ci-robot
Copy link
Contributor

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by: apelisse
To complete the pull request process, please assign deads2k
You can assign the PR to them by writing /assign @deads2k in a comment when ready.

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 /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@apelisse
Copy link
Member Author

/assign @deads2k @liggitt @erictune

@k8s-ci-robot k8s-ci-robot added the size/L Denotes a PR that changes 100-499 lines, ignoring generated files. label Jun 12, 2019
@apelisse
Copy link
Member Author

/cc @jennybuckley @kwiesmueller

@k8s-ci-robot k8s-ci-robot added kind/kep Categorizes KEP tracking issues and PRs modifying the KEP directory sig/api-machinery Categorizes an issue or PR as relevant to SIG API Machinery. labels Jun 12, 2019
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.
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 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.

Copy link
Member Author

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?

Copy link
Contributor

@sttts sttts Jun 18, 2019

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.

Copy link
Contributor

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.

Copy link
Member Author

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.

Copy link
Contributor

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
null undefined
empty 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

Copy link
Contributor

@jpbetz jpbetz Sep 27, 2019

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

Copy link
Contributor

@sttts sttts Sep 30, 2019

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.

Copy link
Contributor

@sttts sttts Sep 30, 2019

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.

Copy link
Contributor

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:
Copy link
Member Author

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.

@erictune
Copy link
Member

Is it a goal that this could, in theory, be used to annotate the Pod type?
If so, it looks like recursive is insufficient.

I think we'd want to do:

type PodSpec struct {
...
// No new items can be added, but mutable fields of items can be modified.
// +immutable=keys 
Containers []Container
...
}

type Container struct {
	// +immutable
	Name string `json:"name" protobuf:"bytes,1,opt,name=name"`

         // Can be updated.  
	Image string `json:"image,omitempty" protobuf:"bytes,2,opt,name=image"`
        ...
}

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.
Copy link
Member

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.
Copy link
Member

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.

Copy link
Contributor

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

@erictune
Copy link
Member

I think we can get by with just one "selection" behavior, if we want to keep it simple. That behavior is:

  • +immutable tag on a scalar FieldDecl means that field is immutable.
  • +immutable tag on a map of list FieldDecl means that the set of map keys cannot change. Each item is validated separately, comparing same keys of old/new. This tag does not affect item validation.
  • +immutable tag on a list FieldDecl means that that the list length cannot change. Each item is validated separately, comparing same keys of old/new. This tag does not affect item validation.

@apelisse
Copy link
Member Author

Agreed, that makes a lot of sense to me.

@erictune
Copy link
Member

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.
It also supports where you want to allow new 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 +immutable specifiers. I think this is a good-enough pattern for other Deployment-like use cases, and so there is no need for special support for this case (like "Recursive with addition and/or deletion").


### Semantics

We'll define immutable as "writeOnce", which means that the fields can only be
Copy link
Member

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.

@michaelgugino
Copy link

I'm interested in immutable fields in CRDs as well.

Would it be simpler to do something like:

spec:
  immutableFields:
  otherField
  ...

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.

@jpbetz
Copy link
Contributor

jpbetz commented Sep 30, 2019

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

@apelisse
Copy link
Member Author

Sure, I'll update the KEP

@k8s-ci-robot k8s-ci-robot added the needs-rebase Indicates a PR cannot be merged because it has merge conflicts with HEAD. label Oct 17, 2019
@k8s-ci-robot k8s-ci-robot removed the needs-rebase Indicates a PR cannot be merged because it has merge conflicts with HEAD. label Oct 17, 2019
@apelisse
Copy link
Member Author

apelisse commented Oct 17, 2019

Replaced by #1265

@apelisse apelisse closed this Oct 17, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
cncf-cla: yes Indicates the PR's author has signed the CNCF CLA. kind/kep Categorizes KEP tracking issues and PRs modifying the KEP directory sig/api-machinery Categorizes an issue or PR as relevant to SIG API Machinery. size/L Denotes a PR that changes 100-499 lines, ignoring generated files.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet