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

sig-api-machinery KEP: Defaulting for Custom Resources #1006

Merged
merged 1 commit into from
May 2, 2019

Conversation

sttts
Copy link
Contributor

@sttts sttts commented Apr 26, 2019

This topic is unblocked by #1002.

@k8s-ci-robot k8s-ci-robot added the cncf-cla: yes Indicates the PR's author has signed the CNCF CLA. label Apr 26, 2019
@k8s-ci-robot k8s-ci-robot added size/L Denotes a PR that changes 100-499 lines, ignoring generated files. 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 Apr 26, 2019
@sttts
Copy link
Contributor Author

sttts commented Apr 26, 2019

@sttts sttts force-pushed the sttts-defaulting branch 3 times, most recently from 3cfa64d to 2fd2eef Compare April 26, 2019 10:49
@sttts sttts changed the title Sig-API-Machinery KEP: Defaulting for Custom Resources sig-api-machinery KEP: Defaulting for Custom Resources Apr 26, 2019
Copy link
Member

@apelisse apelisse left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for writing this Stefan. I'd just really like native types and CRD to look and feel very similar. You're not mentioning how kubebuilder would integrate with this feature (I think that could be useful).

keps/sig-api-machinery/20190426-crd-defaulting.md Outdated Show resolved Hide resolved
keps/sig-api-machinery/20190426-crd-defaulting.md Outdated Show resolved Hide resolved
@sttts
Copy link
Contributor Author

sttts commented Apr 27, 2019

@apelisse added a sentence about kubebuilder. It just needs another tag to define the default. Should be super straight forward. /cc @DirectXMan12

Copy link
Contributor

@DirectXMan12 DirectXMan12 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

couple of comments inline, otherwise big 👍 from me

keps/sig-api-machinery/20190426-crd-defaulting.md Outdated Show resolved Hide resolved
@sttts
Copy link
Contributor Author

sttts commented Apr 30, 2019

@DirectXMan12 addressed your comment.

keps/sig-api-machinery/20190426-crd-defaulting.md Outdated Show resolved Hide resolved
keps/sig-api-machinery/20190426-crd-defaulting.md Outdated Show resolved Hide resolved

![Decoding steps which must apply defaults](20190426-crd-defaulting-pipeline.png)

We rely on the validation steps in the request pipeline to verify that the default value is of the right type.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would it be practicable to validate the default value at CRD create/update time? If possible, I'd like to avoid persisting fundamentally flawed CRDs

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With structural schemas, yes. We can validate them in advance.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

More precisely we can verify the types, not value validation. So there will always be a chance that the default does not fullfil the later. But as both validation and defaults are under control of the same party, we should be fine.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added


We rely on the validation steps in the request pipeline to verify that the default value is of the right type.

The `default` field in the CRD types is considered alpha quality. We will add a `CustomResourceDefaulting` feature gate. Values for `default` will be rejected if the gate is not enabled.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if we anticipate this field being able to be populated by default in 1.16, we must not fail validation if we encounter this in existing objects. we would need to follow a process similar to https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api_changes.md#alpha-field-in-existing-api-version

Since CRD validation has already been rejecting this field, the only tweak would be "Before persisting the object to storage, reject the disabled alpha field on create, and on update if the existing object does not already have a value in the field."

The important thing is that we allow/preserve data in the Default field on update if the existing object had data in that field, even if the alpha feature gate is disabled

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sounds good

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added ratcheting validaton

keps/sig-api-machinery/20190426-crd-defaulting.md Outdated Show resolved Hide resolved
type: array
items:
type: integer
default: [1]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I went looking for the footnote...
...
it's been a long day

keps/sig-api-machinery/20190426-crd-defaulting.md Outdated Show resolved Hide resolved
keps/sig-api-machinery/20190426-crd-defaulting.md Outdated Show resolved Hide resolved

We do this in the serializer by passing a real defaulter to [`versioningserializer.NewCodec`](https://github.com/kubernetes/apimachinery/blob/master/pkg/runtime/serializer/versioning/versioning.go#L49) such that defaulting is done natively just after the binary payload has been unmarshalled into an `map[string]interface{}` and pruning of [KEP: Pruning for CustomResources](https://github.com/kubernetes/enhancements/pull/709) was done, compare the yellow boxes in the following figure:

![Decoding steps which must apply defaults](20190426-crd-defaulting-pipeline.png)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I hard a hard time telling from the diagram, what about after reading the response from a conversion webhook?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Like for native types: no defaulting after conversion.

Copy link
Member

@liggitt liggitt May 1, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is persisted into etcd in this scenario?

  • crd has two versions (v1, v2)
  • v1 defaults field a to 1
  • v2 defaults field b to 2
  • v2 is the storage version
  • user submits v1 object with a and b unset

Clearly, a defaults to 1 as part of deserialize->default->validate of the user's request

What is less clear to me is if v2 defaulting (setting b to 2) is applied after conversion, before storing in etcd.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oops, crossed wires. that answered my question, thanks.

Copy link
Member

@liggitt liggitt May 1, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

probably worth calling out explicitly (maybe even with that scenario). the v2 defaulting would get applied on the way out of storage, so from an API user's perspective, I think they would see the newly created object returned with both a:1, b:2 set, but only a:1 would be in etcd, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

made this explicit:

Like for native resources, we do defaulting

* during request payload deserialization
* after mutating webhook admission
* during read from storage.

Note: like for native resources, we do not default after webhook conversions. Hence, webhook conversions must be complete in the sense that they return defaulted objects. Technically we could do defaulting, but to match native resources, we do not.

2. recursively follow the given CustomResource instance and the structural schema, applying defaults where an object field is
* undefined (`_, ok := obj[field]; !ok`)
* `nil` if the field not nullable
* empty in case of lists and maps, and if nullable is not set.
Copy link
Member

@liggitt liggitt May 1, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in case of lists

clarify how we determine this.

  • type: "array" in the schema?
  • _, ok := default.([]interface{})?
  • _, ok := value.([]interface{})?

all of the above?

and maps

clarify how we determine this.

  • type: "object" in the schema?
  • _, ok := default.(map[string]interface{})?
  • _, ok := value.(map[string]interface{})?

all of the above?

I mostly want to make sure defaulting doesn't replace an empty array or object with a default value of the correct type prior to validation and mask what should be reported to the user as a schema validation error

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Made the cases explicit as you wrote down above.

Note: we could do type validation during pruning, i.e. as part of the deserialization process, to match native types.

@k8s-ci-robot k8s-ci-robot removed this from the v1.15 milestone May 1, 2019

[Kubebuilder's crd-gen](https://github.com/kubernetes-sigs/controller-tools/tree/master/cmd/crd) can make use of this feature by adding another tag, e.g. `// +default=<arbitrary-json-value>`. Defaults are arbitrary JSON values, which must also validate and are not subject to pruning (defaulting happens after pruning). This is an implicit assumption that will be checked by the apiserver.
[Kubebuilder's crd-gen](https://github.com/kubernetes-sigs/controller-tools/tree/master/cmd/crd) can make use of this feature by adding another tag, e.g. `// +default=<arbitrary-json-value>`. Defaults are arbitrary JSON values, which must also validate (types are checked during CRD creation and update, value validation is checked for requests, but not for etcd reads) and are not subject to pruning (defaulting happens after pruning).
Copy link
Member

@liggitt liggitt May 1, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is it possible to ensure the default value does not contain a field that would get dropped via pruning? this would help prevent typos. I'm willing to do more expensive things in CRD create/update validation to improve the user experience, as long as it falls out in a relatively straightforward way in the code

Copy link
Member

@liggitt liggitt May 1, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for a given JSONSchemaProps object, I was envisioning checks like this:

if props.Default != null {
  reflect.DeepEqual(
    props.Default,
    prune(
      props.Default,
      makeStructuralSchema(props),
      isPreservingUnknownFields, /* from parent schemas or CRD field */
    ),
  )

  validate(props.Default, ConvertJSONSchemaProps(props))
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, we can do that.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added

and for `array` type in the schema one of these:

* `if v, ok := obj[fld]; !ok` => default
* `else if !nullable && v == nil` => default
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if I'm reading this correctly, we'd want a nullable field to skip all the checks after the first one, right? something like this?

* `if v, ok := obj[fld]; !ok` => default
* `else if nullable` => no default
* `else if v == nil` => default
...

same for object

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@liggitt I read this as being very similar to what Stefan wrote, but harder to read?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if the schema specifies the field is nullable, and the current value is [], my example would not default, and stefan's would

I think we want to avoid treating nil and []/{} values differently for defaulting purposes

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh I see what happened here.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it's nil and non-nullable, shouldn't it be an error anyway?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's think in terms of UX behavior:

{
  defaulted-list: [] # Do I want this defaulted? Not sure, I'm specifying a value after all ...
}
---
{
  defaulted-list: null # Do I want this defaulted? Not sure, I'm specificying a value after all ...
}
---
{
  # defaulted-list should certainly be defaulted
}

One of the principle we used in apply was that if users specified something, then we assume that it's what they want, and setting something to nil is different from not setting it at all.

Copy link
Member

@liggitt liggitt May 2, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it's nil and non-nullable, shouldn't it be an error anyway?

That's a good point. We could default only if the property was completely absent.

Things I like about only defaulting unspecified properties:

  • It's simpler to explain
  • It's simpler to implement
  • It doesn't mess with user data in any way
  • It avoids masking schema errors if they explicitly send null for a property that is not nullable, even if it has a default

Opposing considerations:

  • The distinction between "null" and "absent" is not easily maintained in all serialization formats (notably protobuf), so if that got lost in a round-trip, we could end up applying defaulting anyway (this isn't a huge concern, given we currently maintain the distinction for custom resources, so I think we have to figure out a way to continue doing so if we ever switch to persist in other formats like proto)
  • We would not be able to replicate defaulting rules for built-in resources using this standard in a hypothetical world where built-in types get converted to CRDs (this also isn't a huge concern, given there are tons of defaulting rules for built-in types we couldn't replicate using openapi at all)
  • Sending explicit null values in some patch formats today removes fields, so there's some prior art for null == unset in kube. For example, kubectl patch ... --type=merge -p '{"spec":{"key":null}}' removes an existing key property, it does not persist a literal null value. I'm not sure what server-side apply does here.

Overall, I think the simpler and more intuitive behavior is probably what we want.

@liggitt
Copy link
Member

liggitt commented May 1, 2019

/lgtm

would like a follow-up clarifying #1006 (comment)

@k8s-ci-robot k8s-ci-robot added the lgtm "Looks good to me", indicates that a PR is ready to be merged. label May 1, 2019
@liggitt
Copy link
Member

liggitt commented May 1, 2019

cc @lavalamp @deads2k
for approval

@k8s-ci-robot k8s-ci-robot removed the lgtm "Looks good to me", indicates that a PR is ready to be merged. label May 1, 2019
@sttts sttts force-pushed the sttts-defaulting branch 2 times, most recently from cd531c7 to 8c96d68 Compare May 1, 2019 21:53
@sttts sttts force-pushed the sttts-defaulting branch 3 times, most recently from 4ba62c6 to 5ce436c Compare May 2, 2019 13:39
@liggitt
Copy link
Member

liggitt commented May 2, 2019

/lgtm

@k8s-ci-robot k8s-ci-robot added the lgtm "Looks good to me", indicates that a PR is ready to be merged. label May 2, 2019
@deads2k
Copy link
Contributor

deads2k commented May 2, 2019

Spoken with the stakeholders, we think this is ready.

/approve

@k8s-ci-robot
Copy link
Contributor

[APPROVALNOTIFIER] This PR is APPROVED

This pull-request has been approved by: deads2k, liggitt, 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 /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@k8s-ci-robot k8s-ci-robot added the approved Indicates a PR has been approved by an approver from all required OWNERS files. label May 2, 2019
@k8s-ci-robot k8s-ci-robot merged commit 7134f6e into kubernetes:master May 2, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
approved Indicates a PR has been approved by an approver from all required OWNERS files. 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 lgtm "Looks good to me", indicates that a PR is ready to be merged. 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

9 participants