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

Create KEP for built-in Defaulting #1928

Merged
merged 1 commit into from
Oct 5, 2020

Conversation

apelisse
Copy link
Member

@apelisse apelisse commented Aug 6, 2020

Create KEP for built-in defaulting, along with mechanisms so that defaulting can behave the same for CRDs.

Proposes the creation of a new default marker for built-in types, and align the default marker in kubebuilder.
That marker will decide what default field we end-up with in the openapi and how this flag will be used.

@k8s-ci-robot k8s-ci-robot added cncf-cla: yes Indicates the PR's author has signed the CNCF CLA. size/L Denotes a PR that changes 100-499 lines, ignoring generated files. labels Aug 6, 2020
@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. sig/architecture Categorizes an issue or PR as relevant to SIG Architecture. labels Aug 6, 2020
@apelisse
Copy link
Member Author

apelisse commented Aug 6, 2020

/assign @sttts

@apelisse apelisse force-pushed the kep-default branch 2 times, most recently from e2262d9 to c85ce91 Compare August 6, 2020 21:02
### Goals

The goal is to add a new `// +default` tag to our current built-in Go
IDL. That tag will be transformed into the OpenAPI `default` tag and
Copy link
Contributor

Choose a reason for hiding this comment

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

What are the formatting rules for the +default tag value? I'd expect it to match what OpenAPI defines, roughly, or what we have in CRDs today, but might be worth referencing the primary documentation for that here and commenting about any special considerations for putting the value into a godoc tag.

Copy link
Contributor

Choose a reason for hiding this comment

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

Oh, also, How malformed default tags are detected and reported back to the developer is an interesting detail.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, will solve the comment: we should probably take this exactly as the openapi takes it.
And yes, we can fail when we generate the openapi for the types.

Copy link
Contributor

Choose a reason for hiding this comment

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

The default tag in OpenAPI is not what you think it is. It's not "our default". We might want x-kubernetes-default instead.

Also note that we will never publish the default. But of course it's fine to get it into SSA via OpenAPi.

OpenAPI uses just json for the default. kubebuilder has some other, home-grown syntax.

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 talked to Solly, we changed the format to yaml so that it's easier to format (and still allows structures through its json form). kubebuilder would probably be able to change the format since it's a different marker name anyway.

The default tag in OpenAPI is not what you think it is. It's not "our default". We might want x-kubernetes-default instead.

Can you elaborate on how it's different?

This seems to be what defines a default: https://swagger.io/specification/v2/:

Declares the value of the item that the server will use if none is provided.

That seems to match with what I think we're doing.

Copy link
Contributor

Choose a reason for hiding this comment

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

There is one maybe ugly detail which makes it different to the default of our native types:

A default in a struct (= object in OpenAPI slang) for native types applies even if the whole struct is omitted (as long as the struct is not a pointer).

A default in a struct in the OpenAPI sense (and CRDs for that matter) only applies when the struct / object is specified.

This leads to this kind of constructions to workaround the latter:

type: object
properties:
  foo:
    type: object
    properties:
      bar:
        type: string
        default: "abc"
    default: {}

In other words, the implicit default of objects is undefined.

At least we defined the default semantics of defaults for CRDs in a way, that the outer default {} is applied first, and then the value for bar is set. I.e. the effective default is {"foo": {"bar": "abc"}} here.

Copy link
Contributor

Choose a reason for hiding this comment

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

Problem with ^^: due to Golang unmarshal semantics, a default of a PodSepc in native types and a default of a PodSpec in a CRD will differ.

I would feel better if we operate on JSON in either case instead of generation of defaulters for native types with different semantics.

Copy link
Member Author

Choose a reason for hiding this comment

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

Thanks, that's a very good comment. I've added a section that addresses this. I'll try to provide more example.

Copy link
Member Author

Choose a reason for hiding this comment

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

At least we defined the default semantics of defaults for CRDs in a way, that the outer default {} is applied first, and then the value for bar is set. I.e. the effective default is {"foo": {"bar": "abc"}} here.

That saves our lives. I think that imposing a {} default on non-pointer structs solves that problem.

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 would feel better if we operate on JSON in either case instead of generation of defaulters for native types with different semantics.

I think if we follow the steps that I've specified now, operating on json or built-in types should be equivalent.

Copy link
Member

@lavalamp lavalamp left a comment

Choose a reason for hiding this comment

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

I'd love to see motion on this!

keps/sig-api-machinery/NNNN-built-in-default/README.md Outdated Show resolved Hide resolved
Since the goal of this proposal is to increase consistency with the CRD
defaulting, we propose to use the same mechanism (both in OpenAPI and
similar to what kubebuilder does). This defaulting will happen BEFORE
the existing defaulting functions are called (existing functions can be
Copy link
Member

Choose a reason for hiding this comment

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

I guess I would have expected this to actually just generate those functions, rather than be a separate mechanism?

keps/sig-api-machinery/NNNN-built-in-default/README.md Outdated Show resolved Hide resolved
keps/sig-api-machinery/NNNN-built-in-default/README.md Outdated Show resolved Hide resolved
keps/sig-api-machinery/NNNN-built-in-default/README.md Outdated Show resolved Hide resolved
keps/sig-api-machinery/NNNN-built-in-default/README.md Outdated Show resolved Hide resolved
There are multiple challenges to running defaults declaratively and
changing the current mechanisms:

- We don't use OpenAPI to drive validation nor defaulting
Copy link
Contributor

Choose a reason for hiding this comment

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

Why mention validation? Validation is acting on internal types. That's too late. We have to get into the decoding step, i.e. it must be part of the scheme.

Comment on lines 261 to 271
# CRD Normalization

In order to have a consistent behavior between CRDs and built-in, the
following two things need to happen:
1. Using the flag with CRDs requires the present of a
`x-kubernetes-remove-nulls` flag (automatically set when using defaults
in built-in types). When present, the apiserver will remove all null
values from CRDs before defaulting, after validation, on all updates.
2. Non-pointer structs will, in both kubebuilder and for built-in types,
disallow the use of `+default` and force a value of `{}`, since this is
automatically done by Go deserialization.
Copy link
Member Author

Choose a reason for hiding this comment

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

@thockin @DirectXMan12 This is a much simpler version of the normalization solution that I was looking for. I'll try to add additional examples here, as discussed with Solly, to show that this makes sense.

@k8s-ci-robot k8s-ci-robot added size/XL Denotes a PR that changes 500-999 lines, ignoring generated files. and removed size/L Denotes a PR that changes 100-499 lines, ignoring generated files. labels Sep 22, 2020

Forcing the "one-line" subset allows us to sidestep a lot of the YAML
weirdness in syntax, but we can still just use a YAML parser to parse
the expressions if we want.
Copy link
Contributor

Choose a reason for hiding this comment

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

a detail: null is allowed in default values?

Also already mentioned above somewhere: we should only allow validating values. We will see how clever the generator and kubebuilder can be made to check that (best effort, doesn't have to be perfect). CRDs will fails to be created if the value does not validate.

@sttts
Copy link
Contributor

sttts commented Oct 2, 2020

@apelisse This is much much better now.

One major gap I still see: CRDs need not only null removal, but also zero-value removal. Today's default semantics say that only unspecified values are defaulted.

@apelisse
Copy link
Member Author

apelisse commented Oct 5, 2020

One major gap I still see: CRDs need not only null removal, but also zero-value removal. Today's default semantics say that only unspecified values are defaulted.

I don't think they are specifically a problem: https://github.com/kubernetes/enhancements/pull/1928/files#diff-f3bb704316dddab7a3f5f1bb6b5b7a50R401

@lavalamp
Copy link
Member

lavalamp commented Oct 5, 2020

I'd like this to make the enhancements freeze and it's 99% there. If you could send a follow-up getting the remaining comments addressed, that'd be great, thanks.

/lgtm

@k8s-ci-robot k8s-ci-robot added the lgtm "Looks good to me", indicates that a PR is ready to be merged. label Oct 5, 2020
@apelisse
Copy link
Member Author

apelisse commented Oct 5, 2020

/hold cancel

Happy to address feedback as needed even after this is merged.

@k8s-ci-robot k8s-ci-robot removed the do-not-merge/hold Indicates that a PR should not merge because someone has issued a /hold command. label Oct 5, 2020
@k8s-ci-robot k8s-ci-robot merged commit 056a5f2 into kubernetes:master Oct 5, 2020
@k8s-ci-robot k8s-ci-robot added this to the v1.20 milestone Oct 5, 2020
type: string
default: default-name
defaulted:
type: boolean
Copy link
Contributor

Choose a reason for hiding this comment

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

it's an int above, not a bool.

properties:
invalid:
type: string
default: default-name
Copy link
Contributor

Choose a reason for hiding this comment

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

why invalid zero value? Where is it a) in the type above and b) here in OpenAPI.

Copy link
Contributor

Choose a reason for hiding this comment

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

With this OpenAPI spec "" is a completely valid value.

@sttts
Copy link
Contributor

sttts commented Oct 6, 2020

I don't think they are specifically a problem: https://github.com/kubernetes/enhancements/pull/1928/files#diff-f3bb704316dddab7a3f5f1bb6b5b7a50R401

I disagree. At least there is a major part missing in the KEP. Please add that. Without I question the status "implementable".

@liggitt
Copy link
Member

liggitt commented Oct 6, 2020

Without I question the status "implementable".

agree it should not be marked implementable with TBD approvers and unresolved questions

// +default=0
Number int
}
```
Copy link
Member

Choose a reason for hiding this comment

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

We could even go one step further and disallow non-{} defaults for structs.

That would be a lot simpler and avoid the same field getting different defaults specified for it at different levels, like this:

properties:
  foo:
    type: object
    default: {"bar":{"baz":1}}
    properties:
      bar:
        type: object
        default: {"baz":2}
        properties:
          baz:
          type: number
          default: 3

Would that restriction break any use cases we care about?

// +default=0
Number int
}
```
Copy link
Member

Choose a reason for hiding this comment

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

Limiting the defaults of complex types (objects and arrays) to single-level values ({} and []) would make generating the defaulting functions significantly easier.

// +default=32
Integer int

// +default=bar
Copy link
Member

Choose a reason for hiding this comment

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

Can we spec WRT whitespace and escapes? Can I have a value with a newline in it? Tab? Space?

Copy link
Member Author

@apelisse apelisse Oct 20, 2020

Choose a reason for hiding this comment

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

https://play.golang.org/p/kxNxW48GJ7Z

package main

import (
	"encoding/json"
	"fmt"
)

func main() {
	var i interface{}
	err := json.Unmarshal([]byte(`{" ": "tab:\t\nspace: "}`), &i)
	if err != nil {
		panic(err)
	}
	fmt.Println(i)
        // Output: map[ :tab:	
        // space: ]
}

On the other hand, this is forbidden:
```golang
Type Root struct {
// Defaults on non-pointer structs are FORBIDDEN:
Copy link
Member

Choose a reason for hiding this comment

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

Forbidden or ignored? What actually happens if someone specifies this? Will CI fail?

Copy link
Member Author

Choose a reason for hiding this comment

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

Forbidden. It will fail.

if obj.Name == "" {
obj.Name = "default-name"
}
if obj.Number == 0 {
Copy link
Member

Choose a reason for hiding this comment

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

we should be able to elide this and replace it with a comment


### Graduation Criteria

As an internal feature, this will go straight to stable.
Copy link
Member

Choose a reason for hiding this comment

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

I think the graduation criteria should be that all eligible manually-written default functions are converted to this mechanism and the manual code deleted. That would leave only "weird" cases in */defaults.go.

Copy link
Member

Choose a reason for hiding this comment

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

if dynamic defaulting leaks into user manually-written defaults, it might be a bit more complex to replicate this in static markers. @apelisse did you discuss this case in the KEP / comments; i may have missed it?

func myDefaultingFunc(obj *SomeObject) {
	if obj.foo == 0 {
		obj.foo = 2
	}
	obj.bar = obj.foo - 1
}

Copy link
Member Author

@apelisse apelisse Oct 20, 2020

Choose a reason for hiding this comment

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

Right, that's why the "declarative" marker defaults will be applied after custom defaulting logic.

In your example, if there is a declarative marker for foo or bar, neither will be applied since they both have 2 and (that defaulting function is weird since bar is always set ;-) 1 set respectively.

Copy link
Member Author

@apelisse apelisse Oct 20, 2020

Choose a reason for hiding this comment

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

Also, clearly, declarative markers won't be able to have this kind of logic, that's on purpose, more often than not, you're doing something wrong if you need this.

Copy link
Member

@neolit123 neolit123 Oct 20, 2020

Choose a reason for hiding this comment

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

Right, that's why the "declarative" marker defaults will be applied after custom defaulting logic.

interesting, i would have expected the dynamic (custom) defaulting to apply after the static defaulting from markers.

In your example, if there is a declarative marker for foo or bar, neither will be applied since they both have 2 and (that defaulting function is weird since bar is always set ;-) 1 set respectively.

i guess i've tried to showcase that bar's defaulting may solely depend on the value of foo.

Copy link
Member Author

Choose a reason for hiding this comment

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

interesting, i would have expected the dynamic (custom) defaulting to apply after the static defaulting from markers.

I haven't been able to think of an example where it would be better one way or another, but this is possibly simpler to reason about. What's left unspecified after the custom function will have the declarative value applied.

i guess, i've tried to showcase that bar's defaulting may solely depend on the value of foo.

Yes, that's what I understood. But we're specifically not allowing this kind of cross-field defaulting

Copy link
Member

@neolit123 neolit123 Oct 20, 2020

Choose a reason for hiding this comment

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

Also, clearly, declarative markers won't be able to have this kind of logic, that's on purpose, more often than not, you're doing something wrong if you need this.

hm, if so, there has to be a way to alert authors of new APIs about similar bad practices (if considered bad practices). potentially in documentation.

Copy link
Member Author

Choose a reason for hiding this comment

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

Good point, I think we should.

@thockin
Copy link
Member

thockin commented Oct 20, 2020

For the record, I am a HUGE proponent of this direction and I think we eventually need a proper IDL to do justice to the set of problems we have here. I am eager to see what is left after we move all the trivial cases to this mechanism.

One though - As defined, this leaves little room for any other "smart" defaults. E.g. if I want to say the default value for field X is the same as field Y, I might say +default=util.SameAs("Y") - but that is a syntactically valid string, as per this spec. If we require more strict syntax (e.g. quoted strings) this is no longer ambiguous, and we have room to maneuver later.

Alternately, if we thing that quoted strings we could later add +default-fn=util.SameAs("Y") and say that default and default-fn are mutually exclusive.

@apelisse
Copy link
Member Author

I don't think we'll ever do +default=<function> though :-)

@thockin
Copy link
Member

thockin commented Oct 22, 2020 via email

@sttts
Copy link
Contributor

sttts commented Oct 23, 2020

+default=<some-javascript>, awesome idea. I heard some cloud native components now have web assembly embedded.

@neolit123
Copy link
Member

just wanted to point out that the --api-audiences flag of the kube-apisserver appears to exhibit this "bound defaulting" we discussed here:
#1928 (review)

Identifiers of the API. The service account token authenticator will validate that tokens used against the API are bound to at least one of these audiences. If the --service-account-issuer flag is configured and this flag is not, this field defaults to a single element list containing the issuer URL.

https://kubernetes.io/docs/reference/command-line-tools-reference/kube-apiserver/

so if the kube-apiserver one day has a configuration file, the defaulting of the api-audiences field would technically depend on the value of the service-account-issuer field.

@lavalamp
Copy link
Member

+default=<some-javascript>, awesome idea. I heard some cloud native components now have web assembly embedded.

I'm current a big -1 on this, we need to have a long conversation if we want to make defaults anything other than "plain old data".

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. sig/architecture Categorizes an issue or PR as relevant to SIG Architecture. size/XL Denotes a PR that changes 500-999 lines, ignoring generated files.
Projects
None yet
Development

Successfully merging this pull request may close these issues.