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

⚠️ Simplify webhook wiring with Defaulter and Validator interfaces #328

Merged
merged 5 commits into from
Mar 11, 2019

Conversation

mengqiy
Copy link
Member

@mengqiy mengqiy commented Feb 14, 2019

If a CRD type implements the Defaulter and Validator interface, 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.

@k8s-ci-robot k8s-ci-robot added the cncf-cla: yes Indicates the PR's author has signed the CNCF CLA. label Feb 14, 2019
@k8s-ci-robot k8s-ci-robot added the size/XXL Denotes a PR that changes 1000+ lines, ignoring generated files. label Feb 14, 2019
Copy link
Contributor

@droot droot left a comment

Choose a reason for hiding this comment

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

This is looking good.

if wh == nil {
continue
}
s.Register("path", wh)
Copy link
Contributor

Choose a reason for hiding this comment

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

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 {
Copy link
Contributor

Choose a reason for hiding this comment

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

For name itself doesn't convey the intent. Wondering if we should give it a more friendly name like EnableValidationFor or EnableDefaultsFor ?

@k8s-ci-robot k8s-ci-robot added size/XL Denotes a PR that changes 500-999 lines, ignoring generated files. and removed size/XXL Denotes a PR that changes 1000+ lines, ignoring generated files. labels Feb 22, 2019
@mengqiy
Copy link
Member Author

mengqiy commented Feb 22, 2019

Addressed comments.
PTAL
If looks OK, I will push more tests and godoc.

Copy link
Contributor

@droot droot left a comment

Choose a reason for hiding this comment

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

The change looks good, have a few comments.

crdexample/pkg/resource.go Outdated Show resolved Hide resolved
out := new(FirstMateStatus)
in.DeepCopyInto(out)
return out
}
Copy link
Contributor

Choose a reason for hiding this comment

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

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 ?

pkg/builder/build.go Outdated Show resolved Hide resolved
if err != nil {
return nil, err
}
err = svr.EnableValidationFor(blder.mgr.GetScheme(), blder.apiType)
Copy link
Contributor

Choose a reason for hiding this comment

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

Won't svr have access to the scheme ? If yes, then we can get rid of scheme parameter from EnableDefaultsFor and EnableValidationFor.

Copy link
Member Author

@mengqiy mengqiy Feb 22, 2019

Choose a reason for hiding this comment

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

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.

pkg/manager/internal.go Show resolved Hide resolved
return err
}
if len(gvks) != 1 {
return fmt.Errorf("expected only GVK returned by scheme.ObjectKinds")
Copy link
Contributor

Choose a reason for hiding this comment

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

we should some webhook related context to the error message and also include all the GVKs to help in debugging.

pkg/webhook/server.go Outdated Show resolved Hide resolved
s.Register("/validate-"+generatePath(gvks[0]), wh)
}
return nil
}
Copy link
Contributor

Choose a reason for hiding this comment

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

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.

pkg/webhook/server.go Outdated Show resolved Hide resolved
pkg/webhook/server.go Outdated Show resolved Hide resolved
@mengqiy
Copy link
Member Author

mengqiy commented Feb 25, 2019

Addressed comments. PTAL at the interfaces.
2 things are still open for discussion:

@DirectXMan12 WDYT

Copy link
Contributor

@droot droot left a comment

Choose a reason for hiding this comment

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

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)
Copy link
Contributor

Choose a reason for hiding this comment

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

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)
Copy link
Contributor

Choose a reason for hiding this comment

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

+1 on setting decoder this way.

crdexample/controller.go Outdated Show resolved Hide resolved
crdexample/logutil/log.go Outdated Show resolved Hide resolved
var fmLog = logutil.Log.WithName("firstmate-reconciler")

// FirstMateController reconciles ReplicaSets
type FirstMateController struct {
Copy link
Contributor

Choose a reason for hiding this comment

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

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"
Copy link
Contributor

Choose a reason for hiding this comment

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

lets switch these over to using the alias package (ctrl "sigs.k8s.io/controller-runtime") as opposed to a bajillion separate packages.

crdexample/pkg/resource.go Outdated Show resolved Hide resolved
@@ -35,7 +35,7 @@ func NewDecoder(scheme *runtime.Scheme) (*Decoder, error) {
}

// 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 {
Copy link
Contributor

Choose a reason for hiding this comment

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

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 {
Copy link
Contributor

Choose a reason for hiding this comment

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

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, ".")
Copy link
Contributor

Choose a reason for hiding this comment

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

why aren't we just strings.Replace(gvk.Group, ".", "-") or something?

Copy link
Member Author

@mengqiy mengqiy Feb 28, 2019

Choose a reason for hiding this comment

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

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?

Copy link
Contributor

Choose a reason for hiding this comment

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

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

pkg/webhook/validator.go Outdated Show resolved Hide resolved
return nil
}

mwh := newDefaultingWebhookFor(object)
Copy link
Contributor

Choose a reason for hiding this comment

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

we should opportunistically merge validating and defaulting hooks into a single hook when possible, to avoid the extra round-trip

Copy link
Member Author

Choose a reason for hiding this comment

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

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.

Copy link
Contributor

Choose a reason for hiding this comment

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

good point 👍

@DirectXMan12 DirectXMan12 changed the title Simplify the webhook interface ⚠️ Simplify the webhook interface Feb 27, 2019
@DirectXMan12 DirectXMan12 added this to the 0.2.0 milestone Feb 27, 2019
@mengqiy
Copy link
Member Author

mengqiy commented Feb 28, 2019

pkg dir is ready for another round of reviewing interfaces.
I haven't make a lot of changes in examples/crd. I'd like to discuss with @DirectXMan12 more before spending more than time on it.

@@ -218,3 +240,60 @@ func (blder *Builder) doController(r reconcile.Reconciler) error {
blder.ctrl, err = newController(name, blder.mgr, controller.Options{Reconciler: r})
return err
}

func (blder *Builder) doWebhook() error {
if blder.mgr.GetScheme() == nil {
Copy link
Contributor

Choose a reason for hiding this comment

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

we default the scheme, no? This should never be nil

Copy link
Member Author

Choose a reason for hiding this comment

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

Was being more defensive here. But if you think it's not necessary at all, I will drop it.

pkg/builder/build.go Outdated Show resolved Hide resolved
pkg/builder/build.go Outdated Show resolved Hide resolved
pkg/webhook/admission/decode.go Outdated Show resolved Hide resolved
pkg/webhook/admission/validator.go Show resolved Hide resolved
pkg/webhook/server.go Outdated Show resolved Hide resolved
pkg/webhook/admission/defaulter.go Outdated Show resolved Hide resolved

appsv1 "k8s.io/api/apps/v1"
_ "k8s.io/client-go/plugin/pkg/client/auth/gcp"
"sigs.k8s.io/controller-runtime/examples/crd/pkg"
Copy link
Contributor

Choose a reason for hiding this comment

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

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
Copy link
Contributor

Choose a reason for hiding this comment

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

don't think we should have the deployment stuff here -- this should be only code. Deployment examples can live in KB

Copy link
Member Author

@mengqiy mengqiy Mar 1, 2019

Choose a reason for hiding this comment

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

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 {
Copy link
Contributor

Choose a reason for hiding this comment

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

should have webhook generation annotations?

@mengqiy mengqiy added the do-not-merge/hold Indicates that a PR should not merge because someone has issued a /hold command. label Mar 4, 2019
@mengqiy
Copy link
Member Author

mengqiy commented Mar 4, 2019

PTAL

@mengqiy mengqiy removed the do-not-merge/hold Indicates that a PR should not merge because someone has issued a /hold command. label Mar 8, 2019
@mengqiy
Copy link
Member Author

mengqiy commented Mar 8, 2019

Rebased on HEAD to resolved the conflicts and squashed some commits.
Ready for the final round of review for API changes e.g. pkg/manager/manager.go

@mengqiy mengqiy added the do-not-merge/hold Indicates that a PR should not merge because someone has issued a /hold command. label Mar 8, 2019
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.

minor nits inline

gvk.Version + "-" + strings.ToLower(gvk.Kind)

defaulter, isDefaulter := blder.apiType.(admission.Defaulter)
if isDefaulter {
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: collapse this into a compound statement.

}
}

validator, isValidator := blder.apiType.(admission.Validator)
Copy link
Contributor

Choose a reason for hiding this comment

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

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)
Copy link
Contributor

Choose a reason for hiding this comment

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

log the GVK?

pkg/builder/build.go Show resolved Hide resolved
@DirectXMan12
Copy link
Contributor

/lgtm
/approve

Can you follow up with a PR with slightly more comprehensive tests?

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

[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

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 Mar 9, 2019
@mengqiy
Copy link
Member Author

mengqiy commented Mar 9, 2019

Can you follow up with a PR with slightly more comprehensive tests?

Sure!

@mengqiy mengqiy removed the do-not-merge/hold Indicates that a PR should not merge because someone has issued a /hold command. label Mar 11, 2019
Mengqi Yu added 5 commits March 11, 2019 10:16
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.
@k8s-ci-robot
Copy link
Contributor

New changes are detected. LGTM label has been removed.

@k8s-ci-robot k8s-ci-robot removed the lgtm "Looks good to me", indicates that a PR is ready to be merged. label Mar 11, 2019
@mengqiy
Copy link
Member Author

mengqiy commented Mar 11, 2019

Forgot to remove the do-not-merge label last Friday, and this PR ended up with a small merge conflict with #352 in the comments.
I rebased and resolved conflicts.

Re-applying the label per #328 (comment), since there is no actual code changes.

@mengqiy mengqiy added the lgtm "Looks good to me", indicates that a PR is ready to be merged. label Mar 11, 2019
@k8s-ci-robot k8s-ci-robot merged commit 8810470 into kubernetes-sigs:master Mar 11, 2019
@mengqiy mengqiy deleted the simplifywhif branch March 12, 2019 04:08
@mengqiy mengqiy changed the title ⚠️ Simplify the webhook interface ⚠️ Simplify webhook wiring with Defaulter and Validator interfaces Sep 5, 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. lgtm "Looks good to me", indicates that a PR is ready to be merged. 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.

None yet

4 participants