-
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
Create KEP for built-in Defaulting #1928
Conversation
/assign @sttts |
e2262d9
to
c85ce91
Compare
### 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 |
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.
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.
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.
Oh, also, How malformed default tags are detected and reported back to the developer is an interesting detail.
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, 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.
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.
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.
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 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.
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 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.
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.
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.
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, that's a very good comment. I've added a section that addresses this. I'll try to provide more 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.
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.
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 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.
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'd love to see motion on this!
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 |
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 guess I would have expected this to actually just generate those functions, rather than be a separate mechanism?
There are multiple challenges to running defaults declaratively and | ||
changing the current mechanisms: | ||
|
||
- We don't use OpenAPI to drive validation nor defaulting |
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 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.
0efa992
to
834fa72
Compare
# 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. |
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.
@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.
|
||
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. |
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.
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.
@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. |
I don't think they are specifically a problem: https://github.com/kubernetes/enhancements/pull/1928/files#diff-f3bb704316dddab7a3f5f1bb6b5b7a50R401 |
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 |
/hold cancel Happy to address feedback as needed even after this is merged. |
type: string | ||
default: default-name | ||
defaulted: | ||
type: boolean |
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.
it's an int above, not a bool.
properties: | ||
invalid: | ||
type: string | ||
default: default-name |
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 invalid zero value? Where is it a) in the type above and b) here in OpenAPI.
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 this OpenAPI spec ""
is a completely valid value.
I disagree. At least there is a major part missing in the KEP. Please add that. Without I question the status "implementable". |
agree it should not be marked implementable with TBD approvers and unresolved questions |
// +default=0 | ||
Number int | ||
} | ||
``` |
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 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 | ||
} | ||
``` |
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.
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 |
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.
Can we spec WRT whitespace and escapes? Can I have a value with a newline in it? Tab? Space?
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.
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: |
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.
Forbidden or ignored? What actually happens if someone specifies this? Will CI fail?
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.
Forbidden. It will fail.
if obj.Name == "" { | ||
obj.Name = "default-name" | ||
} | ||
if obj.Number == 0 { |
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 should be able to elide this and replace it with a comment
|
||
### Graduation Criteria | ||
|
||
As an internal feature, this will go straight to stable. |
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 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.
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 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
}
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.
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.
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.
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.
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.
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
.
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.
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
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.
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.
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.
Good point, I think we should.
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 Alternately, if we thing that quoted strings we could later add |
I don't think we'll ever do |
Famous last words.
…On Tue, Oct 20, 2020 at 12:17 PM Antoine Pelisse ***@***.***> wrote:
I don't think we'll ever do +default=<function> though :-)
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#1928 (comment)>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/ABKWAVDHDQCS6BMAAQHVV73SLXO5TANCNFSM4PW562FA>
.
|
|
just wanted to point out that the
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 |
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". |
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.