⚠️ Simplify webhook wiring with Defaulter and Validator interfaces#328
⚠️ Simplify webhook wiring with Defaulter and Validator interfaces#328k8s-ci-robot merged 5 commits intokubernetes-sigs:masterfrom
Conversation
cbd3775 to
81c3025
Compare
| if wh == nil { | ||
| continue | ||
| } | ||
| s.Register("path", wh) |
There was a problem hiding this comment.
We can generate the path using object's GVK and name. And, the default path generation should probably be shared with controller-tools so that generator is in sync with this code ?
| once sync.Once | ||
| // For registers defaulting and validation webhooks for the provided types that implement either | ||
| // webhooktuil.Validator or webhookutil.Defaulter. | ||
| func (s *Server) For(objects ...runtime.Object) error { |
There was a problem hiding this comment.
For name itself doesn't convey the intent. Wondering if we should give it a more friendly name like EnableValidationFor or EnableDefaultsFor ?
81c3025 to
5264408
Compare
|
Addressed comments. |
droot
left a comment
There was a problem hiding this comment.
The change looks good, have a few comments.
| out := new(FirstMateStatus) | ||
| in.DeepCopyInto(out) | ||
| return out | ||
| } |
There was a problem hiding this comment.
I have seen these example projects add too much noise to the repo and introduce additional maintenance burden. Is there a way to avoid it ?
| if err != nil { | ||
| return nil, err | ||
| } | ||
| err = svr.EnableValidationFor(blder.mgr.GetScheme(), blder.apiType) |
There was a problem hiding this comment.
Won't svr have access to the scheme ? If yes, then we can get rid of scheme parameter from EnableDefaultsFor and EnableValidationFor.
There was a problem hiding this comment.
EnableValidationFor happens at setup time. But the setup time is earlier than the inject time.
But I just realized that there is another way to do it.
| return err | ||
| } | ||
| if len(gvks) != 1 { | ||
| return fmt.Errorf("expected only GVK returned by scheme.ObjectKinds") |
There was a problem hiding this comment.
we should some webhook related context to the error message and also include all the GVKs to help in debugging.
| s.Register("/validate-"+generatePath(gvks[0]), wh) | ||
| } | ||
| return nil | ||
| } |
There was a problem hiding this comment.
I am sort of wondering if we should enable validation/defaulting by default when webhook server starts ? (This will save people one step if they want to enable validation/defaulting). And I am thinking of doing the same thing for conversion.
5264408 to
8c836d7
Compare
|
Addressed comments. PTAL at the interfaces.
@DirectXMan12 WDYT |
droot
left a comment
There was a problem hiding this comment.
Looking good to me. Will defer it to @DirectXMan12 for the final LGTM because two issues that you pointed out still need to be resolved.
| // If we get a NotImplementDefaulterValidatorInterfacesError, we discord the Server instance. | ||
| // If other type of error, surface it. | ||
| if err == nil { | ||
| e := blder.mgr.Add(svr) |
There was a problem hiding this comment.
quick question: shouldn't this be done before calling svr.EnableWebhooksFor anyways to ensure mgr injects dependencies for webhooks (mainly scheme) ?
| func (h *mutatingHandler) InjectScheme(s *runtime.Scheme) error { | ||
| h.scheme = s | ||
| var err error | ||
| h.decoder, err = admission.NewDecoder(h.scheme) |
There was a problem hiding this comment.
+1 on setting decoder this way.
| var fmLog = logutil.Log.WithName("firstmate-reconciler") | ||
|
|
||
| // FirstMateController reconciles ReplicaSets | ||
| type FirstMateController struct { |
There was a problem hiding this comment.
name this something useful -- while "firstmatecontroller" is fun, it's not particularly descriptive
|
|
||
| appsv1 "k8s.io/api/apps/v1" | ||
| _ "k8s.io/client-go/plugin/pkg/client/auth/gcp" | ||
| "sigs.k8s.io/controller-runtime/crdexample/logutil" |
There was a problem hiding this comment.
lets switch these over to using the alias package (ctrl "sigs.k8s.io/controller-runtime") as opposed to a bajillion separate packages.
|
|
||
| // Decode decodes the inlined object in the AdmissionRequest into the passed-in runtime.Object. | ||
| func (d *Decoder) Decode(req Request, into runtime.Object) error { | ||
| func (d *Decoder) Decode(rawObj runtime.RawExtension, into runtime.Object) error { |
There was a problem hiding this comment.
add an additional method for this -- the plain old decode latest from admission request behavior is really nice.
| // neither the Defaulter nor the Validator interfaces. | ||
| // It returns a NotImplementDefaulterValidatorInterfacesError error if a provided object implements | ||
| // neither the Defaulter interface nor the Validator interface. | ||
| func (s *Server) EnableWebhooksFor(objects ...runtime.Object) error { |
There was a problem hiding this comment.
I'm not convinced this needs to be a method (as opposed to a helper or helpers). The helper makes it more composable, easier to test individually, and lets us actually have a type signature that requires an object that implements the interfaces (removing the error path in favor of compile-time checking).
|
|
||
| func generatePath(gvk schema.GroupVersionKind) string { | ||
| var pathItems []string | ||
| splittedGroup := strings.Split(gvk.Group, ".") |
There was a problem hiding this comment.
why aren't we just strings.Replace(gvk.Group, ".", "-") or something?
There was a problem hiding this comment.
Curret behavior doesn't include domain name in the path:
Group: apps.example.com
Kind: Foo
I will get /mutate-apps-foo instead of /mutate-apps-example-com-foo. IMO it's more concise, but there may be a small risk of collision. e.g. with
Group: apps.bar.com
Kind: Foo
I don't have a strong opinion about if we should have domain name.
Thoughts?
There was a problem hiding this comment.
I don't think conciseness here is valuable (only ever have to write this once, and it's usually going to be autogenerated), and I'd rather avoid collisions
| return nil | ||
| } | ||
|
|
||
| mwh := newDefaultingWebhookFor(object) |
There was a problem hiding this comment.
we should opportunistically merge validating and defaulting hooks into a single hook when possible, to avoid the extra round-trip
There was a problem hiding this comment.
we should opportunistically merge validating and defaulting hooks into a single hook when possible, to avoid the extra round-trip
This means you want to put defaulting and validating hooks in one mutatingWebhook. But doing it this way is not safe, since there may be another mutating webhook changes the object after your validation logic. The safe way to do it is making validation logic in a validating webhook, which is always run after all mutating webhooks.
6c379c1 to
49dca2b
Compare
|
|
| } | ||
|
|
||
| func (blder *Builder) doWebhook() error { | ||
| if blder.mgr.GetScheme() == nil { |
There was a problem hiding this comment.
we default the scheme, no? This should never be nil
There was a problem hiding this comment.
Was being more defensive here. But if you think it's not necessary at all, I will drop it.
|
|
||
| appsv1 "k8s.io/api/apps/v1" | ||
| _ "k8s.io/client-go/plugin/pkg/client/auth/gcp" | ||
| "sigs.k8s.io/controller-runtime/examples/crd/pkg" |
There was a problem hiding this comment.
let's use ctrl "sigs.k8s.io/controller-runtime" instead of most of these in the examples
| @@ -0,0 +1,17 @@ | |||
| # Build the manager binary | |||
There was a problem hiding this comment.
don't think we should have the deployment stuff here -- this should be only code. Deployment examples can live in KB
There was a problem hiding this comment.
Ack. Will polish the CRD example and remote deployment bits in the example.
| var _ admission.Validator = &FirstMate{} | ||
|
|
||
| // ValidateCreate implements webhookutil.validator so a webhook will be registered for the type | ||
| func (f *FirstMate) ValidateCreate() error { |
There was a problem hiding this comment.
should have webhook generation annotations?
|
PTAL |
|
Rebased on HEAD to resolved the conflicts and squashed some commits. |
| gvk.Version + "-" + strings.ToLower(gvk.Kind) | ||
|
|
||
| defaulter, isDefaulter := blder.apiType.(admission.Defaulter) | ||
| if isDefaulter { |
There was a problem hiding this comment.
nit: collapse this into a compound statement.
| } | ||
| } | ||
|
|
||
| validator, isValidator := blder.apiType.(admission.Validator) |
There was a problem hiding this comment.
nit: collapse this into a compound statement
| vwh := admission.ValidatingWebhookFor(validator) | ||
| if vwh != nil { | ||
| path := "/validate-" + partialPath | ||
| log.Info("registering a validating webhook", "path", path) |
|
/lgtm Can you follow up with a PR with slightly more comprehensive tests? |
|
[APPROVALNOTIFIER] This PR is APPROVED This pull-request has been approved by: DirectXMan12, mengqiy The full list of commands accepted by this bot can be found here. The pull request process is described here DetailsNeeds approval from an approver in each of these files:
Approvers can indicate their approval by writing |
Sure! |
If user defines a CRD type and make it implement the Defaulter and (or) the Validator interface, it will automatically wire a webhook server for the admission webhooks.
|
New changes are detected. LGTM label has been removed. |
|
Forgot to remove the Re-applying the label per #328 (comment), since there is no actual code changes. |
If a CRD type implements the
DefaulterandValidatorinterface, we will generate the admission webhook for the CRD type.For non-CRD type, we provide methods to pass in path and handlers to create the mutating|validting webhook.
godoc and tests are on the way.